UNPKG

955 kBJavaScriptView Raw
1/**
2 * @license Highmaps JS v5.0.5 (2016-11-29)
3 *
4 * (c) 2011-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: 'Highmaps',
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 color = H.color,
2234 each = H.each,
2235 getTZOffset = H.getTZOffset,
2236 isTouchDevice = H.isTouchDevice,
2237 merge = H.merge,
2238 pick = H.pick,
2239 svg = H.svg,
2240 win = H.win;
2241
2242 /* ****************************************************************************
2243 * Handle the options *
2244 *****************************************************************************/
2245 H.defaultOptions = {
2246
2247 colors: '#7cb5ec #434348 #90ed7d #f7a35c #8085e9 #f15c80 #e4d354 #2b908f #f45b5b #91e8e1'.split(' '),
2248
2249 symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'],
2250 lang: {
2251 loading: 'Loading...',
2252 months: ['January', 'February', 'March', 'April', 'May', 'June', 'July',
2253 'August', 'September', 'October', 'November', 'December'
2254 ],
2255 shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
2256 weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
2257 // invalidDate: '',
2258 decimalPoint: '.',
2259 numericSymbols: ['k', 'M', 'G', 'T', 'P', 'E'], // SI prefixes used in axis labels
2260 resetZoom: 'Reset zoom',
2261 resetZoomTitle: 'Reset zoom level 1:1',
2262 thousandsSep: ' '
2263 },
2264 global: {
2265 useUTC: true,
2266 //timezoneOffset: 0,
2267
2268 VMLRadialGradientURL: 'http://code.highcharts.com/5.0.5/gfx/vml-radial-gradient.png'
2269
2270 },
2271 chart: {
2272 //animation: true,
2273 //alignTicks: false,
2274 //reflow: true,
2275 //className: null,
2276 //events: { load, selection },
2277 //margin: [null],
2278 //marginTop: null,
2279 //marginRight: null,
2280 //marginBottom: null,
2281 //marginLeft: null,
2282 borderRadius: 0,
2283
2284 defaultSeriesType: 'line',
2285 ignoreHiddenSeries: true,
2286 //inverted: false,
2287 spacing: [10, 10, 15, 10],
2288 //spacingTop: 10,
2289 //spacingRight: 10,
2290 //spacingBottom: 15,
2291 //spacingLeft: 10,
2292 //zoomType: ''
2293 resetZoomButton: {
2294 theme: {
2295 zIndex: 20
2296 },
2297 position: {
2298 align: 'right',
2299 x: -10,
2300 //verticalAlign: 'top',
2301 y: 10
2302 }
2303 // relativeTo: 'plot'
2304 },
2305 width: null,
2306 height: null,
2307
2308
2309 borderColor: '#335cad',
2310 //borderWidth: 0,
2311 //style: {
2312 // fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font
2313 // fontSize: '12px'
2314 //},
2315 backgroundColor: '#ffffff',
2316 //plotBackgroundColor: null,
2317 plotBorderColor: '#cccccc'
2318 //plotBorderWidth: 0,
2319 //plotShadow: false
2320
2321 },
2322 title: {
2323 text: 'Chart title',
2324 align: 'center',
2325 // floating: false,
2326 margin: 15,
2327 // x: 0,
2328 // verticalAlign: 'top',
2329 // y: null,
2330 // style: {}, // defined inline
2331 widthAdjust: -44
2332
2333 },
2334 subtitle: {
2335 text: '',
2336 align: 'center',
2337 // floating: false
2338 // x: 0,
2339 // verticalAlign: 'top',
2340 // y: null,
2341 // style: {}, // defined inline
2342 widthAdjust: -44
2343 },
2344
2345 plotOptions: {},
2346 labels: {
2347 //items: [],
2348 style: {
2349 //font: defaultFont,
2350 position: 'absolute',
2351 color: '#333333'
2352 }
2353 },
2354 legend: {
2355 enabled: true,
2356 align: 'center',
2357 //floating: false,
2358 layout: 'horizontal',
2359 labelFormatter: function() {
2360 return this.name;
2361 },
2362 //borderWidth: 0,
2363 borderColor: '#999999',
2364 borderRadius: 0,
2365 navigation: {
2366
2367 activeColor: '#003399',
2368 inactiveColor: '#cccccc'
2369
2370 // animation: true,
2371 // arrowSize: 12
2372 // style: {} // text styles
2373 },
2374 // margin: 20,
2375 // reversed: false,
2376 // backgroundColor: null,
2377 /*style: {
2378 padding: '5px'
2379 },*/
2380
2381 itemStyle: {
2382 color: '#333333',
2383 fontSize: '12px',
2384 fontWeight: 'bold'
2385 },
2386 itemHoverStyle: {
2387 //cursor: 'pointer', removed as of #601
2388 color: '#000000'
2389 },
2390 itemHiddenStyle: {
2391 color: '#cccccc'
2392 },
2393 shadow: false,
2394
2395 itemCheckboxStyle: {
2396 position: 'absolute',
2397 width: '13px', // for IE precision
2398 height: '13px'
2399 },
2400 // itemWidth: undefined,
2401 squareSymbol: true,
2402 // symbolRadius: 0,
2403 // symbolWidth: 16,
2404 symbolPadding: 5,
2405 verticalAlign: 'bottom',
2406 // width: undefined,
2407 x: 0,
2408 y: 0,
2409 title: {
2410 //text: null,
2411
2412 style: {
2413 fontWeight: 'bold'
2414 }
2415
2416 }
2417 },
2418
2419 loading: {
2420 // hideDuration: 100,
2421 // showDuration: 0,
2422
2423 labelStyle: {
2424 fontWeight: 'bold',
2425 position: 'relative',
2426 top: '45%'
2427 },
2428 style: {
2429 position: 'absolute',
2430 backgroundColor: '#ffffff',
2431 opacity: 0.5,
2432 textAlign: 'center'
2433 }
2434
2435 },
2436
2437 tooltip: {
2438 enabled: true,
2439 animation: svg,
2440 //crosshairs: null,
2441 borderRadius: 3,
2442 dateTimeLabelFormats: {
2443 millisecond: '%A, %b %e, %H:%M:%S.%L',
2444 second: '%A, %b %e, %H:%M:%S',
2445 minute: '%A, %b %e, %H:%M',
2446 hour: '%A, %b %e, %H:%M',
2447 day: '%A, %b %e, %Y',
2448 week: 'Week from %A, %b %e, %Y',
2449 month: '%B %Y',
2450 year: '%Y'
2451 },
2452 footerFormat: '',
2453 //formatter: defaultFormatter,
2454 /* todo: em font-size when finished comparing against HC4
2455 headerFormat: '<span style="font-size: 0.85em">{point.key}</span><br/>',
2456 */
2457 padding: 8,
2458
2459 //shape: 'callout',
2460 //shared: false,
2461 snap: isTouchDevice ? 25 : 10,
2462
2463 backgroundColor: color('#f7f7f7').setOpacity(0.85).get(),
2464 borderWidth: 1,
2465 headerFormat: '<span style="font-size: 10px">{point.key}</span><br/>',
2466 pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b><br/>',
2467 shadow: true,
2468 style: {
2469 color: '#333333',
2470 cursor: 'default',
2471 fontSize: '12px',
2472 pointerEvents: 'none', // #1686 http://caniuse.com/#feat=pointer-events
2473 whiteSpace: 'nowrap'
2474 }
2475
2476 //xDateFormat: '%A, %b %e, %Y',
2477 //valueDecimals: null,
2478 //valuePrefix: '',
2479 //valueSuffix: ''
2480 },
2481
2482 credits: {
2483 enabled: true,
2484 href: 'http://www.highcharts.com',
2485 position: {
2486 align: 'right',
2487 x: -10,
2488 verticalAlign: 'bottom',
2489 y: -5
2490 },
2491
2492 style: {
2493 cursor: 'pointer',
2494 color: '#999999',
2495 fontSize: '9px'
2496 },
2497
2498 text: 'Highcharts.com'
2499 }
2500 };
2501
2502
2503
2504 /**
2505 * Set the time methods globally based on the useUTC option. Time method can be
2506 * either local time or UTC (default). It is called internally on initiating
2507 * Highcharts and after running `Highcharts.setOptions`.
2508 *
2509 * @private
2510 */
2511 function setTimeMethods() {
2512 var globalOptions = H.defaultOptions.global,
2513 Date,
2514 useUTC = globalOptions.useUTC,
2515 GET = useUTC ? 'getUTC' : 'get',
2516 SET = useUTC ? 'setUTC' : 'set';
2517
2518 H.Date = Date = globalOptions.Date || win.Date; // Allow using a different Date class
2519 Date.hcTimezoneOffset = useUTC && globalOptions.timezoneOffset;
2520 Date.hcGetTimezoneOffset = useUTC && globalOptions.getTimezoneOffset;
2521 Date.hcMakeTime = function(year, month, date, hours, minutes, seconds) {
2522 var d;
2523 if (useUTC) {
2524 d = Date.UTC.apply(0, arguments);
2525 d += getTZOffset(d);
2526 } else {
2527 d = new Date(
2528 year,
2529 month,
2530 pick(date, 1),
2531 pick(hours, 0),
2532 pick(minutes, 0),
2533 pick(seconds, 0)
2534 ).getTime();
2535 }
2536 return d;
2537 };
2538 each(['Minutes', 'Hours', 'Day', 'Date', 'Month', 'FullYear'], function(s) {
2539 Date['hcGet' + s] = GET + s;
2540 });
2541 each(['Milliseconds', 'Seconds', 'Minutes', 'Hours', 'Date', 'Month', 'FullYear'], function(s) {
2542 Date['hcSet' + s] = SET + s;
2543 });
2544 }
2545
2546 /**
2547 * Merge the default options with custom options and return the new options structure
2548 * @param {Object} options The new custom options
2549 */
2550 H.setOptions = function(options) {
2551
2552 // Copy in the default options
2553 H.defaultOptions = merge(true, H.defaultOptions, options);
2554
2555 // Apply UTC
2556 setTimeMethods();
2557
2558 return H.defaultOptions;
2559 };
2560
2561 /**
2562 * Get the updated default options. Until 3.0.7, merely exposing defaultOptions for outside modules
2563 * wasn't enough because the setOptions method created a new object.
2564 */
2565 H.getOptions = function() {
2566 return H.defaultOptions;
2567 };
2568
2569
2570 // Series defaults
2571 H.defaultPlotOptions = H.defaultOptions.plotOptions;
2572
2573 // set the default time methods
2574 setTimeMethods();
2575
2576 }(Highcharts));
2577 (function(H) {
2578 /**
2579 * (c) 2010-2016 Torstein Honsi
2580 *
2581 * License: www.highcharts.com/license
2582 */
2583 'use strict';
2584 var SVGElement,
2585 SVGRenderer,
2586
2587 addEvent = H.addEvent,
2588 animate = H.animate,
2589 attr = H.attr,
2590 charts = H.charts,
2591 color = H.color,
2592 css = H.css,
2593 createElement = H.createElement,
2594 defined = H.defined,
2595 deg2rad = H.deg2rad,
2596 destroyObjectProperties = H.destroyObjectProperties,
2597 doc = H.doc,
2598 each = H.each,
2599 extend = H.extend,
2600 erase = H.erase,
2601 grep = H.grep,
2602 hasTouch = H.hasTouch,
2603 isArray = H.isArray,
2604 isFirefox = H.isFirefox,
2605 isMS = H.isMS,
2606 isObject = H.isObject,
2607 isString = H.isString,
2608 isWebKit = H.isWebKit,
2609 merge = H.merge,
2610 noop = H.noop,
2611 pick = H.pick,
2612 pInt = H.pInt,
2613 removeEvent = H.removeEvent,
2614 splat = H.splat,
2615 stop = H.stop,
2616 svg = H.svg,
2617 SVG_NS = H.SVG_NS,
2618 symbolSizes = H.symbolSizes,
2619 win = H.win;
2620
2621 /**
2622 * @typedef {Object} SVGDOMElement - An SVG DOM element.
2623 */
2624 /**
2625 * The SVGElement prototype is a JavaScript wrapper for SVG elements used in the
2626 * rendering layer of Highcharts. Combined with the {@link SVGRenderer} object,
2627 * these prototypes allow freeform annotation in the charts or even in HTML
2628 * pages without instanciating a chart. The SVGElement can also wrap HTML
2629 * labels, when `text` or `label` elements are created with the `useHTML`
2630 * parameter.
2631 *
2632 * The SVGElement instances are created through factory functions on the
2633 * {@link SVGRenderer} object, like [rect]{@link SVGRenderer#rect},
2634 * [path]{@link SVGRenderer#path}, [text]{@link SVGRenderer#text}, [label]{@link
2635 * SVGRenderer#label}, [g]{@link SVGRenderer#g} and more.
2636 *
2637 * @class
2638 */
2639 SVGElement = H.SVGElement = function() {
2640 return this;
2641 };
2642 SVGElement.prototype = {
2643
2644 // Default base for animation
2645 opacity: 1,
2646 SVG_NS: SVG_NS,
2647
2648 /**
2649 * For labels, these CSS properties are applied to the `text` node directly.
2650 * @type {Array.<string>}
2651 */
2652 textProps: ['direction', 'fontSize', 'fontWeight', 'fontFamily',
2653 'fontStyle', 'color', 'lineHeight', 'width', 'textDecoration',
2654 'textOverflow', 'textOutline'
2655 ],
2656
2657 /**
2658 * Initialize the SVG renderer. This function only exists to make the
2659 * initiation process overridable. It should not be called directly.
2660 *
2661 * @param {SVGRenderer} renderer The SVGRenderer instance to initialize to.
2662 * @param {String} nodeName The SVG node name.
2663 * @returns {void}
2664 */
2665 init: function(renderer, nodeName) {
2666
2667 /**
2668 * The DOM node. Each SVGRenderer instance wraps a main DOM node, but
2669 * may also represent more nodes.
2670 * @type {SVGDOMNode|HTMLDOMNode}
2671 */
2672 this.element = nodeName === 'span' ?
2673 createElement(nodeName) :
2674 doc.createElementNS(this.SVG_NS, nodeName);
2675
2676 /**
2677 * The renderer that the SVGElement belongs to.
2678 * @type {SVGRenderer}
2679 */
2680 this.renderer = renderer;
2681 },
2682
2683 /**
2684 * Animate to given attributes or CSS properties.
2685 *
2686 * @param {SVGAttributes} params SVG attributes or CSS to animate.
2687 * @param {AnimationOptions} [options] Animation options.
2688 * @param {Function} [complete] Function to perform at the end of animation.
2689 * @returns {SVGElement} Returns the SVGElement for chaining.
2690 */
2691 animate: function(params, options, complete) {
2692 var animOptions = pick(options, this.renderer.globalAnimation, true);
2693 if (animOptions) {
2694 if (complete) { // allows using a callback with the global animation without overwriting it
2695 animOptions.complete = complete;
2696 }
2697 animate(this, params, animOptions);
2698 } else {
2699 this.attr(params, null, complete);
2700 }
2701 return this;
2702 },
2703
2704 /**
2705 * @typedef {Object} GradientOptions
2706 * @property {Object} linearGradient Holds an object that defines the start
2707 * position and the end position relative to the shape.
2708 * @property {Number} linearGradient.x1 Start horizontal position of the
2709 * gradient. Ranges 0-1.
2710 * @property {Number} linearGradient.x2 End horizontal position of the
2711 * gradient. Ranges 0-1.
2712 * @property {Number} linearGradient.y1 Start vertical position of the
2713 * gradient. Ranges 0-1.
2714 * @property {Number} linearGradient.y2 End vertical position of the
2715 * gradient. Ranges 0-1.
2716 * @property {Object} radialGradient Holds an object that defines the center
2717 * position and the radius.
2718 * @property {Number} radialGradient.cx Center horizontal position relative
2719 * to the shape. Ranges 0-1.
2720 * @property {Number} radialGradient.cy Center vertical position relative
2721 * to the shape. Ranges 0-1.
2722 * @property {Number} radialGradient.r Radius relative to the shape. Ranges
2723 * 0-1.
2724 * @property {Array.<Array>} stops The first item in each tuple is the
2725 * position in the gradient, where 0 is the start of the gradient and 1
2726 * is the end of the gradient. Multiple stops can be applied. The second
2727 * item is the color for each stop. This color can also be given in the
2728 * rgba format.
2729 *
2730 * @example
2731 * // Linear gradient used as a color option
2732 * color: {
2733 * linearGradient: { x1: 0, x2: 0, y1: 0, y2: 1 },
2734 * stops: [
2735 * [0, '#003399'], // start
2736 * [0.5, '#ffffff'], // middle
2737 * [1, '#3366AA'] // end
2738 * ]
2739 * }
2740 * }
2741 */
2742 /**
2743 * Build and apply an SVG gradient out of a common JavaScript configuration
2744 * object. This function is called from the attribute setters.
2745 *
2746 * @private
2747 * @param {GradientOptions} color The gradient options structure.
2748 * @param {string} prop The property to apply, can either be `fill` or
2749 * `stroke`.
2750 * @param {SVGDOMElement} elem SVG DOM element to apply the gradient on.
2751 */
2752 colorGradient: function(color, prop, elem) {
2753 var renderer = this.renderer,
2754 colorObject,
2755 gradName,
2756 gradAttr,
2757 radAttr,
2758 gradients,
2759 gradientObject,
2760 stops,
2761 stopColor,
2762 stopOpacity,
2763 radialReference,
2764 n,
2765 id,
2766 key = [],
2767 value;
2768
2769 // Apply linear or radial gradients
2770 if (color.linearGradient) {
2771 gradName = 'linearGradient';
2772 } else if (color.radialGradient) {
2773 gradName = 'radialGradient';
2774 }
2775
2776 if (gradName) {
2777 gradAttr = color[gradName];
2778 gradients = renderer.gradients;
2779 stops = color.stops;
2780 radialReference = elem.radialReference;
2781
2782 // Keep < 2.2 kompatibility
2783 if (isArray(gradAttr)) {
2784 color[gradName] = gradAttr = {
2785 x1: gradAttr[0],
2786 y1: gradAttr[1],
2787 x2: gradAttr[2],
2788 y2: gradAttr[3],
2789 gradientUnits: 'userSpaceOnUse'
2790 };
2791 }
2792
2793 // Correct the radial gradient for the radial reference system
2794 if (gradName === 'radialGradient' && radialReference && !defined(gradAttr.gradientUnits)) {
2795 radAttr = gradAttr; // Save the radial attributes for updating
2796 gradAttr = merge(gradAttr,
2797 renderer.getRadialAttr(radialReference, radAttr), {
2798 gradientUnits: 'userSpaceOnUse'
2799 }
2800 );
2801 }
2802
2803 // Build the unique key to detect whether we need to create a new element (#1282)
2804 for (n in gradAttr) {
2805 if (n !== 'id') {
2806 key.push(n, gradAttr[n]);
2807 }
2808 }
2809 for (n in stops) {
2810 key.push(stops[n]);
2811 }
2812 key = key.join(',');
2813
2814 // Check if a gradient object with the same config object is created within this renderer
2815 if (gradients[key]) {
2816 id = gradients[key].attr('id');
2817
2818 } else {
2819
2820 // Set the id and create the element
2821 gradAttr.id = id = H.uniqueKey();
2822 gradients[key] = gradientObject = renderer.createElement(gradName)
2823 .attr(gradAttr)
2824 .add(renderer.defs);
2825
2826 gradientObject.radAttr = radAttr;
2827
2828 // The gradient needs to keep a list of stops to be able to destroy them
2829 gradientObject.stops = [];
2830 each(stops, function(stop) {
2831 var stopObject;
2832 if (stop[1].indexOf('rgba') === 0) {
2833 colorObject = H.color(stop[1]);
2834 stopColor = colorObject.get('rgb');
2835 stopOpacity = colorObject.get('a');
2836 } else {
2837 stopColor = stop[1];
2838 stopOpacity = 1;
2839 }
2840 stopObject = renderer.createElement('stop').attr({
2841 offset: stop[0],
2842 'stop-color': stopColor,
2843 'stop-opacity': stopOpacity
2844 }).add(gradientObject);
2845
2846 // Add the stop element to the gradient
2847 gradientObject.stops.push(stopObject);
2848 });
2849 }
2850
2851 // Set the reference to the gradient object
2852 value = 'url(' + renderer.url + '#' + id + ')';
2853 elem.setAttribute(prop, value);
2854 elem.gradient = key;
2855
2856 // Allow the color to be concatenated into tooltips formatters etc. (#2995)
2857 color.toString = function() {
2858 return value;
2859 };
2860 }
2861 },
2862
2863 /**
2864 * Apply a text outline through a custom CSS property, by copying the text
2865 * element and apply stroke to the copy. Used internally. Contrast checks
2866 * at http://jsfiddle.net/highcharts/43soe9m1/2/ .
2867 *
2868 * @private
2869 * @param {String} textOutline A custom CSS `text-outline` setting, defined
2870 * by `width color`.
2871 * @example
2872 * // Specific color
2873 * text.css({
2874 * textOutline: '1px black'
2875 * });
2876 * // Automatic contrast
2877 * text.css({
2878 * color: '#000000', // black text
2879 * textOutline: '1px contrast' // => white outline
2880 * });
2881 */
2882 applyTextOutline: function(textOutline) {
2883 var elem = this.element,
2884 tspans,
2885 hasContrast = textOutline.indexOf('contrast') !== -1,
2886 styles = {},
2887 color,
2888 strokeWidth;
2889
2890 // When the text shadow is set to contrast, use dark stroke for light
2891 // text and vice versa.
2892 if (hasContrast) {
2893 styles.textOutline = textOutline = textOutline.replace(
2894 /contrast/g,
2895 this.renderer.getContrast(elem.style.fill)
2896 );
2897 }
2898
2899 this.fakeTS = true; // Fake text shadow
2900
2901 // In order to get the right y position of the clone,
2902 // copy over the y setter
2903 this.ySetter = this.xSetter;
2904
2905 tspans = [].slice.call(elem.getElementsByTagName('tspan'));
2906
2907 // Extract the stroke width and color
2908 textOutline = textOutline.split(' ');
2909 color = textOutline[textOutline.length - 1];
2910 strokeWidth = textOutline[0];
2911
2912 if (strokeWidth && strokeWidth !== 'none') {
2913
2914 // Since the stroke is applied on center of the actual outline, we
2915 // need to double it to get the correct stroke-width outside the
2916 // glyphs.
2917 strokeWidth = strokeWidth.replace(
2918 /(^[\d\.]+)(.*?)$/g,
2919 function(match, digit, unit) {
2920 return (2 * digit) + unit;
2921 }
2922 );
2923
2924 // Remove shadows from previous runs
2925 each(tspans, function(tspan) {
2926 if (tspan.getAttribute('class') === 'highcharts-text-outline') {
2927 // Remove then erase
2928 erase(tspans, elem.removeChild(tspan));
2929 }
2930 });
2931
2932 // For each of the tspans, create a stroked copy behind it.
2933 each(tspans, function(tspan, y) {
2934 var clone;
2935
2936 // Let the first line start at the correct X position
2937 if (y === 0) {
2938 tspan.setAttribute('x', elem.getAttribute('x'));
2939 y = elem.getAttribute('y');
2940 tspan.setAttribute('y', y || 0);
2941 if (y === null) {
2942 elem.setAttribute('y', 0);
2943 }
2944 }
2945
2946 // Create the clone and apply outline properties
2947 clone = tspan.cloneNode(1);
2948 attr(clone, {
2949 'class': 'highcharts-text-outline',
2950 'fill': color,
2951 'stroke': color,
2952 'stroke-width': strokeWidth,
2953 'stroke-linejoin': 'round'
2954 });
2955 elem.insertBefore(clone, elem.firstChild);
2956 });
2957 }
2958 },
2959
2960 /**
2961 *
2962 * @typedef {Object} SVGAttributes An object of key-value pairs for SVG
2963 * attributes. Attributes in Highcharts elements for the most parts
2964 * correspond to SVG, but some are specific to Highcharts, like `zIndex`,
2965 * `rotation`, `translateX`, `translateY`, `scaleX` and `scaleY`. SVG
2966 * attributes containing a hyphen are _not_ camel-cased, they should be
2967 * quoted to preserve the hyphen.
2968 * @example
2969 * {
2970 * 'stroke': '#ff0000', // basic
2971 * 'stroke-width': 2, // hyphenated
2972 * 'rotation': 45 // custom
2973 * 'd': ['M', 10, 10, 'L', 30, 30, 'z'] // path definition, note format
2974 * }
2975 */
2976 /**
2977 * Apply native and custom attributes to the SVG elements.
2978 *
2979 * In order to set the rotation center for rotation, set x and y to 0 and
2980 * use `translateX` and `translateY` attributes to position the element
2981 * instead.
2982 *
2983 * Attributes frequently used in Highcharts are `fill`, `stroke`,
2984 * `stroke-width`.
2985 *
2986 * @param {SVGAttributes|String} hash - The native and custom SVG
2987 * attributes.
2988 * @param {string} [val] - If the type of the first argument is `string`,
2989 * the second can be a value, which will serve as a single attribute
2990 * setter. If the first argument is a string and the second is undefined,
2991 * the function serves as a getter and the current value of the property
2992 * is returned.
2993 * @param {Function} complete - A callback function to execute after setting
2994 * the attributes. This makes the function compliant and interchangeable
2995 * with the {@link SVGElement#animate} function.
2996 * @param {boolean} continueAnimation - Used internally when `.attr` is
2997 * called as part of an animation step. Otherwise, calling `.attr` for an
2998 * attribute will stop animation for that attribute.
2999 *
3000 * @returns {SVGElement|string|number} If used as a setter, it returns the
3001 * current {@link SVGElement} so the calls can be chained. If used as a
3002 * getter, the current value of the attribute is returned.
3003 *
3004 * @example
3005 * // Set multiple attributes
3006 * element.attr({
3007 * stroke: 'red',
3008 * fill: 'blue',
3009 * x: 10,
3010 * y: 10
3011 * });
3012 *
3013 * // Set a single attribute
3014 * element.attr('stroke', 'red');
3015 *
3016 * // Get an attribute
3017 * element.attr('stroke'); // => 'red'
3018 *
3019 */
3020 attr: function(hash, val, complete, continueAnimation) {
3021 var key,
3022 value,
3023 element = this.element,
3024 hasSetSymbolSize,
3025 ret = this,
3026 skipAttr,
3027 setter;
3028
3029 // single key-value pair
3030 if (typeof hash === 'string' && val !== undefined) {
3031 key = hash;
3032 hash = {};
3033 hash[key] = val;
3034 }
3035
3036 // used as a getter: first argument is a string, second is undefined
3037 if (typeof hash === 'string') {
3038 ret = (this[hash + 'Getter'] || this._defaultGetter).call(this, hash, element);
3039
3040 // setter
3041 } else {
3042
3043 for (key in hash) {
3044 value = hash[key];
3045 skipAttr = false;
3046
3047 // Unless .attr is from the animator update, stop current
3048 // running animation of this property
3049 if (!continueAnimation) {
3050 stop(this, key);
3051 }
3052
3053 if (this.symbolName && /^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)/.test(key)) {
3054 if (!hasSetSymbolSize) {
3055 this.symbolAttr(hash);
3056 hasSetSymbolSize = true;
3057 }
3058 skipAttr = true;
3059 }
3060
3061 if (this.rotation && (key === 'x' || key === 'y')) {
3062 this.doTransform = true;
3063 }
3064
3065 if (!skipAttr) {
3066 setter = this[key + 'Setter'] || this._defaultSetter;
3067 setter.call(this, value, key, element);
3068
3069
3070 // Let the shadow follow the main element
3071 if (this.shadows && /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(key)) {
3072 this.updateShadows(key, value, setter);
3073 }
3074
3075 }
3076 }
3077
3078 // Update transform. Do this outside the loop to prevent redundant updating for batch setting
3079 // of attributes.
3080 if (this.doTransform) {
3081 this.updateTransform();
3082 this.doTransform = false;
3083 }
3084
3085 }
3086
3087 // In accordance with animate, run a complete callback
3088 if (complete) {
3089 complete();
3090 }
3091
3092 return ret;
3093 },
3094
3095
3096 /**
3097 * Update the shadow elements with new attributes.
3098 *
3099 * @private
3100 * @param {String} key - The attribute name.
3101 * @param {String|Number} value - The value of the attribute.
3102 * @param {Function} setter - The setter function, inherited from the
3103 * parent wrapper
3104 * @returns {void}
3105 */
3106 updateShadows: function(key, value, setter) {
3107 var shadows = this.shadows,
3108 i = shadows.length;
3109
3110 while (i--) {
3111 setter.call(
3112 shadows[i],
3113 key === 'height' ?
3114 Math.max(value - (shadows[i].cutHeight || 0), 0) :
3115 key === 'd' ? this.d : value,
3116 key,
3117 shadows[i]
3118 );
3119 }
3120 },
3121
3122
3123 /**
3124 * Add a class name to an element.
3125 *
3126 * @param {string} className - The new class name to add.
3127 * @param {boolean} [replace=false] - When true, the existing class name(s)
3128 * will be overwritten with the new one. When false, the new one is
3129 * added.
3130 * @returns {SVGElement} Return the SVG element for chainability.
3131 */
3132 addClass: function(className, replace) {
3133 var currentClassName = this.attr('class') || '';
3134
3135 if (currentClassName.indexOf(className) === -1) {
3136 if (!replace) {
3137 className =
3138 (currentClassName + (currentClassName ? ' ' : '') +
3139 className).replace(' ', ' ');
3140 }
3141 this.attr('class', className);
3142 }
3143 return this;
3144 },
3145
3146 /**
3147 * Check if an element has the given class name.
3148 * @param {string} className - The class name to check for.
3149 * @return {Boolean}
3150 */
3151 hasClass: function(className) {
3152 return attr(this.element, 'class').indexOf(className) !== -1;
3153 },
3154
3155 /**
3156 * Remove a class name from the element.
3157 * @param {string} className The class name to remove.
3158 * @return {SVGElement} Returns the SVG element for chainability.
3159 */
3160 removeClass: function(className) {
3161 attr(this.element, 'class', (attr(this.element, 'class') || '').replace(className, ''));
3162 return this;
3163 },
3164
3165 /**
3166 * If one of the symbol size affecting parameters are changed,
3167 * check all the others only once for each call to an element's
3168 * .attr() method
3169 * @param {Object} hash - The attributes to set.
3170 * @private
3171 */
3172 symbolAttr: function(hash) {
3173 var wrapper = this;
3174
3175 each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR', 'anchorX', 'anchorY'], function(key) {
3176 wrapper[key] = pick(hash[key], wrapper[key]);
3177 });
3178
3179 wrapper.attr({
3180 d: wrapper.renderer.symbols[wrapper.symbolName](
3181 wrapper.x,
3182 wrapper.y,
3183 wrapper.width,
3184 wrapper.height,
3185 wrapper
3186 )
3187 });
3188 },
3189
3190 /**
3191 * Apply a clipping rectangle to this element.
3192 *
3193 * @param {ClipRect} [clipRect] - The clipping rectangle. If skipped, the
3194 * current clip is removed.
3195 * @returns {SVGElement} Returns the SVG element to allow chaining.
3196 */
3197 clip: function(clipRect) {
3198 return this.attr(
3199 'clip-path',
3200 clipRect ?
3201 'url(' + this.renderer.url + '#' + clipRect.id + ')' :
3202 'none'
3203 );
3204 },
3205
3206 /**
3207 * Calculate the coordinates needed for drawing a rectangle crisply and
3208 * return the calculated attributes.
3209 *
3210 * @param {Object} rect - A rectangle.
3211 * @param {number} rect.x - The x position.
3212 * @param {number} rect.y - The y position.
3213 * @param {number} rect.width - The width.
3214 * @param {number} rect.height - The height.
3215 * @param {number} [strokeWidth] - The stroke width to consider when
3216 * computing crisp positioning. It can also be set directly on the rect
3217 * parameter.
3218 *
3219 * @returns {{x: Number, y: Number, width: Number, height: Number}} The
3220 * modified rectangle arguments.
3221 */
3222 crisp: function(rect, strokeWidth) {
3223
3224 var wrapper = this,
3225 key,
3226 attribs = {},
3227 normalizer;
3228
3229 strokeWidth = strokeWidth || rect.strokeWidth || 0;
3230 normalizer = Math.round(strokeWidth) % 2 / 2; // Math.round because strokeWidth can sometimes have roundoff errors
3231
3232 // normalize for crisp edges
3233 rect.x = Math.floor(rect.x || wrapper.x || 0) + normalizer;
3234 rect.y = Math.floor(rect.y || wrapper.y || 0) + normalizer;
3235 rect.width = Math.floor((rect.width || wrapper.width || 0) - 2 * normalizer);
3236 rect.height = Math.floor((rect.height || wrapper.height || 0) - 2 * normalizer);
3237 if (defined(rect.strokeWidth)) {
3238 rect.strokeWidth = strokeWidth;
3239 }
3240
3241 for (key in rect) {
3242 if (wrapper[key] !== rect[key]) { // only set attribute if changed
3243 wrapper[key] = attribs[key] = rect[key];
3244 }
3245 }
3246
3247 return attribs;
3248 },
3249
3250 /**
3251 * Set styles for the element. In addition to CSS styles supported by
3252 * native SVG and HTML elements, there are also some custom made for
3253 * Highcharts, like `width`, `ellipsis` and `textOverflow` for SVG text
3254 * elements.
3255 * @param {CSSObject} styles The new CSS styles.
3256 * @returns {SVGElement} Return the SVG element for chaining.
3257 */
3258 css: function(styles) {
3259 var elemWrapper = this,
3260 oldStyles = elemWrapper.styles,
3261 newStyles = {},
3262 elem = elemWrapper.element,
3263 textWidth,
3264 n,
3265 serializedCss = '',
3266 hyphenate,
3267 hasNew = !oldStyles;
3268
3269 // convert legacy
3270 if (styles && styles.color) {
3271 styles.fill = styles.color;
3272 }
3273
3274 // Filter out existing styles to increase performance (#2640)
3275 if (oldStyles) {
3276 for (n in styles) {
3277 if (styles[n] !== oldStyles[n]) {
3278 newStyles[n] = styles[n];
3279 hasNew = true;
3280 }
3281 }
3282 }
3283 if (hasNew) {
3284 textWidth = elemWrapper.textWidth =
3285 (styles && styles.width && elem.nodeName.toLowerCase() === 'text' && pInt(styles.width)) ||
3286 elemWrapper.textWidth; // #3501
3287
3288 // Merge the new styles with the old ones
3289 if (oldStyles) {
3290 styles = extend(
3291 oldStyles,
3292 newStyles
3293 );
3294 }
3295
3296 // store object
3297 elemWrapper.styles = styles;
3298
3299 if (textWidth && (!svg && elemWrapper.renderer.forExport)) {
3300 delete styles.width;
3301 }
3302
3303 // serialize and set style attribute
3304 if (isMS && !svg) {
3305 css(elemWrapper.element, styles);
3306 } else {
3307 hyphenate = function(a, b) {
3308 return '-' + b.toLowerCase();
3309 };
3310 for (n in styles) {
3311 serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + styles[n] + ';';
3312 }
3313 attr(elem, 'style', serializedCss); // #1881
3314 }
3315
3316
3317 if (elemWrapper.added) {
3318 // Rebuild text after added
3319 if (textWidth) {
3320 elemWrapper.renderer.buildText(elemWrapper);
3321 }
3322
3323 // Apply text outline after added
3324 if (styles && styles.textOutline) {
3325 elemWrapper.applyTextOutline(styles.textOutline);
3326 }
3327 }
3328 }
3329
3330 return elemWrapper;
3331 },
3332
3333
3334 /**
3335 * Get the current stroke width. In classic mode, the setter registers it
3336 * directly on the element.
3337 * @returns {number} The stroke width in pixels.
3338 * @ignore
3339 */
3340 strokeWidth: function() {
3341 return this['stroke-width'] || 0;
3342 },
3343
3344
3345 /**
3346 * Add an event listener. This is a simple setter that replaces all other
3347 * events of the same type, opposed to the {@link Highcharts#addEvent}
3348 * function.
3349 * @param {string} eventType - The event type. If the type is `click`,
3350 * Highcharts will internally translate it to a `touchstart` event on
3351 * touch devices, to prevent the browser from waiting for a click event
3352 * from firing.
3353 * @param {Function} handler - The handler callback.
3354 * @returns {SVGElement} The SVGElement for chaining.
3355 */
3356 on: function(eventType, handler) {
3357 var svgElement = this,
3358 element = svgElement.element;
3359
3360 // touch
3361 if (hasTouch && eventType === 'click') {
3362 element.ontouchstart = function(e) {
3363 svgElement.touchEventFired = Date.now(); // #2269
3364 e.preventDefault();
3365 handler.call(element, e);
3366 };
3367 element.onclick = function(e) {
3368 if (win.navigator.userAgent.indexOf('Android') === -1 ||
3369 Date.now() - (svgElement.touchEventFired || 0) > 1100) {
3370 handler.call(element, e);
3371 }
3372 };
3373 } else {
3374 // simplest possible event model for internal use
3375 element['on' + eventType] = handler;
3376 }
3377 return this;
3378 },
3379
3380 /**
3381 * Set the coordinates needed to draw a consistent radial gradient across
3382 * a shape regardless of positioning inside the chart. Used on pie slices
3383 * to make all the slices have the same radial reference point.
3384 *
3385 * @param {Array} coordinates The center reference. The format is
3386 * `[centerX, centerY, diameter]` in pixels.
3387 * @returns {SVGElement} Returns the SVGElement for chaining.
3388 */
3389 setRadialReference: function(coordinates) {
3390 var existingGradient = this.renderer.gradients[this.element.gradient];
3391
3392 this.element.radialReference = coordinates;
3393
3394 // On redrawing objects with an existing gradient, the gradient needs
3395 // to be repositioned (#3801)
3396 if (existingGradient && existingGradient.radAttr) {
3397 existingGradient.animate(
3398 this.renderer.getRadialAttr(
3399 coordinates,
3400 existingGradient.radAttr
3401 )
3402 );
3403 }
3404
3405 return this;
3406 },
3407
3408 /**
3409 * Move an object and its children by x and y values.
3410 *
3411 * @param {number} x - The x value.
3412 * @param {number} y - The y value.
3413 */
3414 translate: function(x, y) {
3415 return this.attr({
3416 translateX: x,
3417 translateY: y
3418 });
3419 },
3420
3421 /**
3422 * Invert a group, rotate and flip. This is used internally on inverted
3423 * charts, where the points and graphs are drawn as if not inverted, then
3424 * the series group elements are inverted.
3425 *
3426 * @param {boolean} inverted - Whether to invert or not. An inverted shape
3427 * can be un-inverted by setting it to false.
3428 * @returns {SVGElement} Return the SVGElement for chaining.
3429 */
3430 invert: function(inverted) {
3431 var wrapper = this;
3432 wrapper.inverted = inverted;
3433 wrapper.updateTransform();
3434 return wrapper;
3435 },
3436
3437 /**
3438 * Update the transform attribute based on internal properties. Deals with
3439 * the custom `translateX`, `translateY`, `rotation`, `scaleX` and `scaleY`
3440 * attributes and updates the SVG `transform` attribute.
3441 * @private
3442 * @returns {void}
3443 */
3444 updateTransform: function() {
3445 var wrapper = this,
3446 translateX = wrapper.translateX || 0,
3447 translateY = wrapper.translateY || 0,
3448 scaleX = wrapper.scaleX,
3449 scaleY = wrapper.scaleY,
3450 inverted = wrapper.inverted,
3451 rotation = wrapper.rotation,
3452 element = wrapper.element,
3453 transform;
3454
3455 // flipping affects translate as adjustment for flipping around the group's axis
3456 if (inverted) {
3457 translateX += wrapper.attr('width');
3458 translateY += wrapper.attr('height');
3459 }
3460
3461 // Apply translate. Nearly all transformed elements have translation, so instead
3462 // of checking for translate = 0, do it always (#1767, #1846).
3463 transform = ['translate(' + translateX + ',' + translateY + ')'];
3464
3465 // apply rotation
3466 if (inverted) {
3467 transform.push('rotate(90) scale(-1,1)');
3468 } else if (rotation) { // text rotation
3469 transform.push('rotate(' + rotation + ' ' + (element.getAttribute('x') || 0) + ' ' + (element.getAttribute('y') || 0) + ')');
3470
3471 // Delete bBox memo when the rotation changes
3472 //delete wrapper.bBox;
3473 }
3474
3475 // apply scale
3476 if (defined(scaleX) || defined(scaleY)) {
3477 transform.push('scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')');
3478 }
3479
3480 if (transform.length) {
3481 element.setAttribute('transform', transform.join(' '));
3482 }
3483 },
3484
3485 /**
3486 * Bring the element to the front.
3487 *
3488 * @returns {SVGElement} Returns the SVGElement for chaining.
3489 */
3490 toFront: function() {
3491 var element = this.element;
3492 element.parentNode.appendChild(element);
3493 return this;
3494 },
3495
3496
3497 /**
3498 * Align the element relative to the chart or another box.
3499 * ß
3500 * @param {Object} [alignOptions] The alignment options. The function can be
3501 * called without this parameter in order to re-align an element after the
3502 * box has been updated.
3503 * @param {string} [alignOptions.align=left] Horizontal alignment. Can be
3504 * one of `left`, `center` and `right`.
3505 * @param {string} [alignOptions.verticalAlign=top] Vertical alignment. Can
3506 * be one of `top`, `middle` and `bottom`.
3507 * @param {number} [alignOptions.x=0] Horizontal pixel offset from
3508 * alignment.
3509 * @param {number} [alignOptions.y=0] Vertical pixel offset from alignment.
3510 * @param {Boolean} [alignByTranslate=false] Use the `transform` attribute
3511 * with translateX and translateY custom attributes to align this elements
3512 * rather than `x` and `y` attributes.
3513 * @param {String|Object} box The box to align to, needs a width and height.
3514 * When the box is a string, it refers to an object in the Renderer. For
3515 * example, when box is `spacingBox`, it refers to `Renderer.spacingBox`
3516 * which holds `width`, `height`, `x` and `y` properties.
3517 * @returns {SVGElement} Returns the SVGElement for chaining.
3518 */
3519 align: function(alignOptions, alignByTranslate, box) {
3520 var align,
3521 vAlign,
3522 x,
3523 y,
3524 attribs = {},
3525 alignTo,
3526 renderer = this.renderer,
3527 alignedObjects = renderer.alignedObjects,
3528 alignFactor,
3529 vAlignFactor;
3530
3531 // First call on instanciate
3532 if (alignOptions) {
3533 this.alignOptions = alignOptions;
3534 this.alignByTranslate = alignByTranslate;
3535 if (!box || isString(box)) { // boxes other than renderer handle this internally
3536 this.alignTo = alignTo = box || 'renderer';
3537 erase(alignedObjects, this); // prevent duplicates, like legendGroup after resize
3538 alignedObjects.push(this);
3539 box = null; // reassign it below
3540 }
3541
3542 // When called on resize, no arguments are supplied
3543 } else {
3544 alignOptions = this.alignOptions;
3545 alignByTranslate = this.alignByTranslate;
3546 alignTo = this.alignTo;
3547 }
3548
3549 box = pick(box, renderer[alignTo], renderer);
3550
3551 // Assign variables
3552 align = alignOptions.align;
3553 vAlign = alignOptions.verticalAlign;
3554 x = (box.x || 0) + (alignOptions.x || 0); // default: left align
3555 y = (box.y || 0) + (alignOptions.y || 0); // default: top align
3556
3557 // Align
3558 if (align === 'right') {
3559 alignFactor = 1;
3560 } else if (align === 'center') {
3561 alignFactor = 2;
3562 }
3563 if (alignFactor) {
3564 x += (box.width - (alignOptions.width || 0)) / alignFactor;
3565 }
3566 attribs[alignByTranslate ? 'translateX' : 'x'] = Math.round(x);
3567
3568
3569 // Vertical align
3570 if (vAlign === 'bottom') {
3571 vAlignFactor = 1;
3572 } else if (vAlign === 'middle') {
3573 vAlignFactor = 2;
3574 }
3575 if (vAlignFactor) {
3576 y += (box.height - (alignOptions.height || 0)) / vAlignFactor;
3577 }
3578 attribs[alignByTranslate ? 'translateY' : 'y'] = Math.round(y);
3579
3580 // Animate only if already placed
3581 this[this.placed ? 'animate' : 'attr'](attribs);
3582 this.placed = true;
3583 this.alignAttr = attribs;
3584
3585 return this;
3586 },
3587
3588 /**
3589 * Get the bounding box (width, height, x and y) for the element. Generally
3590 * used to get rendered text size. Since this is called a lot in charts,
3591 * the results are cached based on text properties, in order to save DOM
3592 * traffic. The returned bounding box includes the rotation, so for example
3593 * a single text line of rotation 90 will report a greater height, and a
3594 * width corresponding to the line-height.
3595 *
3596 * @param {boolean} [reload] Skip the cache and get the updated DOM bouding
3597 * box.
3598 * @param {number} [rot] Override the element's rotation. This is internally
3599 * used on axis labels with a value of 0 to find out what the bounding box
3600 * would be have been if it were not rotated.
3601 * @returns {Object} The bounding box with `x`, `y`, `width` and `height`
3602 * properties.
3603 */
3604 getBBox: function(reload, rot) {
3605 var wrapper = this,
3606 bBox, // = wrapper.bBox,
3607 renderer = wrapper.renderer,
3608 width,
3609 height,
3610 rotation,
3611 rad,
3612 element = wrapper.element,
3613 styles = wrapper.styles,
3614 fontSize,
3615 textStr = wrapper.textStr,
3616 toggleTextShadowShim,
3617 cache = renderer.cache,
3618 cacheKeys = renderer.cacheKeys,
3619 cacheKey;
3620
3621 rotation = pick(rot, wrapper.rotation);
3622 rad = rotation * deg2rad;
3623
3624
3625 fontSize = styles && styles.fontSize;
3626
3627
3628 if (textStr !== undefined) {
3629
3630 cacheKey = textStr.toString();
3631
3632 // Since numbers are monospaced, and numerical labels appear a lot
3633 // in a chart, we assume that a label of n characters has the same
3634 // bounding box as others of the same length. Unless there is inner
3635 // HTML in the label. In that case, leave the numbers as is (#5899).
3636 if (cacheKey.indexOf('<') === -1) {
3637 cacheKey = cacheKey.replace(/[0-9]/g, '0');
3638 }
3639
3640 // Properties that affect bounding box
3641 cacheKey += [
3642 '',
3643 rotation || 0,
3644 fontSize,
3645 element.style.width,
3646 element.style['text-overflow'] // #5968
3647 ]
3648 .join(',');
3649
3650 }
3651
3652 if (cacheKey && !reload) {
3653 bBox = cache[cacheKey];
3654 }
3655
3656 // No cache found
3657 if (!bBox) {
3658
3659 // SVG elements
3660 if (element.namespaceURI === wrapper.SVG_NS || renderer.forExport) {
3661 try { // Fails in Firefox if the container has display: none.
3662
3663 // When the text shadow shim is used, we need to hide the fake shadows
3664 // to get the correct bounding box (#3872)
3665 toggleTextShadowShim = this.fakeTS && function(display) {
3666 each(element.querySelectorAll('.highcharts-text-outline'), function(tspan) {
3667 tspan.style.display = display;
3668 });
3669 };
3670
3671 // Workaround for #3842, Firefox reporting wrong bounding box for shadows
3672 if (toggleTextShadowShim) {
3673 toggleTextShadowShim('none');
3674 }
3675
3676 bBox = element.getBBox ?
3677 // SVG: use extend because IE9 is not allowed to change width and height in case
3678 // of rotation (below)
3679 extend({}, element.getBBox()) :
3680 // Legacy IE in export mode
3681 {
3682 width: element.offsetWidth,
3683 height: element.offsetHeight
3684 };
3685
3686 // #3842
3687 if (toggleTextShadowShim) {
3688 toggleTextShadowShim('');
3689 }
3690 } catch (e) {}
3691
3692 // If the bBox is not set, the try-catch block above failed. The other condition
3693 // is for Opera that returns a width of -Infinity on hidden elements.
3694 if (!bBox || bBox.width < 0) {
3695 bBox = {
3696 width: 0,
3697 height: 0
3698 };
3699 }
3700
3701
3702 // VML Renderer or useHTML within SVG
3703 } else {
3704
3705 bBox = wrapper.htmlGetBBox();
3706
3707 }
3708
3709 // True SVG elements as well as HTML elements in modern browsers using the .useHTML option
3710 // need to compensated for rotation
3711 if (renderer.isSVG) {
3712 width = bBox.width;
3713 height = bBox.height;
3714
3715 // Workaround for wrong bounding box in IE9 and IE10 (#1101, #1505, #1669, #2568)
3716 if (isMS && styles && styles.fontSize === '11px' && height.toPrecision(3) === '16.9') {
3717 bBox.height = height = 14;
3718 }
3719
3720 // Adjust for rotated text
3721 if (rotation) {
3722 bBox.width = Math.abs(height * Math.sin(rad)) + Math.abs(width * Math.cos(rad));
3723 bBox.height = Math.abs(height * Math.cos(rad)) + Math.abs(width * Math.sin(rad));
3724 }
3725 }
3726
3727 // Cache it. When loading a chart in a hidden iframe in Firefox and IE/Edge, the
3728 // bounding box height is 0, so don't cache it (#5620).
3729 if (cacheKey && bBox.height > 0) {
3730
3731 // Rotate (#4681)
3732 while (cacheKeys.length > 250) {
3733 delete cache[cacheKeys.shift()];
3734 }
3735
3736 if (!cache[cacheKey]) {
3737 cacheKeys.push(cacheKey);
3738 }
3739 cache[cacheKey] = bBox;
3740 }
3741 }
3742 return bBox;
3743 },
3744
3745 /**
3746 * Show the element after it has been hidden.
3747 *
3748 * @param {boolean} [inherit=false] Set the visibility attribute to
3749 * `inherit` rather than `visible`. The difference is that an element with
3750 * `visibility="visible"` will be visible even if the parent is hidden.
3751 *
3752 * @returns {SVGElement} Returns the SVGElement for chaining.
3753 */
3754 show: function(inherit) {
3755 return this.attr({
3756 visibility: inherit ? 'inherit' : 'visible'
3757 });
3758 },
3759
3760 /**
3761 * Hide the element, equivalent to setting the `visibility` attribute to
3762 * `hidden`.
3763 *
3764 * @returns {SVGElement} Returns the SVGElement for chaining.
3765 */
3766 hide: function() {
3767 return this.attr({
3768 visibility: 'hidden'
3769 });
3770 },
3771
3772 /**
3773 * Fade out an element by animating its opacity down to 0, and hide it on
3774 * complete. Used internally for the tooltip.
3775 *
3776 * @param {number} [duration=150] The fade duration in milliseconds.
3777 */
3778 fadeOut: function(duration) {
3779 var elemWrapper = this;
3780 elemWrapper.animate({
3781 opacity: 0
3782 }, {
3783 duration: duration || 150,
3784 complete: function() {
3785 elemWrapper.attr({
3786 y: -9999
3787 }); // #3088, assuming we're only using this for tooltips
3788 }
3789 });
3790 },
3791
3792 /**
3793 * Add the element to the DOM. All elements must be added this way.
3794 *
3795 * @param {SVGElement|SVGDOMElement} [parent] The parent item to add it to.
3796 * If undefined, the element is added to the {@link SVGRenderer.box}.
3797 *
3798 * @returns {SVGElement} Returns the SVGElement for chaining.
3799 *
3800 * @sample highcharts/members/renderer-g - Elements added to a group
3801 */
3802 add: function(parent) {
3803
3804 var renderer = this.renderer,
3805 element = this.element,
3806 inserted;
3807
3808 if (parent) {
3809 this.parentGroup = parent;
3810 }
3811
3812 // mark as inverted
3813 this.parentInverted = parent && parent.inverted;
3814
3815 // build formatted text
3816 if (this.textStr !== undefined) {
3817 renderer.buildText(this);
3818 }
3819
3820 // Mark as added
3821 this.added = true;
3822
3823 // If we're adding to renderer root, or other elements in the group
3824 // have a z index, we need to handle it
3825 if (!parent || parent.handleZ || this.zIndex) {
3826 inserted = this.zIndexSetter();
3827 }
3828
3829 // If zIndex is not handled, append at the end
3830 if (!inserted) {
3831 (parent ? parent.element : renderer.box).appendChild(element);
3832 }
3833
3834 // fire an event for internal hooks
3835 if (this.onAdd) {
3836 this.onAdd();
3837 }
3838
3839 return this;
3840 },
3841
3842 /**
3843 * Removes an element from the DOM.
3844 *
3845 * @private
3846 * @param {SVGDOMElement|HTMLDOMElement} element The DOM node to remove.
3847 */
3848 safeRemoveChild: function(element) {
3849 var parentNode = element.parentNode;
3850 if (parentNode) {
3851 parentNode.removeChild(element);
3852 }
3853 },
3854
3855 /**
3856 * Destroy the element and element wrapper and clear up the DOM and event
3857 * hooks.
3858 *
3859 * @returns {void}
3860 */
3861 destroy: function() {
3862 var wrapper = this,
3863 element = wrapper.element || {},
3864 parentToClean = wrapper.renderer.isSVG && element.nodeName === 'SPAN' && wrapper.parentGroup,
3865 grandParent,
3866 key,
3867 i;
3868
3869 // remove events
3870 element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = element.point = null;
3871 stop(wrapper); // stop running animations
3872
3873 if (wrapper.clipPath) {
3874 wrapper.clipPath = wrapper.clipPath.destroy();
3875 }
3876
3877 // Destroy stops in case this is a gradient object
3878 if (wrapper.stops) {
3879 for (i = 0; i < wrapper.stops.length; i++) {
3880 wrapper.stops[i] = wrapper.stops[i].destroy();
3881 }
3882 wrapper.stops = null;
3883 }
3884
3885 // remove element
3886 wrapper.safeRemoveChild(element);
3887
3888
3889 wrapper.destroyShadows();
3890
3891
3892 // In case of useHTML, clean up empty containers emulating SVG groups (#1960, #2393, #2697).
3893 while (parentToClean && parentToClean.div && parentToClean.div.childNodes.length === 0) {
3894 grandParent = parentToClean.parentGroup;
3895 wrapper.safeRemoveChild(parentToClean.div);
3896 delete parentToClean.div;
3897 parentToClean = grandParent;
3898 }
3899
3900 // remove from alignObjects
3901 if (wrapper.alignTo) {
3902 erase(wrapper.renderer.alignedObjects, wrapper);
3903 }
3904
3905 for (key in wrapper) {
3906 delete wrapper[key];
3907 }
3908
3909 return null;
3910 },
3911
3912
3913 /**
3914 * @typedef {Object} ShadowOptions
3915 * @property {string} [color=#000000] The shadow color.
3916 * @property {number} [offsetX=1] The horizontal offset from the element.
3917 * @property {number} [offsetY=1] The vertical offset from the element.
3918 * @property {number} [opacity=0.15] The shadow opacity.
3919 * @property {number} [width=3] The shadow width or distance from the
3920 * element.
3921 */
3922 /**
3923 * Add a shadow to the element. Must be called after the element is added to
3924 * the DOM. In styled mode, this method is not used, instead use `defs` and
3925 * filters.
3926 *
3927 * @param {boolean|ShadowOptions} shadowOptions The shadow options. If
3928 * `true`, the default options are applied. If `false`, the current
3929 * shadow will be removed.
3930 * @param {SVGElement} [group] The SVG group element where the shadows will
3931 * be applied. The default is to add it to the same parent as the current
3932 * element. Internally, this is ised for pie slices, where all the
3933 * shadows are added to an element behind all the slices.
3934 * @param {boolean} [cutOff] Used internally for column shadows.
3935 *
3936 * @returns {SVGElement} Returns the SVGElement for chaining.
3937 *
3938 * @example
3939 * renderer.rect(10, 100, 100, 100)
3940 * .attr({ fill: 'red' })
3941 * .shadow(true);
3942 */
3943 shadow: function(shadowOptions, group, cutOff) {
3944 var shadows = [],
3945 i,
3946 shadow,
3947 element = this.element,
3948 strokeWidth,
3949 shadowWidth,
3950 shadowElementOpacity,
3951
3952 // compensate for inverted plot area
3953 transform;
3954
3955 if (!shadowOptions) {
3956 this.destroyShadows();
3957
3958 } else if (!this.shadows) {
3959 shadowWidth = pick(shadowOptions.width, 3);
3960 shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth;
3961 transform = this.parentInverted ?
3962 '(-1,-1)' :
3963 '(' + pick(shadowOptions.offsetX, 1) + ', ' + pick(shadowOptions.offsetY, 1) + ')';
3964 for (i = 1; i <= shadowWidth; i++) {
3965 shadow = element.cloneNode(0);
3966 strokeWidth = (shadowWidth * 2) + 1 - (2 * i);
3967 attr(shadow, {
3968 'isShadow': 'true',
3969 'stroke': shadowOptions.color || '#000000',
3970 'stroke-opacity': shadowElementOpacity * i,
3971 'stroke-width': strokeWidth,
3972 'transform': 'translate' + transform,
3973 'fill': 'none'
3974 });
3975 if (cutOff) {
3976 attr(shadow, 'height', Math.max(attr(shadow, 'height') - strokeWidth, 0));
3977 shadow.cutHeight = strokeWidth;
3978 }
3979
3980 if (group) {
3981 group.element.appendChild(shadow);
3982 } else {
3983 element.parentNode.insertBefore(shadow, element);
3984 }
3985
3986 shadows.push(shadow);
3987 }
3988
3989 this.shadows = shadows;
3990 }
3991 return this;
3992
3993 },
3994
3995 /**
3996 * Destroy shadows on the element.
3997 * @private
3998 */
3999 destroyShadows: function() {
4000 each(this.shadows || [], function(shadow) {
4001 this.safeRemoveChild(shadow);
4002 }, this);
4003 this.shadows = undefined;
4004 },
4005
4006
4007
4008 xGetter: function(key) {
4009 if (this.element.nodeName === 'circle') {
4010 if (key === 'x') {
4011 key = 'cx';
4012 } else if (key === 'y') {
4013 key = 'cy';
4014 }
4015 }
4016 return this._defaultGetter(key);
4017 },
4018
4019 /**
4020 * Get the current value of an attribute or pseudo attribute, used mainly
4021 * for animation. Called internally from the {@link SVGRenderer#attr}
4022 * function.
4023 *
4024 * @private
4025 */
4026 _defaultGetter: function(key) {
4027 var ret = pick(this[key], this.element ? this.element.getAttribute(key) : null, 0);
4028
4029 if (/^[\-0-9\.]+$/.test(ret)) { // is numerical
4030 ret = parseFloat(ret);
4031 }
4032 return ret;
4033 },
4034
4035
4036 dSetter: function(value, key, element) {
4037 if (value && value.join) { // join path
4038 value = value.join(' ');
4039 }
4040 if (/(NaN| {2}|^$)/.test(value)) {
4041 value = 'M 0 0';
4042 }
4043 element.setAttribute(key, value);
4044
4045 this[key] = value;
4046 },
4047
4048 dashstyleSetter: function(value) {
4049 var i,
4050 strokeWidth = this['stroke-width'];
4051
4052 // If "inherit", like maps in IE, assume 1 (#4981). With HC5 and the new strokeWidth
4053 // function, we should be able to use that instead.
4054 if (strokeWidth === 'inherit') {
4055 strokeWidth = 1;
4056 }
4057 value = value && value.toLowerCase();
4058 if (value) {
4059 value = value
4060 .replace('shortdashdotdot', '3,1,1,1,1,1,')
4061 .replace('shortdashdot', '3,1,1,1')
4062 .replace('shortdot', '1,1,')
4063 .replace('shortdash', '3,1,')
4064 .replace('longdash', '8,3,')
4065 .replace(/dot/g, '1,3,')
4066 .replace('dash', '4,3,')
4067 .replace(/,$/, '')
4068 .split(','); // ending comma
4069
4070 i = value.length;
4071 while (i--) {
4072 value[i] = pInt(value[i]) * strokeWidth;
4073 }
4074 value = value.join(',')
4075 .replace(/NaN/g, 'none'); // #3226
4076 this.element.setAttribute('stroke-dasharray', value);
4077 }
4078 },
4079
4080 alignSetter: function(value) {
4081 var convert = {
4082 left: 'start',
4083 center: 'middle',
4084 right: 'end'
4085 };
4086 this.element.setAttribute('text-anchor', convert[value]);
4087 },
4088 opacitySetter: function(value, key, element) {
4089 this[key] = value;
4090 element.setAttribute(key, value);
4091 },
4092 titleSetter: function(value) {
4093 var titleNode = this.element.getElementsByTagName('title')[0];
4094 if (!titleNode) {
4095 titleNode = doc.createElementNS(this.SVG_NS, 'title');
4096 this.element.appendChild(titleNode);
4097 }
4098
4099 // Remove text content if it exists
4100 if (titleNode.firstChild) {
4101 titleNode.removeChild(titleNode.firstChild);
4102 }
4103
4104 titleNode.appendChild(
4105 doc.createTextNode(
4106 (String(pick(value), '')).replace(/<[^>]*>/g, '') // #3276, #3895
4107 )
4108 );
4109 },
4110 textSetter: function(value) {
4111 if (value !== this.textStr) {
4112 // Delete bBox memo when the text changes
4113 delete this.bBox;
4114
4115 this.textStr = value;
4116 if (this.added) {
4117 this.renderer.buildText(this);
4118 }
4119 }
4120 },
4121 fillSetter: function(value, key, element) {
4122 if (typeof value === 'string') {
4123 element.setAttribute(key, value);
4124 } else if (value) {
4125 this.colorGradient(value, key, element);
4126 }
4127 },
4128 visibilitySetter: function(value, key, element) {
4129 // IE9-11 doesn't handle visibilty:inherit well, so we remove the attribute instead (#2881, #3909)
4130 if (value === 'inherit') {
4131 element.removeAttribute(key);
4132 } else {
4133 element.setAttribute(key, value);
4134 }
4135 },
4136 zIndexSetter: function(value, key) {
4137 var renderer = this.renderer,
4138 parentGroup = this.parentGroup,
4139 parentWrapper = parentGroup || renderer,
4140 parentNode = parentWrapper.element || renderer.box,
4141 childNodes,
4142 otherElement,
4143 otherZIndex,
4144 element = this.element,
4145 inserted,
4146 run = this.added,
4147 i;
4148
4149 if (defined(value)) {
4150 element.zIndex = value; // So we can read it for other elements in the group
4151 value = +value;
4152 if (this[key] === value) { // Only update when needed (#3865)
4153 run = false;
4154 }
4155 this[key] = value;
4156 }
4157
4158 // Insert according to this and other elements' zIndex. Before .add() is called,
4159 // nothing is done. Then on add, or by later calls to zIndexSetter, the node
4160 // is placed on the right place in the DOM.
4161 if (run) {
4162 value = this.zIndex;
4163
4164 if (value && parentGroup) {
4165 parentGroup.handleZ = true;
4166 }
4167
4168 childNodes = parentNode.childNodes;
4169 for (i = 0; i < childNodes.length && !inserted; i++) {
4170 otherElement = childNodes[i];
4171 otherZIndex = otherElement.zIndex;
4172 if (otherElement !== element && (
4173 // Insert before the first element with a higher zIndex
4174 pInt(otherZIndex) > value ||
4175 // If no zIndex given, insert before the first element with a zIndex
4176 (!defined(value) && defined(otherZIndex)) ||
4177 // Negative zIndex versus no zIndex:
4178 // On all levels except the highest. If the parent is <svg>,
4179 // then we don't want to put items before <desc> or <defs>
4180 (value < 0 && !defined(otherZIndex) && parentNode !== renderer.box)
4181
4182 )) {
4183 parentNode.insertBefore(element, otherElement);
4184 inserted = true;
4185 }
4186 }
4187 if (!inserted) {
4188 parentNode.appendChild(element);
4189 }
4190 }
4191 return inserted;
4192 },
4193 _defaultSetter: function(value, key, element) {
4194 element.setAttribute(key, value);
4195 }
4196 };
4197
4198 // Some shared setters and getters
4199 SVGElement.prototype.yGetter = SVGElement.prototype.xGetter;
4200 SVGElement.prototype.translateXSetter = SVGElement.prototype.translateYSetter =
4201 SVGElement.prototype.rotationSetter = SVGElement.prototype.verticalAlignSetter =
4202 SVGElement.prototype.scaleXSetter = SVGElement.prototype.scaleYSetter = function(value, key) {
4203 this[key] = value;
4204 this.doTransform = true;
4205 };
4206
4207
4208 // WebKit and Batik have problems with a stroke-width of zero, so in this case we remove the
4209 // stroke attribute altogether. #1270, #1369, #3065, #3072.
4210 SVGElement.prototype['stroke-widthSetter'] = SVGElement.prototype.strokeSetter = function(value, key, element) {
4211 this[key] = value;
4212 // Only apply the stroke attribute if the stroke width is defined and larger than 0
4213 if (this.stroke && this['stroke-width']) {
4214 SVGElement.prototype.fillSetter.call(this, this.stroke, 'stroke', element); // use prototype as instance may be overridden
4215 element.setAttribute('stroke-width', this['stroke-width']);
4216 this.hasStroke = true;
4217 } else if (key === 'stroke-width' && value === 0 && this.hasStroke) {
4218 element.removeAttribute('stroke');
4219 this.hasStroke = false;
4220 }
4221 };
4222
4223
4224 /**
4225 * Allows direct access to the Highcharts rendering layer in order to draw
4226 * primitive shapes like circles, rectangles, paths or text directly on a chart,
4227 * or independent from any chart. The SVGRenderer represents a wrapper object
4228 * for SVGin modern browsers and through the VMLRenderer, for VML in IE < 8.
4229 *
4230 * An existing chart's renderer can be accessed through {@link Chart#renderer}.
4231 * The renderer can also be used completely decoupled from a chart.
4232 *
4233 * @param {HTMLDOMElement} container - Where to put the SVG in the web page.
4234 * @param {number} width - The width of the SVG.
4235 * @param {number} height - The height of the SVG.
4236 * @param {boolean} [forExport=false] - Whether the rendered content is intended
4237 * for export.
4238 * @param {boolean} [allowHTML=true] - Whether the renderer is allowed to
4239 * include HTML text, which will be projected on top of the SVG.
4240 *
4241 * @example
4242 * // Use directly without a chart object.
4243 * var renderer = new Highcharts.Renderer(parentNode, 600, 400);
4244 *
4245 * @sample highcharts/members/renderer-on-chart - Annotating a chart programmatically.
4246 * @sample highcharts/members/renderer-basic - Independedt SVG drawing.
4247 *
4248 * @class
4249 */
4250 SVGRenderer = H.SVGRenderer = function() {
4251 this.init.apply(this, arguments);
4252 };
4253 SVGRenderer.prototype = {
4254 /**
4255 * A pointer to the renderer's associated Element class. The VMLRenderer
4256 * will have a pointer to VMLElement here.
4257 * @type {SVGElement}
4258 */
4259 Element: SVGElement,
4260 SVG_NS: SVG_NS,
4261 /**
4262 * Initialize the SVGRenderer. Overridable initiator function that takes
4263 * the same parameters as the constructor.
4264 */
4265 init: function(container, width, height, style, forExport, allowHTML) {
4266 var renderer = this,
4267 boxWrapper,
4268 element,
4269 desc;
4270
4271 boxWrapper = renderer.createElement('svg')
4272 .attr({
4273 'version': '1.1',
4274 'class': 'highcharts-root'
4275 })
4276
4277 .css(this.getStyle(style));
4278 element = boxWrapper.element;
4279 container.appendChild(element);
4280
4281 // For browsers other than IE, add the namespace attribute (#1978)
4282 if (container.innerHTML.indexOf('xmlns') === -1) {
4283 attr(element, 'xmlns', this.SVG_NS);
4284 }
4285
4286 // object properties
4287 renderer.isSVG = true;
4288
4289 /**
4290 * The root `svg` node of the renderer.
4291 * @type {SVGDOMElement}
4292 */
4293 this.box = element;
4294 /**
4295 * The wrapper for the root `svg` node of the renderer.
4296 * @type {SVGElement}
4297 */
4298 this.boxWrapper = boxWrapper;
4299 renderer.alignedObjects = [];
4300
4301 /**
4302 * Page url used for internal references.
4303 * @type {string}
4304 */
4305 // #24, #672, #1070
4306 this.url = (isFirefox || isWebKit) && doc.getElementsByTagName('base').length ?
4307 win.location.href
4308 .replace(/#.*?$/, '') // remove the hash
4309 .replace(/([\('\)])/g, '\\$1') // escape parantheses and quotes
4310 .replace(/ /g, '%20') : // replace spaces (needed for Safari only)
4311 '';
4312
4313 // Add description
4314 desc = this.createElement('desc').add();
4315 desc.element.appendChild(doc.createTextNode('Created with Highmaps 5.0.5'));
4316
4317
4318 renderer.defs = this.createElement('defs').add();
4319 renderer.allowHTML = allowHTML;
4320 renderer.forExport = forExport;
4321 renderer.gradients = {}; // Object where gradient SvgElements are stored
4322 renderer.cache = {}; // Cache for numerical bounding boxes
4323 renderer.cacheKeys = [];
4324 renderer.imgCount = 0;
4325
4326 renderer.setSize(width, height, false);
4327
4328
4329
4330 // Issue 110 workaround:
4331 // In Firefox, if a div is positioned by percentage, its pixel position may land
4332 // between pixels. The container itself doesn't display this, but an SVG element
4333 // inside this container will be drawn at subpixel precision. In order to draw
4334 // sharp lines, this must be compensated for. This doesn't seem to work inside
4335 // iframes though (like in jsFiddle).
4336 var subPixelFix, rect;
4337 if (isFirefox && container.getBoundingClientRect) {
4338 subPixelFix = function() {
4339 css(container, {
4340 left: 0,
4341 top: 0
4342 });
4343 rect = container.getBoundingClientRect();
4344 css(container, {
4345 left: (Math.ceil(rect.left) - rect.left) + 'px',
4346 top: (Math.ceil(rect.top) - rect.top) + 'px'
4347 });
4348 };
4349
4350 // run the fix now
4351 subPixelFix();
4352
4353 // run it on resize
4354 renderer.unSubPixelFix = addEvent(win, 'resize', subPixelFix);
4355 }
4356 },
4357
4358
4359
4360 /**
4361 * Get the global style setting for the renderer.
4362 * @private
4363 * @param {CSSObject} style - Style settings.
4364 * @return {CSSObject} The style settings mixed with defaults.
4365 */
4366 getStyle: function(style) {
4367 this.style = extend({
4368
4369 fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif', // default font
4370 fontSize: '12px'
4371
4372 }, style);
4373 return this.style;
4374 },
4375 /**
4376 * Apply the global style on the renderer, mixed with the default styles.
4377 * @param {CSSObject} style - CSS to apply.
4378 */
4379 setStyle: function(style) {
4380 this.boxWrapper.css(this.getStyle(style));
4381 },
4382
4383
4384 /**
4385 * Detect whether the renderer is hidden. This happens when one of the
4386 * parent elements has display: none. Used internally to detect when we need
4387 * to render preliminarily in another div to get the text bounding boxes
4388 * right.
4389 *
4390 * @returns {boolean} True if it is hidden.
4391 */
4392 isHidden: function() { // #608
4393 return !this.boxWrapper.getBBox().width;
4394 },
4395
4396 /**
4397 * Destroys the renderer and its allocated members.
4398 */
4399 destroy: function() {
4400 var renderer = this,
4401 rendererDefs = renderer.defs;
4402 renderer.box = null;
4403 renderer.boxWrapper = renderer.boxWrapper.destroy();
4404
4405 // Call destroy on all gradient elements
4406 destroyObjectProperties(renderer.gradients || {});
4407 renderer.gradients = null;
4408
4409 // Defs are null in VMLRenderer
4410 // Otherwise, destroy them here.
4411 if (rendererDefs) {
4412 renderer.defs = rendererDefs.destroy();
4413 }
4414
4415 // Remove sub pixel fix handler (#982)
4416 if (renderer.unSubPixelFix) {
4417 renderer.unSubPixelFix();
4418 }
4419
4420 renderer.alignedObjects = null;
4421
4422 return null;
4423 },
4424
4425 /**
4426 * Create a wrapper for an SVG element. Serves as a factory for
4427 * {@link SVGElement}, but this function is itself mostly called from
4428 * primitive factories like {@link SVGRenderer#path}, {@link
4429 * SVGRenderer#rect} or {@link SVGRenderer#text}.
4430 *
4431 * @param {string} nodeName - The node name, for example `rect`, `g` etc.
4432 * @returns {SVGElement} The generated SVGElement.
4433 */
4434 createElement: function(nodeName) {
4435 var wrapper = new this.Element();
4436 wrapper.init(this, nodeName);
4437 return wrapper;
4438 },
4439
4440 /**
4441 * Dummy function for plugins, called every time the renderer is updated.
4442 * Prior to Highcharts 5, this was used for the canvg renderer.
4443 * @function
4444 */
4445 draw: noop,
4446
4447 /**
4448 * Get converted radial gradient attributes according to the radial
4449 * reference. Used internally from the {@link SVGElement#colorGradient}
4450 * function.
4451 *
4452 * @private
4453 */
4454 getRadialAttr: function(radialReference, gradAttr) {
4455 return {
4456 cx: (radialReference[0] - radialReference[2] / 2) + gradAttr.cx * radialReference[2],
4457 cy: (radialReference[1] - radialReference[2] / 2) + gradAttr.cy * radialReference[2],
4458 r: gradAttr.r * radialReference[2]
4459 };
4460 },
4461
4462 /**
4463 * Parse a simple HTML string into SVG tspans. Called internally when text
4464 * is set on an SVGElement. The function supports a subset of HTML tags,
4465 * CSS text features like `width`, `text-overflow`, `white-space`, and
4466 * also attributes like `href` and `style`.
4467 * @private
4468 * @param {SVGElement} wrapper The parent SVGElement.
4469 */
4470 buildText: function(wrapper) {
4471 var textNode = wrapper.element,
4472 renderer = this,
4473 forExport = renderer.forExport,
4474 textStr = pick(wrapper.textStr, '').toString(),
4475 hasMarkup = textStr.indexOf('<') !== -1,
4476 lines,
4477 childNodes = textNode.childNodes,
4478 clsRegex,
4479 styleRegex,
4480 hrefRegex,
4481 wasTooLong,
4482 parentX = attr(textNode, 'x'),
4483 textStyles = wrapper.styles,
4484 width = wrapper.textWidth,
4485 textLineHeight = textStyles && textStyles.lineHeight,
4486 textOutline = textStyles && textStyles.textOutline,
4487 ellipsis = textStyles && textStyles.textOverflow === 'ellipsis',
4488 i = childNodes.length,
4489 tempParent = width && !wrapper.added && this.box,
4490 getLineHeight = function(tspan) {
4491 var fontSizeStyle;
4492
4493 fontSizeStyle = /(px|em)$/.test(tspan && tspan.style.fontSize) ?
4494 tspan.style.fontSize :
4495 ((textStyles && textStyles.fontSize) || renderer.style.fontSize || 12);
4496
4497
4498 return textLineHeight ?
4499 pInt(textLineHeight) :
4500 renderer.fontMetrics(
4501 fontSizeStyle,
4502 // Get the computed size from parent if not explicit
4503 tspan.getAttribute('style') ? tspan : textNode
4504 ).h;
4505 },
4506 unescapeAngleBrackets = function(inputStr) {
4507 return inputStr.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
4508 };
4509
4510 /// remove old text
4511 while (i--) {
4512 textNode.removeChild(childNodes[i]);
4513 }
4514
4515 // Skip tspans, add text directly to text node. The forceTSpan is a hook
4516 // used in text outline hack.
4517 if (!hasMarkup && !textOutline && !ellipsis && !width && textStr.indexOf(' ') === -1) {
4518 textNode.appendChild(doc.createTextNode(unescapeAngleBrackets(textStr)));
4519
4520 // Complex strings, add more logic
4521 } else {
4522
4523 clsRegex = /<.*class="([^"]+)".*>/;
4524 styleRegex = /<.*style="([^"]+)".*>/;
4525 hrefRegex = /<.*href="(http[^"]+)".*>/;
4526
4527 if (tempParent) {
4528 tempParent.appendChild(textNode); // attach it to the DOM to read offset width
4529 }
4530
4531 if (hasMarkup) {
4532 lines = textStr
4533 .replace(/<(b|strong)>/g, '<span style="font-weight:bold">')
4534 .replace(/<(i|em)>/g, '<span style="font-style:italic">')
4535 .replace(/<a/g, '<span')
4536 .replace(/<\/(b|strong|i|em|a)>/g, '</span>')
4537 .split(/<br.*?>/g);
4538
4539 } else {
4540 lines = [textStr];
4541 }
4542
4543
4544 // Trim empty lines (#5261)
4545 lines = grep(lines, function(line) {
4546 return line !== '';
4547 });
4548
4549
4550 // build the lines
4551 each(lines, function buildTextLines(line, lineNo) {
4552 var spans,
4553 spanNo = 0;
4554 line = line
4555 .replace(/^\s+|\s+$/g, '') // Trim to prevent useless/costly process on the spaces (#5258)
4556 .replace(/<span/g, '|||<span')
4557 .replace(/<\/span>/g, '</span>|||');
4558 spans = line.split('|||');
4559
4560 each(spans, function buildTextSpans(span) {
4561 if (span !== '' || spans.length === 1) {
4562 var attributes = {},
4563 tspan = doc.createElementNS(renderer.SVG_NS, 'tspan'),
4564 spanCls,
4565 spanStyle; // #390
4566 if (clsRegex.test(span)) {
4567 spanCls = span.match(clsRegex)[1];
4568 attr(tspan, 'class', spanCls);
4569 }
4570 if (styleRegex.test(span)) {
4571 spanStyle = span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2');
4572 attr(tspan, 'style', spanStyle);
4573 }
4574 if (hrefRegex.test(span) && !forExport) { // Not for export - #1529
4575 attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"');
4576 css(tspan, {
4577 cursor: 'pointer'
4578 });
4579 }
4580
4581 span = unescapeAngleBrackets(span.replace(/<(.|\n)*?>/g, '') || ' ');
4582
4583 // Nested tags aren't supported, and cause crash in Safari (#1596)
4584 if (span !== ' ') {
4585
4586 // add the text node
4587 tspan.appendChild(doc.createTextNode(span));
4588
4589 if (!spanNo) { // first span in a line, align it to the left
4590 if (lineNo && parentX !== null) {
4591 attributes.x = parentX;
4592 }
4593 } else {
4594 attributes.dx = 0; // #16
4595 }
4596
4597 // add attributes
4598 attr(tspan, attributes);
4599
4600 // Append it
4601 textNode.appendChild(tspan);
4602
4603 // first span on subsequent line, add the line height
4604 if (!spanNo && lineNo) {
4605
4606 // allow getting the right offset height in exporting in IE
4607 if (!svg && forExport) {
4608 css(tspan, {
4609 display: 'block'
4610 });
4611 }
4612
4613 // Set the line height based on the font size of either
4614 // the text element or the tspan element
4615 attr(
4616 tspan,
4617 'dy',
4618 getLineHeight(tspan)
4619 );
4620 }
4621
4622 /*if (width) {
4623 renderer.breakText(wrapper, width);
4624 }*/
4625
4626 // Check width and apply soft breaks or ellipsis
4627 if (width) {
4628 var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273
4629 noWrap = textStyles.whiteSpace === 'nowrap',
4630 hasWhiteSpace = spans.length > 1 || lineNo || (words.length > 1 && !noWrap),
4631 tooLong,
4632 actualWidth,
4633 rest = [],
4634 dy = getLineHeight(tspan),
4635 rotation = wrapper.rotation,
4636 wordStr = span, // for ellipsis
4637 cursor = wordStr.length, // binary search cursor
4638 bBox;
4639
4640 while ((hasWhiteSpace || ellipsis) && (words.length || rest.length)) {
4641 wrapper.rotation = 0; // discard rotation when computing box
4642 bBox = wrapper.getBBox(true);
4643 actualWidth = bBox.width;
4644
4645 // Old IE cannot measure the actualWidth for SVG elements (#2314)
4646 if (!svg && renderer.forExport) {
4647 actualWidth = renderer.measureSpanWidth(tspan.firstChild.data, wrapper.styles);
4648 }
4649
4650 tooLong = actualWidth > width;
4651
4652 // For ellipsis, do a binary search for the correct string length
4653 if (wasTooLong === undefined) {
4654 wasTooLong = tooLong; // First time
4655 }
4656 if (ellipsis && wasTooLong) {
4657 cursor /= 2;
4658
4659 if (wordStr === '' || (!tooLong && cursor < 0.5)) {
4660 words = []; // All ok, break out
4661 } else {
4662 wordStr = span.substring(0, wordStr.length + (tooLong ? -1 : 1) * Math.ceil(cursor));
4663 words = [wordStr + (width > 3 ? '\u2026' : '')];
4664 tspan.removeChild(tspan.firstChild);
4665 }
4666
4667 // Looping down, this is the first word sequence that is not too long,
4668 // so we can move on to build the next line.
4669 } else if (!tooLong || words.length === 1) {
4670 words = rest;
4671 rest = [];
4672
4673 if (words.length && !noWrap) {
4674 tspan = doc.createElementNS(SVG_NS, 'tspan');
4675 attr(tspan, {
4676 dy: dy,
4677 x: parentX
4678 });
4679 if (spanStyle) { // #390
4680 attr(tspan, 'style', spanStyle);
4681 }
4682 textNode.appendChild(tspan);
4683 }
4684 if (actualWidth > width) { // a single word is pressing it out
4685 width = actualWidth;
4686 }
4687 } else { // append to existing line tspan
4688 tspan.removeChild(tspan.firstChild);
4689 rest.unshift(words.pop());
4690 }
4691 if (words.length) {
4692 tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-')));
4693 }
4694 }
4695 wrapper.rotation = rotation;
4696 }
4697
4698 spanNo++;
4699 }
4700 }
4701 });
4702 });
4703
4704 if (wasTooLong) {
4705 wrapper.attr('title', wrapper.textStr);
4706 }
4707 if (tempParent) {
4708 tempParent.removeChild(textNode); // attach it to the DOM to read offset width
4709 }
4710
4711 // Apply the text outline
4712 if (textOutline && wrapper.applyTextOutline) {
4713 wrapper.applyTextOutline(textOutline);
4714 }
4715 }
4716 },
4717
4718
4719
4720 /*
4721 breakText: function (wrapper, width) {
4722 var bBox = wrapper.getBBox(),
4723 node = wrapper.element,
4724 textLength = node.textContent.length,
4725 pos = Math.round(width * textLength / bBox.width), // try this position first, based on average character width
4726 increment = 0,
4727 finalPos;
4728
4729 if (bBox.width > width) {
4730 while (finalPos === undefined) {
4731 textLength = node.getSubStringLength(0, pos);
4732
4733 if (textLength <= width) {
4734 if (increment === -1) {
4735 finalPos = pos;
4736 } else {
4737 increment = 1;
4738 }
4739 } else {
4740 if (increment === 1) {
4741 finalPos = pos - 1;
4742 } else {
4743 increment = -1;
4744 }
4745 }
4746 pos += increment;
4747 }
4748 }
4749 console.log('width', width, 'stringWidth', node.getSubStringLength(0, finalPos))
4750 },
4751 */
4752
4753 /**
4754 * Returns white for dark colors and black for bright colors.
4755 *
4756 * @param {ColorString} rgba - The color to get the contrast for.
4757 * @returns {string} The contrast color, either `#000000` or `#FFFFFF`.
4758 */
4759 getContrast: function(rgba) {
4760 rgba = color(rgba).rgba;
4761 return rgba[0] + rgba[1] + rgba[2] > 2 * 255 ? '#000000' : '#FFFFFF';
4762 },
4763
4764 /**
4765 * Create a button with preset states.
4766 * @param {string} text - The text or HTML to draw.
4767 * @param {number} x - The x position of the button's left side.
4768 * @param {number} y - The y position of the button's top side.
4769 * @param {Function} callback - The function to execute on button click or
4770 * touch.
4771 * @param {SVGAttributes} [normalState] - SVG attributes for the normal
4772 * state.
4773 * @param {SVGAttributes} [hoverState] - SVG attributes for the hover state.
4774 * @param {SVGAttributes} [pressedState] - SVG attributes for the pressed
4775 * state.
4776 * @param {SVGAttributes} [disabledState] - SVG attributes for the disabled
4777 * state.
4778 * @param {Symbol} [shape=rect] - The shape type.
4779 * @returns {SVGRenderer} The button element.
4780 */
4781 button: function(text, x, y, callback, normalState, hoverState, pressedState, disabledState, shape) {
4782 var label = this.label(text, x, y, shape, null, null, null, null, 'button'),
4783 curState = 0;
4784
4785 // Default, non-stylable attributes
4786 label.attr(merge({
4787 'padding': 8,
4788 'r': 2
4789 }, normalState));
4790
4791
4792 // Presentational
4793 var normalStyle,
4794 hoverStyle,
4795 pressedStyle,
4796 disabledStyle;
4797
4798 // Normal state - prepare the attributes
4799 normalState = merge({
4800 fill: '#f7f7f7',
4801 stroke: '#cccccc',
4802 'stroke-width': 1,
4803 style: {
4804 color: '#333333',
4805 cursor: 'pointer',
4806 fontWeight: 'normal'
4807 }
4808 }, normalState);
4809 normalStyle = normalState.style;
4810 delete normalState.style;
4811
4812 // Hover state
4813 hoverState = merge(normalState, {
4814 fill: '#e6e6e6'
4815 }, hoverState);
4816 hoverStyle = hoverState.style;
4817 delete hoverState.style;
4818
4819 // Pressed state
4820 pressedState = merge(normalState, {
4821 fill: '#e6ebf5',
4822 style: {
4823 color: '#000000',
4824 fontWeight: 'bold'
4825 }
4826 }, pressedState);
4827 pressedStyle = pressedState.style;
4828 delete pressedState.style;
4829
4830 // Disabled state
4831 disabledState = merge(normalState, {
4832 style: {
4833 color: '#cccccc'
4834 }
4835 }, disabledState);
4836 disabledStyle = disabledState.style;
4837 delete disabledState.style;
4838
4839
4840 // Add the events. IE9 and IE10 need mouseover and mouseout to funciton (#667).
4841 addEvent(label.element, isMS ? 'mouseover' : 'mouseenter', function() {
4842 if (curState !== 3) {
4843 label.setState(1);
4844 }
4845 });
4846 addEvent(label.element, isMS ? 'mouseout' : 'mouseleave', function() {
4847 if (curState !== 3) {
4848 label.setState(curState);
4849 }
4850 });
4851
4852 label.setState = function(state) {
4853 // Hover state is temporary, don't record it
4854 if (state !== 1) {
4855 label.state = curState = state;
4856 }
4857 // Update visuals
4858 label.removeClass(/highcharts-button-(normal|hover|pressed|disabled)/)
4859 .addClass('highcharts-button-' + ['normal', 'hover', 'pressed', 'disabled'][state || 0]);
4860
4861
4862 label.attr([normalState, hoverState, pressedState, disabledState][state || 0])
4863 .css([normalStyle, hoverStyle, pressedStyle, disabledStyle][state || 0]);
4864
4865 };
4866
4867
4868
4869 // Presentational attributes
4870 label
4871 .attr(normalState)
4872 .css(extend({
4873 cursor: 'default'
4874 }, normalStyle));
4875
4876
4877 return label
4878 .on('click', function(e) {
4879 if (curState !== 3) {
4880 callback.call(label, e);
4881 }
4882 });
4883 },
4884
4885 /**
4886 * Make a straight line crisper by not spilling out to neighbour pixels.
4887 *
4888 * @param {Array} points - The original points on the format `['M', 0, 0,
4889 * 'L', 100, 0]`.
4890 * @param {number} width - The width of the line.
4891 * @returns {Array} The original points array, but modified to render
4892 * crisply.
4893 */
4894 crispLine: function(points, width) {
4895 // normalize to a crisp line
4896 if (points[1] === points[4]) {
4897 // Substract due to #1129. Now bottom and left axis gridlines behave the same.
4898 points[1] = points[4] = Math.round(points[1]) - (width % 2 / 2);
4899 }
4900 if (points[2] === points[5]) {
4901 points[2] = points[5] = Math.round(points[2]) + (width % 2 / 2);
4902 }
4903 return points;
4904 },
4905
4906
4907 /**
4908 * Draw a path, wraps the SVG `path` element.
4909 *
4910 * @param {Array} [path] An SVG path definition in array form.
4911 *
4912 * @example
4913 * var path = renderer.path(['M', 10, 10, 'L', 30, 30, 'z'])
4914 * .attr({ stroke: '#ff00ff' })
4915 * .add();
4916 * @returns {SVGElement} The generated wrapper element.
4917 */
4918 /**
4919 * Draw a path, wraps the SVG `path` element.
4920 *
4921 * @param {SVGAttributes} [attribs] The initial attributes.
4922 * @returns {SVGElement} The generated wrapper element.
4923 */
4924 path: function(path) {
4925 var attribs = {
4926
4927 fill: 'none'
4928
4929 };
4930 if (isArray(path)) {
4931 attribs.d = path;
4932 } else if (isObject(path)) { // attributes
4933 extend(attribs, path);
4934 }
4935 return this.createElement('path').attr(attribs);
4936 },
4937
4938 /**
4939 * Draw a circle, wraps the SVG `circle` element.
4940 *
4941 * @param {number} [x] The center x position.
4942 * @param {number} [y] The center y position.
4943 * @param {number} [r] The radius.
4944 * @returns {SVGElement} The generated wrapper element.
4945 */
4946 /**
4947 * Draw a circle, wraps the SVG `circle` element.
4948 *
4949 * @param {SVGAttributes} [attribs] The initial attributes.
4950 * @returns {SVGElement} The generated wrapper element.
4951 */
4952 circle: function(x, y, r) {
4953 var attribs = isObject(x) ? x : {
4954 x: x,
4955 y: y,
4956 r: r
4957 },
4958 wrapper = this.createElement('circle');
4959
4960 // Setting x or y translates to cx and cy
4961 wrapper.xSetter = wrapper.ySetter = function(value, key, element) {
4962 element.setAttribute('c' + key, value);
4963 };
4964
4965 return wrapper.attr(attribs);
4966 },
4967
4968 /**
4969 * Draw and return an arc.
4970 * @param {number} [x=0] Center X position.
4971 * @param {number} [y=0] Center Y position.
4972 * @param {number} [r=0] The outer radius of the arc.
4973 * @param {number} [innerR=0] Inner radius like used in donut charts.
4974 * @param {number} [start=0] The starting angle of the arc in radians, where
4975 * 0 is to the right and `-Math.PI/2` is up.
4976 * @param {number} [end=0] The ending angle of the arc in radians, where 0
4977 * is to the right and `-Math.PI/2` is up.
4978 * @returns {SVGElement} The generated wrapper element.
4979 */
4980 /**
4981 * Draw and return an arc. Overloaded function that takes arguments object.
4982 * @param {SVGAttributes} attribs Initial SVG attributes.
4983 * @returns {SVGElement} The generated wrapper element.
4984 */
4985 arc: function(x, y, r, innerR, start, end) {
4986 var arc;
4987
4988 if (isObject(x)) {
4989 y = x.y;
4990 r = x.r;
4991 innerR = x.innerR;
4992 start = x.start;
4993 end = x.end;
4994 x = x.x;
4995 }
4996
4997 // Arcs are defined as symbols for the ability to set
4998 // attributes in attr and animate
4999 arc = this.symbol('arc', x || 0, y || 0, r || 0, r || 0, {
5000 innerR: innerR || 0,
5001 start: start || 0,
5002 end: end || 0
5003 });
5004 arc.r = r; // #959
5005 return arc;
5006 },
5007
5008 /**
5009 * Draw and return a rectangle.
5010 * @param {number} [x] Left position.
5011 * @param {number} [y] Top position.
5012 * @param {number} [width] Width of the rectangle.
5013 * @param {number} [height] Height of the rectangle.
5014 * @param {number} [r] Border corner radius.
5015 * @param {number} [strokeWidth] A stroke width can be supplied to allow
5016 * crisp drawing.
5017 * @returns {SVGElement} The generated wrapper element.
5018 */
5019 /**
5020 * Draw and return a rectangle.
5021 * @param {SVGAttributes} [attributes] General SVG attributes for the
5022 * rectangle.
5023 * @returns {SVGElement} The generated wrapper element.
5024 */
5025 rect: function(x, y, width, height, r, strokeWidth) {
5026
5027 r = isObject(x) ? x.r : r;
5028
5029 var wrapper = this.createElement('rect'),
5030 attribs = isObject(x) ? x : x === undefined ? {} : {
5031 x: x,
5032 y: y,
5033 width: Math.max(width, 0),
5034 height: Math.max(height, 0)
5035 };
5036
5037
5038 if (strokeWidth !== undefined) {
5039 attribs.strokeWidth = strokeWidth;
5040 attribs = wrapper.crisp(attribs);
5041 }
5042 attribs.fill = 'none';
5043
5044
5045 if (r) {
5046 attribs.r = r;
5047 }
5048
5049 wrapper.rSetter = function(value, key, element) {
5050 attr(element, {
5051 rx: value,
5052 ry: value
5053 });
5054 };
5055
5056 return wrapper.attr(attribs);
5057 },
5058
5059 /**
5060 * Resize the {@link SVGRenderer#box} and re-align all aligned child
5061 * elements.
5062 * @param {number} width The new pixel width.
5063 * @param {number} height The new pixel height.
5064 * @param {boolean} animate Whether to animate.
5065 */
5066 setSize: function(width, height, animate) {
5067 var renderer = this,
5068 alignedObjects = renderer.alignedObjects,
5069 i = alignedObjects.length;
5070
5071 renderer.width = width;
5072 renderer.height = height;
5073
5074 renderer.boxWrapper.animate({
5075 width: width,
5076 height: height
5077 }, {
5078 step: function() {
5079 this.attr({
5080 viewBox: '0 0 ' + this.attr('width') + ' ' + this.attr('height')
5081 });
5082 },
5083 duration: pick(animate, true) ? undefined : 0
5084 });
5085
5086 while (i--) {
5087 alignedObjects[i].align();
5088 }
5089 },
5090
5091 /**
5092 * Create and return an svg group element.
5093 *
5094 * @param {string} [name] The group will be given a class name of
5095 * `highcharts-{name}`. This can be used for styling and scripting.
5096 * @returns {SVGElement} The generated wrapper element.
5097 */
5098 g: function(name) {
5099 var elem = this.createElement('g');
5100 return name ? elem.attr({
5101 'class': 'highcharts-' + name
5102 }) : elem;
5103 },
5104
5105 /**
5106 * Display an image.
5107 * @param {string} src The image source.
5108 * @param {number} [x] The X position.
5109 * @param {number} [y] The Y position.
5110 * @param {number} [width] The image width. If omitted, it defaults to the
5111 * image file width.
5112 * @param {number} [height] The image height. If omitted it defaults to the
5113 * image file height.
5114 * @returns {SVGElement} The generated wrapper element.
5115 */
5116 image: function(src, x, y, width, height) {
5117 var attribs = {
5118 preserveAspectRatio: 'none'
5119 },
5120 elemWrapper;
5121
5122 // optional properties
5123 if (arguments.length > 1) {
5124 extend(attribs, {
5125 x: x,
5126 y: y,
5127 width: width,
5128 height: height
5129 });
5130 }
5131
5132 elemWrapper = this.createElement('image').attr(attribs);
5133
5134 // set the href in the xlink namespace
5135 if (elemWrapper.element.setAttributeNS) {
5136 elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink',
5137 'href', src);
5138 } else {
5139 // could be exporting in IE
5140 // using href throws "not supported" in ie7 and under, requries regex shim to fix later
5141 elemWrapper.element.setAttribute('hc-svg-href', src);
5142 }
5143 return elemWrapper;
5144 },
5145
5146 /**
5147 * Draw a symbol out of pre-defined shape paths from {@SVGRenderer#symbols}.
5148 * It is used in Highcharts for point makers, which cake a `symbol` option,
5149 * and label and button backgrounds like in the tooltip and stock flags.
5150 *
5151 * @param {Symbol} symbol - The symbol name.
5152 * @param {number} x - The X coordinate for the top left position.
5153 * @param {number} y - The Y coordinate for the top left position.
5154 * @param {number} width - The pixel width.
5155 * @param {number} height - The pixel height.
5156 * @param {Object} [options] - Additional options, depending on the actual
5157 * symbol drawn.
5158 * @param {number} [options.anchorX] - The anchor X position for the
5159 * `callout` symbol. This is where the chevron points to.
5160 * @param {number} [options.anchorY] - The anchor Y position for the
5161 * `callout` symbol. This is where the chevron points to.
5162 * @param {number} [options.end] - The end angle of an `arc` symbol.
5163 * @param {boolean} [options.open] - Whether to draw `arc` symbol open or
5164 * closed.
5165 * @param {number} [options.r] - The radius of an `arc` symbol, or the
5166 * border radius for the `callout` symbol.
5167 * @param {number} [options.start] - The start angle of an `arc` symbol.
5168 */
5169 symbol: function(symbol, x, y, width, height, options) {
5170
5171 var ren = this,
5172 obj,
5173
5174 // get the symbol definition function
5175 symbolFn = this.symbols[symbol],
5176
5177 // check if there's a path defined for this symbol
5178 path = defined(x) && symbolFn && symbolFn(
5179 Math.round(x),
5180 Math.round(y),
5181 width,
5182 height,
5183 options
5184 ),
5185 imageRegex = /^url\((.*?)\)$/,
5186 imageSrc,
5187 centerImage;
5188
5189 if (symbolFn) {
5190 obj = this.path(path);
5191
5192
5193 obj.attr('fill', 'none');
5194
5195
5196 // expando properties for use in animate and attr
5197 extend(obj, {
5198 symbolName: symbol,
5199 x: x,
5200 y: y,
5201 width: width,
5202 height: height
5203 });
5204 if (options) {
5205 extend(obj, options);
5206 }
5207
5208
5209 // image symbols
5210 } else if (imageRegex.test(symbol)) {
5211
5212
5213 imageSrc = symbol.match(imageRegex)[1];
5214
5215 // Create the image synchronously, add attribs async
5216 obj = this.image(imageSrc);
5217
5218 // The image width is not always the same as the symbol width. The
5219 // image may be centered within the symbol, as is the case when
5220 // image shapes are used as label backgrounds, for example in flags.
5221 obj.imgwidth = pick(
5222 symbolSizes[imageSrc] && symbolSizes[imageSrc].width,
5223 options && options.width
5224 );
5225 obj.imgheight = pick(
5226 symbolSizes[imageSrc] && symbolSizes[imageSrc].height,
5227 options && options.height
5228 );
5229 /**
5230 * Set the size and position
5231 */
5232 centerImage = function() {
5233 obj.attr({
5234 width: obj.width,
5235 height: obj.height
5236 });
5237 };
5238
5239 /**
5240 * Width and height setters that take both the image's physical size
5241 * and the label size into consideration, and translates the image
5242 * to center within the label.
5243 */
5244 each(['width', 'height'], function(key) {
5245 obj[key + 'Setter'] = function(value, key) {
5246 var attribs = {},
5247 imgSize = this['img' + key],
5248 trans = key === 'width' ? 'translateX' : 'translateY';
5249 this[key] = value;
5250 if (defined(imgSize)) {
5251 if (this.element) {
5252 this.element.setAttribute(key, imgSize);
5253 }
5254 if (!this.alignByTranslate) {
5255 attribs[trans] = ((this[key] || 0) - imgSize) / 2;
5256 this.attr(attribs);
5257 }
5258 }
5259 };
5260 });
5261
5262
5263 if (defined(x)) {
5264 obj.attr({
5265 x: x,
5266 y: y
5267 });
5268 }
5269 obj.isImg = true;
5270
5271 if (defined(obj.imgwidth) && defined(obj.imgheight)) {
5272 centerImage();
5273 } else {
5274 // Initialize image to be 0 size so export will still function if there's no cached sizes.
5275 obj.attr({
5276 width: 0,
5277 height: 0
5278 });
5279
5280 // Create a dummy JavaScript image to get the width and height. Due to a bug in IE < 8,
5281 // the created element must be assigned to a variable in order to load (#292).
5282 createElement('img', {
5283 onload: function() {
5284
5285 var chart = charts[ren.chartIndex];
5286
5287 // Special case for SVGs on IE11, the width is not accessible until the image is
5288 // part of the DOM (#2854).
5289 if (this.width === 0) {
5290 css(this, {
5291 position: 'absolute',
5292 top: '-999em'
5293 });
5294 doc.body.appendChild(this);
5295 }
5296
5297 // Center the image
5298 symbolSizes[imageSrc] = { // Cache for next
5299 width: this.width,
5300 height: this.height
5301 };
5302 obj.imgwidth = this.width;
5303 obj.imgheight = this.height;
5304
5305 if (obj.element) {
5306 centerImage();
5307 }
5308
5309 // Clean up after #2854 workaround.
5310 if (this.parentNode) {
5311 this.parentNode.removeChild(this);
5312 }
5313
5314 // Fire the load event when all external images are loaded
5315 ren.imgCount--;
5316 if (!ren.imgCount && chart && chart.onload) {
5317 chart.onload();
5318 }
5319 },
5320 src: imageSrc
5321 });
5322 this.imgCount++;
5323 }
5324 }
5325
5326 return obj;
5327 },
5328
5329 /**
5330 * @typedef {string} Symbol
5331 *
5332 * Can be one of `arc`, `callout`, `circle`, `diamond`, `square`,
5333 * `triangle`, `triangle-down`. Symbols are used internally for point
5334 * markers, button and label borders and backgrounds, or custom shapes.
5335 * Extendable by adding to {@link SVGRenderer#symbols}.
5336 */
5337 /**
5338 * An extendable collection of functions for defining symbol paths.
5339 */
5340 symbols: {
5341 'circle': function(x, y, w, h) {
5342 var cpw = 0.166 * w;
5343 return [
5344 'M', x + w / 2, y,
5345 'C', x + w + cpw, y, x + w + cpw, y + h, x + w / 2, y + h,
5346 'C', x - cpw, y + h, x - cpw, y, x + w / 2, y,
5347 'Z'
5348 ];
5349 },
5350
5351 'square': function(x, y, w, h) {
5352 return [
5353 'M', x, y,
5354 'L', x + w, y,
5355 x + w, y + h,
5356 x, y + h,
5357 'Z'
5358 ];
5359 },
5360
5361 'triangle': function(x, y, w, h) {
5362 return [
5363 'M', x + w / 2, y,
5364 'L', x + w, y + h,
5365 x, y + h,
5366 'Z'
5367 ];
5368 },
5369
5370 'triangle-down': function(x, y, w, h) {
5371 return [
5372 'M', x, y,
5373 'L', x + w, y,
5374 x + w / 2, y + h,
5375 'Z'
5376 ];
5377 },
5378 'diamond': function(x, y, w, h) {
5379 return [
5380 'M', x + w / 2, y,
5381 'L', x + w, y + h / 2,
5382 x + w / 2, y + h,
5383 x, y + h / 2,
5384 'Z'
5385 ];
5386 },
5387 'arc': function(x, y, w, h, options) {
5388 var start = options.start,
5389 radius = options.r || w || h,
5390 end = options.end - 0.001, // to prevent cos and sin of start and end from becoming equal on 360 arcs (related: #1561)
5391 innerRadius = options.innerR,
5392 open = options.open,
5393 cosStart = Math.cos(start),
5394 sinStart = Math.sin(start),
5395 cosEnd = Math.cos(end),
5396 sinEnd = Math.sin(end),
5397 longArc = options.end - start < Math.PI ? 0 : 1;
5398
5399 return [
5400 'M',
5401 x + radius * cosStart,
5402 y + radius * sinStart,
5403 'A', // arcTo
5404 radius, // x radius
5405 radius, // y radius
5406 0, // slanting
5407 longArc, // long or short arc
5408 1, // clockwise
5409 x + radius * cosEnd,
5410 y + radius * sinEnd,
5411 open ? 'M' : 'L',
5412 x + innerRadius * cosEnd,
5413 y + innerRadius * sinEnd,
5414 'A', // arcTo
5415 innerRadius, // x radius
5416 innerRadius, // y radius
5417 0, // slanting
5418 longArc, // long or short arc
5419 0, // clockwise
5420 x + innerRadius * cosStart,
5421 y + innerRadius * sinStart,
5422
5423 open ? '' : 'Z' // close
5424 ];
5425 },
5426
5427 /**
5428 * Callout shape used for default tooltips, also used for rounded rectangles in VML
5429 */
5430 callout: function(x, y, w, h, options) {
5431 var arrowLength = 6,
5432 halfDistance = 6,
5433 r = Math.min((options && options.r) || 0, w, h),
5434 safeDistance = r + halfDistance,
5435 anchorX = options && options.anchorX,
5436 anchorY = options && options.anchorY,
5437 path;
5438
5439 path = [
5440 'M', x + r, y,
5441 'L', x + w - r, y, // top side
5442 'C', x + w, y, x + w, y, x + w, y + r, // top-right corner
5443 'L', x + w, y + h - r, // right side
5444 'C', x + w, y + h, x + w, y + h, x + w - r, y + h, // bottom-right corner
5445 'L', x + r, y + h, // bottom side
5446 'C', x, y + h, x, y + h, x, y + h - r, // bottom-left corner
5447 'L', x, y + r, // left side
5448 'C', x, y, x, y, x + r, y // top-left corner
5449 ];
5450
5451 // Anchor on right side
5452 if (anchorX && anchorX > w) {
5453
5454 // Chevron
5455 if (anchorY > y + safeDistance && anchorY < y + h - safeDistance) {
5456 path.splice(13, 3,
5457 'L', x + w, anchorY - halfDistance,
5458 x + w + arrowLength, anchorY,
5459 x + w, anchorY + halfDistance,
5460 x + w, y + h - r
5461 );
5462
5463 // Simple connector
5464 } else {
5465 path.splice(13, 3,
5466 'L', x + w, h / 2,
5467 anchorX, anchorY,
5468 x + w, h / 2,
5469 x + w, y + h - r
5470 );
5471 }
5472
5473 // Anchor on left side
5474 } else if (anchorX && anchorX < 0) {
5475
5476 // Chevron
5477 if (anchorY > y + safeDistance && anchorY < y + h - safeDistance) {
5478 path.splice(33, 3,
5479 'L', x, anchorY + halfDistance,
5480 x - arrowLength, anchorY,
5481 x, anchorY - halfDistance,
5482 x, y + r
5483 );
5484
5485 // Simple connector
5486 } else {
5487 path.splice(33, 3,
5488 'L', x, h / 2,
5489 anchorX, anchorY,
5490 x, h / 2,
5491 x, y + r
5492 );
5493 }
5494
5495 } else if (anchorY && anchorY > h && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace bottom
5496 path.splice(23, 3,
5497 'L', anchorX + halfDistance, y + h,
5498 anchorX, y + h + arrowLength,
5499 anchorX - halfDistance, y + h,
5500 x + r, y + h
5501 );
5502 } else if (anchorY && anchorY < 0 && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace top
5503 path.splice(3, 3,
5504 'L', anchorX - halfDistance, y,
5505 anchorX, y - arrowLength,
5506 anchorX + halfDistance, y,
5507 w - r, y
5508 );
5509 }
5510
5511 return path;
5512 }
5513 },
5514
5515 /**
5516 * @typedef {SVGElement} ClipRect - A clipping rectangle that can be applied
5517 * to one or more {@link SVGElement} instances. It is instanciated with the
5518 * {@link SVGRenderer#clipRect} function and applied with the {@link
5519 * SVGElement#clip} function.
5520 *
5521 * @example
5522 * var circle = renderer.circle(100, 100, 100)
5523 * .attr({ fill: 'red' })
5524 * .add();
5525 * var clipRect = renderer.clipRect(100, 100, 100, 100);
5526 *
5527 * // Leave only the lower right quarter visible
5528 * circle.clip(clipRect);
5529 */
5530 /**
5531 * Define a clipping rectangle
5532 * @param {String} id
5533 * @param {number} x
5534 * @param {number} y
5535 * @param {number} width
5536 * @param {number} height
5537 * @returns {ClipRect} A clipping rectangle.
5538 */
5539 clipRect: function(x, y, width, height) {
5540 var wrapper,
5541 id = H.uniqueKey(),
5542
5543 clipPath = this.createElement('clipPath').attr({
5544 id: id
5545 }).add(this.defs);
5546
5547 wrapper = this.rect(x, y, width, height, 0).add(clipPath);
5548 wrapper.id = id;
5549 wrapper.clipPath = clipPath;
5550 wrapper.count = 0;
5551
5552 return wrapper;
5553 },
5554
5555
5556
5557
5558
5559 /**
5560 * Add text to the SVG object
5561 * @param {String} str
5562 * @param {number} x Left position
5563 * @param {number} y Top position
5564 * @param {Boolean} useHTML Use HTML to render the text
5565 */
5566 text: function(str, x, y, useHTML) {
5567
5568 // declare variables
5569 var renderer = this,
5570 fakeSVG = !svg && renderer.forExport,
5571 wrapper,
5572 attribs = {};
5573
5574 if (useHTML && (renderer.allowHTML || !renderer.forExport)) {
5575 return renderer.html(str, x, y);
5576 }
5577
5578 attribs.x = Math.round(x || 0); // X is always needed for line-wrap logic
5579 if (y) {
5580 attribs.y = Math.round(y);
5581 }
5582 if (str || str === 0) {
5583 attribs.text = str;
5584 }
5585
5586 wrapper = renderer.createElement('text')
5587 .attr(attribs);
5588
5589 // Prevent wrapping from creating false offsetWidths in export in legacy IE (#1079, #1063)
5590 if (fakeSVG) {
5591 wrapper.css({
5592 position: 'absolute'
5593 });
5594 }
5595
5596 if (!useHTML) {
5597 wrapper.xSetter = function(value, key, element) {
5598 var tspans = element.getElementsByTagName('tspan'),
5599 tspan,
5600 parentVal = element.getAttribute(key),
5601 i;
5602 for (i = 0; i < tspans.length; i++) {
5603 tspan = tspans[i];
5604 // If the x values are equal, the tspan represents a linebreak
5605 if (tspan.getAttribute(key) === parentVal) {
5606 tspan.setAttribute(key, value);
5607 }
5608 }
5609 element.setAttribute(key, value);
5610 };
5611 }
5612
5613 return wrapper;
5614 },
5615
5616 /**
5617 * Utility to return the baseline offset and total line height from the font
5618 * size.
5619 *
5620 * @param {?string} fontSize The current font size to inspect. If not given,
5621 * the font size will be found from the DOM element.
5622 * @param {SVGElement|SVGDOMElement} [elem] The element to inspect for a
5623 * current font size.
5624 * @returns {Object} An object containing `h`: the line height, `b`: the
5625 * baseline relative to the top of the box, and `f`: the font size.
5626 */
5627 fontMetrics: function(fontSize, elem) {
5628 var lineHeight,
5629 baseline;
5630
5631
5632 fontSize = fontSize ||
5633 // When the elem is a DOM element (#5932)
5634 (elem && elem.style && elem.style.fontSize) ||
5635 // Fall back on the renderer style default
5636 (this.style && this.style.fontSize);
5637
5638
5639
5640 // Handle different units
5641 if (/px/.test(fontSize)) {
5642 fontSize = pInt(fontSize);
5643 } else if (/em/.test(fontSize)) {
5644 // The em unit depends on parent items
5645 fontSize = parseFloat(fontSize) *
5646 (elem ? this.fontMetrics(null, elem.parentNode).f : 16);
5647 } else {
5648 fontSize = 12;
5649 }
5650
5651 // Empirical values found by comparing font size and bounding box
5652 // height. Applies to the default font family.
5653 // http://jsfiddle.net/highcharts/7xvn7/
5654 lineHeight = fontSize < 24 ? fontSize + 3 : Math.round(fontSize * 1.2);
5655 baseline = Math.round(lineHeight * 0.8);
5656
5657 return {
5658 h: lineHeight,
5659 b: baseline,
5660 f: fontSize
5661 };
5662 },
5663
5664 /**
5665 * Correct X and Y positioning of a label for rotation (#1764)
5666 */
5667 rotCorr: function(baseline, rotation, alterY) {
5668 var y = baseline;
5669 if (rotation && alterY) {
5670 y = Math.max(y * Math.cos(rotation * deg2rad), 4);
5671 }
5672 return {
5673 x: (-baseline / 3) * Math.sin(rotation * deg2rad),
5674 y: y
5675 };
5676 },
5677
5678 /**
5679 * Add a label, a text item that can hold a colored or gradient background
5680 * as well as a border and shadow. Supported custom attributes include
5681 * `padding`.
5682 *
5683 * @param {string} str
5684 * @param {number} x
5685 * @param {number} y
5686 * @param {String} shape
5687 * @param {number} anchorX In case the shape has a pointer, like a flag, this is the
5688 * coordinates it should be pinned to
5689 * @param {number} anchorY
5690 * @param {Boolean} baseline Whether to position the label relative to the text baseline,
5691 * like renderer.text, or to the upper border of the rectangle.
5692 * @param {String} className Class name for the group
5693 */
5694 label: function(str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) {
5695
5696 var renderer = this,
5697 wrapper = renderer.g(className !== 'button' && 'label'),
5698 text = wrapper.text = renderer.text('', 0, 0, useHTML)
5699 .attr({
5700 zIndex: 1
5701 }),
5702 box,
5703 bBox,
5704 alignFactor = 0,
5705 padding = 3,
5706 paddingLeft = 0,
5707 width,
5708 height,
5709 wrapperX,
5710 wrapperY,
5711 textAlign,
5712 deferredAttr = {},
5713 strokeWidth,
5714 baselineOffset,
5715 hasBGImage = /^url\((.*?)\)$/.test(shape),
5716 needsBox = hasBGImage,
5717 getCrispAdjust,
5718 updateBoxSize,
5719 updateTextPadding,
5720 boxAttr;
5721
5722 if (className) {
5723 wrapper.addClass('highcharts-' + className);
5724 }
5725
5726
5727 needsBox = hasBGImage;
5728 getCrispAdjust = function() {
5729 return (strokeWidth || 0) % 2 / 2;
5730 };
5731
5732
5733
5734 /**
5735 * This function runs after the label is added to the DOM (when the bounding box is
5736 * available), and after the text of the label is updated to detect the new bounding
5737 * box and reflect it in the border box.
5738 */
5739 updateBoxSize = function() {
5740 var style = text.element.style,
5741 crispAdjust,
5742 attribs = {};
5743
5744 bBox = (width === undefined || height === undefined || textAlign) && defined(text.textStr) &&
5745 text.getBBox(); //#3295 && 3514 box failure when string equals 0
5746 wrapper.width = (width || bBox.width || 0) + 2 * padding + paddingLeft;
5747 wrapper.height = (height || bBox.height || 0) + 2 * padding;
5748
5749 // Update the label-scoped y offset
5750 baselineOffset = padding + renderer.fontMetrics(style && style.fontSize, text).b;
5751
5752
5753 if (needsBox) {
5754
5755 // Create the border box if it is not already present
5756 if (!box) {
5757 wrapper.box = box = renderer.symbols[shape] || hasBGImage ? // Symbol definition exists (#5324)
5758 renderer.symbol(shape) :
5759 renderer.rect();
5760
5761 box.addClass(
5762 (className === 'button' ? '' : 'highcharts-label-box') + // Don't use label className for buttons
5763 (className ? ' highcharts-' + className + '-box' : '')
5764 );
5765
5766 box.add(wrapper);
5767
5768 crispAdjust = getCrispAdjust();
5769 attribs.x = crispAdjust;
5770 attribs.y = (baseline ? -baselineOffset : 0) + crispAdjust;
5771 }
5772
5773 // Apply the box attributes
5774 attribs.width = Math.round(wrapper.width);
5775 attribs.height = Math.round(wrapper.height);
5776
5777 box.attr(extend(attribs, deferredAttr));
5778 deferredAttr = {};
5779 }
5780 };
5781
5782 /**
5783 * This function runs after setting text or padding, but only if padding is changed
5784 */
5785 updateTextPadding = function() {
5786 var textX = paddingLeft + padding,
5787 textY;
5788
5789 // determin y based on the baseline
5790 textY = baseline ? 0 : baselineOffset;
5791
5792 // compensate for alignment
5793 if (defined(width) && bBox && (textAlign === 'center' || textAlign === 'right')) {
5794 textX += {
5795 center: 0.5,
5796 right: 1
5797 }[textAlign] * (width - bBox.width);
5798 }
5799
5800 // update if anything changed
5801 if (textX !== text.x || textY !== text.y) {
5802 text.attr('x', textX);
5803 if (textY !== undefined) {
5804 text.attr('y', textY);
5805 }
5806 }
5807
5808 // record current values
5809 text.x = textX;
5810 text.y = textY;
5811 };
5812
5813 /**
5814 * Set a box attribute, or defer it if the box is not yet created
5815 * @param {Object} key
5816 * @param {Object} value
5817 */
5818 boxAttr = function(key, value) {
5819 if (box) {
5820 box.attr(key, value);
5821 } else {
5822 deferredAttr[key] = value;
5823 }
5824 };
5825
5826 /**
5827 * After the text element is added, get the desired size of the border box
5828 * and add it before the text in the DOM.
5829 */
5830 wrapper.onAdd = function() {
5831 text.add(wrapper);
5832 wrapper.attr({
5833 text: (str || str === 0) ? str : '', // alignment is available now // #3295: 0 not rendered if given as a value
5834 x: x,
5835 y: y
5836 });
5837
5838 if (box && defined(anchorX)) {
5839 wrapper.attr({
5840 anchorX: anchorX,
5841 anchorY: anchorY
5842 });
5843 }
5844 };
5845
5846 /*
5847 * Add specific attribute setters.
5848 */
5849
5850 // only change local variables
5851 wrapper.widthSetter = function(value) {
5852 width = value;
5853 };
5854 wrapper.heightSetter = function(value) {
5855 height = value;
5856 };
5857 wrapper['text-alignSetter'] = function(value) {
5858 textAlign = value;
5859 };
5860 wrapper.paddingSetter = function(value) {
5861 if (defined(value) && value !== padding) {
5862 padding = wrapper.padding = value;
5863 updateTextPadding();
5864 }
5865 };
5866 wrapper.paddingLeftSetter = function(value) {
5867 if (defined(value) && value !== paddingLeft) {
5868 paddingLeft = value;
5869 updateTextPadding();
5870 }
5871 };
5872
5873
5874 // change local variable and prevent setting attribute on the group
5875 wrapper.alignSetter = function(value) {
5876 value = {
5877 left: 0,
5878 center: 0.5,
5879 right: 1
5880 }[value];
5881 if (value !== alignFactor) {
5882 alignFactor = value;
5883 if (bBox) { // Bounding box exists, means we're dynamically changing
5884 wrapper.attr({
5885 x: wrapperX
5886 }); // #5134
5887 }
5888 }
5889 };
5890
5891 // apply these to the box and the text alike
5892 wrapper.textSetter = function(value) {
5893 if (value !== undefined) {
5894 text.textSetter(value);
5895 }
5896 updateBoxSize();
5897 updateTextPadding();
5898 };
5899
5900 // apply these to the box but not to the text
5901 wrapper['stroke-widthSetter'] = function(value, key) {
5902 if (value) {
5903 needsBox = true;
5904 }
5905 strokeWidth = this['stroke-width'] = value;
5906 boxAttr(key, value);
5907 };
5908
5909 wrapper.strokeSetter = wrapper.fillSetter = wrapper.rSetter = function(value, key) {
5910 if (key === 'fill' && value) {
5911 needsBox = true;
5912 }
5913 boxAttr(key, value);
5914 };
5915
5916 wrapper.anchorXSetter = function(value, key) {
5917 anchorX = value;
5918 boxAttr(key, Math.round(value) - getCrispAdjust() - wrapperX);
5919 };
5920 wrapper.anchorYSetter = function(value, key) {
5921 anchorY = value;
5922 boxAttr(key, value - wrapperY);
5923 };
5924
5925 // rename attributes
5926 wrapper.xSetter = function(value) {
5927 wrapper.x = value; // for animation getter
5928 if (alignFactor) {
5929 value -= alignFactor * ((width || bBox.width) + 2 * padding);
5930 }
5931 wrapperX = Math.round(value);
5932 wrapper.attr('translateX', wrapperX);
5933 };
5934 wrapper.ySetter = function(value) {
5935 wrapperY = wrapper.y = Math.round(value);
5936 wrapper.attr('translateY', wrapperY);
5937 };
5938
5939 // Redirect certain methods to either the box or the text
5940 var baseCss = wrapper.css;
5941 return extend(wrapper, {
5942 /**
5943 * Pick up some properties and apply them to the text instead of the
5944 * wrapper.
5945 * @ignore
5946 */
5947 css: function(styles) {
5948 if (styles) {
5949 var textStyles = {};
5950 styles = merge(styles); // create a copy to avoid altering the original object (#537)
5951 each(wrapper.textProps, function(prop) {
5952 if (styles[prop] !== undefined) {
5953 textStyles[prop] = styles[prop];
5954 delete styles[prop];
5955 }
5956 });
5957 text.css(textStyles);
5958 }
5959 return baseCss.call(wrapper, styles);
5960 },
5961 /**
5962 * Return the bounding box of the box, not the group.
5963 * @ignore
5964 */
5965 getBBox: function() {
5966 return {
5967 width: bBox.width + 2 * padding,
5968 height: bBox.height + 2 * padding,
5969 x: bBox.x - padding,
5970 y: bBox.y - padding
5971 };
5972 },
5973
5974 /**
5975 * Apply the shadow to the box.
5976 * @ignore
5977 */
5978 shadow: function(b) {
5979 if (b) {
5980 updateBoxSize();
5981 if (box) {
5982 box.shadow(b);
5983 }
5984 }
5985 return wrapper;
5986 },
5987
5988 /**
5989 * Destroy and release memory.
5990 * @ignore
5991 */
5992 destroy: function() {
5993
5994 // Added by button implementation
5995 removeEvent(wrapper.element, 'mouseenter');
5996 removeEvent(wrapper.element, 'mouseleave');
5997
5998 if (text) {
5999 text = text.destroy();
6000 }
6001 if (box) {
6002 box = box.destroy();
6003 }
6004 // Call base implementation to destroy the rest
6005 SVGElement.prototype.destroy.call(wrapper);
6006
6007 // Release local pointers (#1298)
6008 wrapper = renderer = updateBoxSize = updateTextPadding = boxAttr = null;
6009 }
6010 });
6011 }
6012 }; // end SVGRenderer
6013
6014
6015 // general renderer
6016 H.Renderer = SVGRenderer;
6017
6018 }(Highcharts));
6019 (function(H) {
6020 /**
6021 * (c) 2010-2016 Torstein Honsi
6022 *
6023 * License: www.highcharts.com/license
6024 */
6025 'use strict';
6026 var attr = H.attr,
6027 createElement = H.createElement,
6028 css = H.css,
6029 defined = H.defined,
6030 each = H.each,
6031 extend = H.extend,
6032 isFirefox = H.isFirefox,
6033 isMS = H.isMS,
6034 isWebKit = H.isWebKit,
6035 pInt = H.pInt,
6036 SVGElement = H.SVGElement,
6037 SVGRenderer = H.SVGRenderer,
6038 win = H.win,
6039 wrap = H.wrap;
6040
6041 // Extend SvgElement for useHTML option
6042 extend(SVGElement.prototype, /** @lends SVGElement.prototype */ {
6043 /**
6044 * Apply CSS to HTML elements. This is used in text within SVG rendering and
6045 * by the VML renderer
6046 */
6047 htmlCss: function(styles) {
6048 var wrapper = this,
6049 element = wrapper.element,
6050 textWidth = styles && element.tagName === 'SPAN' && styles.width;
6051
6052 if (textWidth) {
6053 delete styles.width;
6054 wrapper.textWidth = textWidth;
6055 wrapper.updateTransform();
6056 }
6057 if (styles && styles.textOverflow === 'ellipsis') {
6058 styles.whiteSpace = 'nowrap';
6059 styles.overflow = 'hidden';
6060 }
6061 wrapper.styles = extend(wrapper.styles, styles);
6062 css(wrapper.element, styles);
6063
6064 return wrapper;
6065 },
6066
6067 /**
6068 * VML and useHTML method for calculating the bounding box based on offsets
6069 * @param {Boolean} refresh Whether to force a fresh value from the DOM or to
6070 * use the cached value
6071 *
6072 * @return {Object} A hash containing values for x, y, width and height
6073 */
6074
6075 htmlGetBBox: function() {
6076 var wrapper = this,
6077 element = wrapper.element;
6078
6079 // faking getBBox in exported SVG in legacy IE
6080 // faking getBBox in exported SVG in legacy IE (is this a duplicate of the fix for #1079?)
6081 if (element.nodeName === 'text') {
6082 element.style.position = 'absolute';
6083 }
6084
6085 return {
6086 x: element.offsetLeft,
6087 y: element.offsetTop,
6088 width: element.offsetWidth,
6089 height: element.offsetHeight
6090 };
6091 },
6092
6093 /**
6094 * VML override private method to update elements based on internal
6095 * properties based on SVG transform
6096 */
6097 htmlUpdateTransform: function() {
6098 // aligning non added elements is expensive
6099 if (!this.added) {
6100 this.alignOnAdd = true;
6101 return;
6102 }
6103
6104 var wrapper = this,
6105 renderer = wrapper.renderer,
6106 elem = wrapper.element,
6107 translateX = wrapper.translateX || 0,
6108 translateY = wrapper.translateY || 0,
6109 x = wrapper.x || 0,
6110 y = wrapper.y || 0,
6111 align = wrapper.textAlign || 'left',
6112 alignCorrection = {
6113 left: 0,
6114 center: 0.5,
6115 right: 1
6116 }[align],
6117 styles = wrapper.styles;
6118
6119 // apply translate
6120 css(elem, {
6121 marginLeft: translateX,
6122 marginTop: translateY
6123 });
6124
6125
6126 if (wrapper.shadows) { // used in labels/tooltip
6127 each(wrapper.shadows, function(shadow) {
6128 css(shadow, {
6129 marginLeft: translateX + 1,
6130 marginTop: translateY + 1
6131 });
6132 });
6133 }
6134
6135
6136 // apply inversion
6137 if (wrapper.inverted) { // wrapper is a group
6138 each(elem.childNodes, function(child) {
6139 renderer.invertChild(child, elem);
6140 });
6141 }
6142
6143 if (elem.tagName === 'SPAN') {
6144
6145 var rotation = wrapper.rotation,
6146 baseline,
6147 textWidth = pInt(wrapper.textWidth),
6148 whiteSpace = styles && styles.whiteSpace,
6149 currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth, wrapper.textAlign].join(',');
6150
6151 if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed
6152
6153
6154 baseline = renderer.fontMetrics(elem.style.fontSize).b;
6155
6156 // Renderer specific handling of span rotation
6157 if (defined(rotation)) {
6158 wrapper.setSpanRotation(rotation, alignCorrection, baseline);
6159 }
6160
6161 // Reset multiline/ellipsis in order to read width (#4928, #5417)
6162 css(elem, {
6163 width: '',
6164 whiteSpace: whiteSpace || 'nowrap'
6165 });
6166
6167 // Update textWidth
6168 if (elem.offsetWidth > textWidth && /[ \-]/.test(elem.textContent || elem.innerText)) { // #983, #1254
6169 css(elem, {
6170 width: textWidth + 'px',
6171 display: 'block',
6172 whiteSpace: whiteSpace || 'normal' // #3331
6173 });
6174 }
6175
6176
6177 wrapper.getSpanCorrection(elem.offsetWidth, baseline, alignCorrection, rotation, align);
6178 }
6179
6180 // apply position with correction
6181 css(elem, {
6182 left: (x + (wrapper.xCorr || 0)) + 'px',
6183 top: (y + (wrapper.yCorr || 0)) + 'px'
6184 });
6185
6186 // force reflow in webkit to apply the left and top on useHTML element (#1249)
6187 if (isWebKit) {
6188 baseline = elem.offsetHeight; // assigned to baseline for lint purpose
6189 }
6190
6191 // record current text transform
6192 wrapper.cTT = currentTextTransform;
6193 }
6194 },
6195
6196 /**
6197 * Set the rotation of an individual HTML span
6198 */
6199 setSpanRotation: function(rotation, alignCorrection, baseline) {
6200 var rotationStyle = {},
6201 cssTransformKey = isMS ? '-ms-transform' : isWebKit ? '-webkit-transform' : isFirefox ? 'MozTransform' : win.opera ? '-o-transform' : '';
6202
6203 rotationStyle[cssTransformKey] = rotationStyle.transform = 'rotate(' + rotation + 'deg)';
6204 rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] = rotationStyle.transformOrigin = (alignCorrection * 100) + '% ' + baseline + 'px';
6205 css(this.element, rotationStyle);
6206 },
6207
6208 /**
6209 * Get the correction in X and Y positioning as the element is rotated.
6210 */
6211 getSpanCorrection: function(width, baseline, alignCorrection) {
6212 this.xCorr = -width * alignCorrection;
6213 this.yCorr = -baseline;
6214 }
6215 });
6216
6217 // Extend SvgRenderer for useHTML option.
6218 extend(SVGRenderer.prototype, /** @lends SVGRenderer.prototype */ {
6219 /**
6220 * Create HTML text node. This is used by the VML renderer as well as the SVG
6221 * renderer through the useHTML option.
6222 *
6223 * @param {String} str
6224 * @param {Number} x
6225 * @param {Number} y
6226 */
6227 html: function(str, x, y) {
6228 var wrapper = this.createElement('span'),
6229 element = wrapper.element,
6230 renderer = wrapper.renderer,
6231 isSVG = renderer.isSVG,
6232 addSetters = function(element, style) {
6233 // These properties are set as attributes on the SVG group, and as
6234 // identical CSS properties on the div. (#3542)
6235 each(['opacity', 'visibility'], function(prop) {
6236 wrap(element, prop + 'Setter', function(proceed, value, key, elem) {
6237 proceed.call(this, value, key, elem);
6238 style[key] = value;
6239 });
6240 });
6241 };
6242
6243 // Text setter
6244 wrapper.textSetter = function(value) {
6245 if (value !== element.innerHTML) {
6246 delete this.bBox;
6247 }
6248 element.innerHTML = this.textStr = value;
6249 wrapper.htmlUpdateTransform();
6250 };
6251
6252 // Add setters for the element itself (#4938)
6253 if (isSVG) { // #4938, only for HTML within SVG
6254 addSetters(wrapper, wrapper.element.style);
6255 }
6256
6257 // Various setters which rely on update transform
6258 wrapper.xSetter = wrapper.ySetter = wrapper.alignSetter = wrapper.rotationSetter = function(value, key) {
6259 if (key === 'align') {
6260 key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML.
6261 }
6262 wrapper[key] = value;
6263 wrapper.htmlUpdateTransform();
6264 };
6265
6266 // Set the default attributes
6267 wrapper
6268 .attr({
6269 text: str,
6270 x: Math.round(x),
6271 y: Math.round(y)
6272 })
6273 .css({
6274
6275 fontFamily: this.style.fontFamily,
6276 fontSize: this.style.fontSize,
6277
6278 position: 'absolute'
6279 });
6280
6281 // Keep the whiteSpace style outside the wrapper.styles collection
6282 element.style.whiteSpace = 'nowrap';
6283
6284 // Use the HTML specific .css method
6285 wrapper.css = wrapper.htmlCss;
6286
6287 // This is specific for HTML within SVG
6288 if (isSVG) {
6289 wrapper.add = function(svgGroupWrapper) {
6290
6291 var htmlGroup,
6292 container = renderer.box.parentNode,
6293 parentGroup,
6294 parents = [];
6295
6296 this.parentGroup = svgGroupWrapper;
6297
6298 // Create a mock group to hold the HTML elements
6299 if (svgGroupWrapper) {
6300 htmlGroup = svgGroupWrapper.div;
6301 if (!htmlGroup) {
6302
6303 // Read the parent chain into an array and read from top down
6304 parentGroup = svgGroupWrapper;
6305 while (parentGroup) {
6306
6307 parents.push(parentGroup);
6308
6309 // Move up to the next parent group
6310 parentGroup = parentGroup.parentGroup;
6311 }
6312
6313 // Ensure dynamically updating position when any parent is translated
6314 each(parents.reverse(), function(parentGroup) {
6315 var htmlGroupStyle,
6316 cls = attr(parentGroup.element, 'class');
6317
6318 if (cls) {
6319 cls = {
6320 className: cls
6321 };
6322 } // else null
6323
6324 // Create a HTML div and append it to the parent div to emulate
6325 // the SVG group structure
6326 htmlGroup = parentGroup.div = parentGroup.div || createElement('div', cls, {
6327 position: 'absolute',
6328 left: (parentGroup.translateX || 0) + 'px',
6329 top: (parentGroup.translateY || 0) + 'px',
6330 display: parentGroup.display,
6331 opacity: parentGroup.opacity, // #5075
6332 pointerEvents: parentGroup.styles && parentGroup.styles.pointerEvents // #5595
6333 }, htmlGroup || container); // the top group is appended to container
6334
6335 // Shortcut
6336 htmlGroupStyle = htmlGroup.style;
6337
6338 // Set listeners to update the HTML div's position whenever the SVG group
6339 // position is changed
6340 extend(parentGroup, {
6341 on: function() {
6342 wrapper.on.apply({
6343 element: parents[0].div
6344 }, arguments);
6345 return parentGroup;
6346 },
6347 translateXSetter: function(value, key) {
6348 htmlGroupStyle.left = value + 'px';
6349 parentGroup[key] = value;
6350 parentGroup.doTransform = true;
6351 },
6352 translateYSetter: function(value, key) {
6353 htmlGroupStyle.top = value + 'px';
6354 parentGroup[key] = value;
6355 parentGroup.doTransform = true;
6356 }
6357 });
6358 addSetters(parentGroup, htmlGroupStyle);
6359 });
6360
6361 }
6362 } else {
6363 htmlGroup = container;
6364 }
6365
6366 htmlGroup.appendChild(element);
6367
6368 // Shared with VML:
6369 wrapper.added = true;
6370 if (wrapper.alignOnAdd) {
6371 wrapper.htmlUpdateTransform();
6372 }
6373
6374 return wrapper;
6375 };
6376 }
6377 return wrapper;
6378 }
6379 });
6380
6381 }(Highcharts));
6382 (function(H) {
6383 /**
6384 * (c) 2010-2016 Torstein Honsi
6385 *
6386 * License: www.highcharts.com/license
6387 */
6388 'use strict';
6389
6390 var VMLRenderer,
6391 VMLRendererExtension,
6392 VMLElement,
6393
6394 createElement = H.createElement,
6395 css = H.css,
6396 defined = H.defined,
6397 deg2rad = H.deg2rad,
6398 discardElement = H.discardElement,
6399 doc = H.doc,
6400 each = H.each,
6401 erase = H.erase,
6402 extend = H.extend,
6403 extendClass = H.extendClass,
6404 isArray = H.isArray,
6405 isNumber = H.isNumber,
6406 isObject = H.isObject,
6407 merge = H.merge,
6408 noop = H.noop,
6409 pick = H.pick,
6410 pInt = H.pInt,
6411 svg = H.svg,
6412 SVGElement = H.SVGElement,
6413 SVGRenderer = H.SVGRenderer,
6414 win = H.win;
6415
6416 /* ****************************************************************************
6417 * *
6418 * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE *
6419 * *
6420 * For applications and websites that don't need IE support, like platform *
6421 * targeted mobile apps and web apps, this code can be removed. *
6422 * *
6423 *****************************************************************************/
6424
6425 /**
6426 * @constructor
6427 */
6428 if (!svg) {
6429
6430 /**
6431 * The VML element wrapper.
6432 */
6433 VMLElement = {
6434
6435 docMode8: doc && doc.documentMode === 8,
6436
6437 /**
6438 * Initialize a new VML element wrapper. It builds the markup as a string
6439 * to minimize DOM traffic.
6440 * @param {Object} renderer
6441 * @param {Object} nodeName
6442 */
6443 init: function(renderer, nodeName) {
6444 var wrapper = this,
6445 markup = ['<', nodeName, ' filled="f" stroked="f"'],
6446 style = ['position: ', 'absolute', ';'],
6447 isDiv = nodeName === 'div';
6448
6449 // divs and shapes need size
6450 if (nodeName === 'shape' || isDiv) {
6451 style.push('left:0;top:0;width:1px;height:1px;');
6452 }
6453 style.push('visibility: ', isDiv ? 'hidden' : 'visible');
6454
6455 markup.push(' style="', style.join(''), '"/>');
6456
6457 // create element with default attributes and style
6458 if (nodeName) {
6459 markup = isDiv || nodeName === 'span' || nodeName === 'img' ?
6460 markup.join('') :
6461 renderer.prepVML(markup);
6462 wrapper.element = createElement(markup);
6463 }
6464
6465 wrapper.renderer = renderer;
6466 },
6467
6468 /**
6469 * Add the node to the given parent
6470 * @param {Object} parent
6471 */
6472 add: function(parent) {
6473 var wrapper = this,
6474 renderer = wrapper.renderer,
6475 element = wrapper.element,
6476 box = renderer.box,
6477 inverted = parent && parent.inverted,
6478
6479 // get the parent node
6480 parentNode = parent ?
6481 parent.element || parent :
6482 box;
6483
6484 if (parent) {
6485 this.parentGroup = parent;
6486 }
6487
6488 // if the parent group is inverted, apply inversion on all children
6489 if (inverted) { // only on groups
6490 renderer.invertChild(element, parentNode);
6491 }
6492
6493 // append it
6494 parentNode.appendChild(element);
6495
6496 // align text after adding to be able to read offset
6497 wrapper.added = true;
6498 if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) {
6499 wrapper.updateTransform();
6500 }
6501
6502 // fire an event for internal hooks
6503 if (wrapper.onAdd) {
6504 wrapper.onAdd();
6505 }
6506
6507 // IE8 Standards can't set the class name before the element is appended
6508 if (this.className) {
6509 this.attr('class', this.className);
6510 }
6511
6512 return wrapper;
6513 },
6514
6515 /**
6516 * VML always uses htmlUpdateTransform
6517 */
6518 updateTransform: SVGElement.prototype.htmlUpdateTransform,
6519
6520 /**
6521 * Set the rotation of a span with oldIE's filter
6522 */
6523 setSpanRotation: function() {
6524 // Adjust for alignment and rotation. Rotation of useHTML content is not yet implemented
6525 // but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+
6526 // has support for CSS3 transform. The getBBox method also needs to be updated
6527 // to compensate for the rotation, like it currently does for SVG.
6528 // Test case: http://jsfiddle.net/highcharts/Ybt44/
6529
6530 var rotation = this.rotation,
6531 costheta = Math.cos(rotation * deg2rad),
6532 sintheta = Math.sin(rotation * deg2rad);
6533
6534 css(this.element, {
6535 filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta,
6536 ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta,
6537 ', sizingMethod=\'auto expand\')'
6538 ].join('') : 'none'
6539 });
6540 },
6541
6542 /**
6543 * Get the positioning correction for the span after rotating.
6544 */
6545 getSpanCorrection: function(width, baseline, alignCorrection, rotation, align) {
6546
6547 var costheta = rotation ? Math.cos(rotation * deg2rad) : 1,
6548 sintheta = rotation ? Math.sin(rotation * deg2rad) : 0,
6549 height = pick(this.elemHeight, this.element.offsetHeight),
6550 quad,
6551 nonLeft = align && align !== 'left';
6552
6553 // correct x and y
6554 this.xCorr = costheta < 0 && -width;
6555 this.yCorr = sintheta < 0 && -height;
6556
6557 // correct for baseline and corners spilling out after rotation
6558 quad = costheta * sintheta < 0;
6559 this.xCorr += sintheta * baseline * (quad ? 1 - alignCorrection : alignCorrection);
6560 this.yCorr -= costheta * baseline * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1);
6561 // correct for the length/height of the text
6562 if (nonLeft) {
6563 this.xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1);
6564 if (rotation) {
6565 this.yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1);
6566 }
6567 css(this.element, {
6568 textAlign: align
6569 });
6570 }
6571 },
6572
6573 /**
6574 * Converts a subset of an SVG path definition to its VML counterpart. Takes an array
6575 * as the parameter and returns a string.
6576 */
6577 pathToVML: function(value) {
6578 // convert paths
6579 var i = value.length,
6580 path = [];
6581
6582 while (i--) {
6583
6584 // Multiply by 10 to allow subpixel precision.
6585 // Substracting half a pixel seems to make the coordinates
6586 // align with SVG, but this hasn't been tested thoroughly
6587 if (isNumber(value[i])) {
6588 path[i] = Math.round(value[i] * 10) - 5;
6589 } else if (value[i] === 'Z') { // close the path
6590 path[i] = 'x';
6591 } else {
6592 path[i] = value[i];
6593
6594 // When the start X and end X coordinates of an arc are too close,
6595 // they are rounded to the same value above. In this case, substract or
6596 // add 1 from the end X and Y positions. #186, #760, #1371, #1410.
6597 if (value.isArc && (value[i] === 'wa' || value[i] === 'at')) {
6598 // Start and end X
6599 if (path[i + 5] === path[i + 7]) {
6600 path[i + 7] += value[i + 7] > value[i + 5] ? 1 : -1;
6601 }
6602 // Start and end Y
6603 if (path[i + 6] === path[i + 8]) {
6604 path[i + 8] += value[i + 8] > value[i + 6] ? 1 : -1;
6605 }
6606 }
6607 }
6608 }
6609
6610
6611 // Loop up again to handle path shortcuts (#2132)
6612 /*while (i++ < path.length) {
6613 if (path[i] === 'H') { // horizontal line to
6614 path[i] = 'L';
6615 path.splice(i + 2, 0, path[i - 1]);
6616 } else if (path[i] === 'V') { // vertical line to
6617 path[i] = 'L';
6618 path.splice(i + 1, 0, path[i - 2]);
6619 }
6620 }*/
6621 return path.join(' ') || 'x';
6622 },
6623
6624 /**
6625 * Set the element's clipping to a predefined rectangle
6626 *
6627 * @param {String} id The id of the clip rectangle
6628 */
6629 clip: function(clipRect) {
6630 var wrapper = this,
6631 clipMembers,
6632 cssRet;
6633
6634 if (clipRect) {
6635 clipMembers = clipRect.members;
6636 erase(clipMembers, wrapper); // Ensure unique list of elements (#1258)
6637 clipMembers.push(wrapper);
6638 wrapper.destroyClip = function() {
6639 erase(clipMembers, wrapper);
6640 };
6641 cssRet = clipRect.getCSS(wrapper);
6642
6643 } else {
6644 if (wrapper.destroyClip) {
6645 wrapper.destroyClip();
6646 }
6647 cssRet = {
6648 clip: wrapper.docMode8 ? 'inherit' : 'rect(auto)'
6649 }; // #1214
6650 }
6651
6652 return wrapper.css(cssRet);
6653
6654 },
6655
6656 /**
6657 * Set styles for the element
6658 * @param {Object} styles
6659 */
6660 css: SVGElement.prototype.htmlCss,
6661
6662 /**
6663 * Removes a child either by removeChild or move to garbageBin.
6664 * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not.
6665 */
6666 safeRemoveChild: function(element) {
6667 // discardElement will detach the node from its parent before attaching it
6668 // to the garbage bin. Therefore it is important that the node is attached and have parent.
6669 if (element.parentNode) {
6670 discardElement(element);
6671 }
6672 },
6673
6674 /**
6675 * Extend element.destroy by removing it from the clip members array
6676 */
6677 destroy: function() {
6678 if (this.destroyClip) {
6679 this.destroyClip();
6680 }
6681
6682 return SVGElement.prototype.destroy.apply(this);
6683 },
6684
6685 /**
6686 * Add an event listener. VML override for normalizing event parameters.
6687 * @param {String} eventType
6688 * @param {Function} handler
6689 */
6690 on: function(eventType, handler) {
6691 // simplest possible event model for internal use
6692 this.element['on' + eventType] = function() {
6693 var evt = win.event;
6694 evt.target = evt.srcElement;
6695 handler(evt);
6696 };
6697 return this;
6698 },
6699
6700 /**
6701 * In stacked columns, cut off the shadows so that they don't overlap
6702 */
6703 cutOffPath: function(path, length) {
6704
6705 var len;
6706
6707 path = path.split(/[ ,]/); // The extra comma tricks the trailing comma remover in "gulp scripts" task
6708 len = path.length;
6709
6710 if (len === 9 || len === 11) {
6711 path[len - 4] = path[len - 2] = pInt(path[len - 2]) - 10 * length;
6712 }
6713 return path.join(' ');
6714 },
6715
6716 /**
6717 * Apply a drop shadow by copying elements and giving them different strokes
6718 * @param {Boolean|Object} shadowOptions
6719 */
6720 shadow: function(shadowOptions, group, cutOff) {
6721 var shadows = [],
6722 i,
6723 element = this.element,
6724 renderer = this.renderer,
6725 shadow,
6726 elemStyle = element.style,
6727 markup,
6728 path = element.path,
6729 strokeWidth,
6730 modifiedPath,
6731 shadowWidth,
6732 shadowElementOpacity;
6733
6734 // some times empty paths are not strings
6735 if (path && typeof path.value !== 'string') {
6736 path = 'x';
6737 }
6738 modifiedPath = path;
6739
6740 if (shadowOptions) {
6741 shadowWidth = pick(shadowOptions.width, 3);
6742 shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth;
6743 for (i = 1; i <= 3; i++) {
6744
6745 strokeWidth = (shadowWidth * 2) + 1 - (2 * i);
6746
6747 // Cut off shadows for stacked column items
6748 if (cutOff) {
6749 modifiedPath = this.cutOffPath(path.value, strokeWidth + 0.5);
6750 }
6751
6752 markup = ['<shape isShadow="true" strokeweight="', strokeWidth,
6753 '" filled="false" path="', modifiedPath,
6754 '" coordsize="10 10" style="', element.style.cssText, '" />'
6755 ];
6756
6757 shadow = createElement(renderer.prepVML(markup),
6758 null, {
6759 left: pInt(elemStyle.left) + pick(shadowOptions.offsetX, 1),
6760 top: pInt(elemStyle.top) + pick(shadowOptions.offsetY, 1)
6761 }
6762 );
6763 if (cutOff) {
6764 shadow.cutOff = strokeWidth + 1;
6765 }
6766
6767 // apply the opacity
6768 markup = [
6769 '<stroke color="',
6770 shadowOptions.color || '#000000',
6771 '" opacity="', shadowElementOpacity * i, '"/>'
6772 ];
6773 createElement(renderer.prepVML(markup), null, null, shadow);
6774
6775
6776 // insert it
6777 if (group) {
6778 group.element.appendChild(shadow);
6779 } else {
6780 element.parentNode.insertBefore(shadow, element);
6781 }
6782
6783 // record it
6784 shadows.push(shadow);
6785
6786 }
6787
6788 this.shadows = shadows;
6789 }
6790 return this;
6791 },
6792 updateShadows: noop, // Used in SVG only
6793
6794 setAttr: function(key, value) {
6795 if (this.docMode8) { // IE8 setAttribute bug
6796 this.element[key] = value;
6797 } else {
6798 this.element.setAttribute(key, value);
6799 }
6800 },
6801 classSetter: function(value) {
6802 // IE8 Standards mode has problems retrieving the className unless set like this.
6803 // IE8 Standards can't set the class name before the element is appended.
6804 (this.added ? this.element : this).className = value;
6805 },
6806 dashstyleSetter: function(value, key, element) {
6807 var strokeElem = element.getElementsByTagName('stroke')[0] ||
6808 createElement(this.renderer.prepVML(['<stroke/>']), null, null, element);
6809 strokeElem[key] = value || 'solid';
6810 this[key] = value;
6811 /* because changing stroke-width will change the dash length
6812 and cause an epileptic effect */
6813 },
6814 dSetter: function(value, key, element) {
6815 var i,
6816 shadows = this.shadows;
6817 value = value || [];
6818 this.d = value.join && value.join(' '); // used in getter for animation
6819
6820 element.path = value = this.pathToVML(value);
6821
6822 // update shadows
6823 if (shadows) {
6824 i = shadows.length;
6825 while (i--) {
6826 shadows[i].path = shadows[i].cutOff ? this.cutOffPath(value, shadows[i].cutOff) : value;
6827 }
6828 }
6829 this.setAttr(key, value);
6830 },
6831 fillSetter: function(value, key, element) {
6832 var nodeName = element.nodeName;
6833 if (nodeName === 'SPAN') { // text color
6834 element.style.color = value;
6835 } else if (nodeName !== 'IMG') { // #1336
6836 element.filled = value !== 'none';
6837 this.setAttr('fillcolor', this.renderer.color(value, element, key, this));
6838 }
6839 },
6840 'fill-opacitySetter': function(value, key, element) {
6841 createElement(
6842 this.renderer.prepVML(['<', key.split('-')[0], ' opacity="', value, '"/>']),
6843 null,
6844 null,
6845 element
6846 );
6847 },
6848 opacitySetter: noop, // Don't bother - animation is too slow and filters introduce artifacts
6849 rotationSetter: function(value, key, element) {
6850 var style = element.style;
6851 this[key] = style[key] = value; // style is for #1873
6852
6853 // Correction for the 1x1 size of the shape container. Used in gauge needles.
6854 style.left = -Math.round(Math.sin(value * deg2rad) + 1) + 'px';
6855 style.top = Math.round(Math.cos(value * deg2rad)) + 'px';
6856 },
6857 strokeSetter: function(value, key, element) {
6858 this.setAttr('strokecolor', this.renderer.color(value, element, key, this));
6859 },
6860 'stroke-widthSetter': function(value, key, element) {
6861 element.stroked = !!value; // VML "stroked" attribute
6862 this[key] = value; // used in getter, issue #113
6863 if (isNumber(value)) {
6864 value += 'px';
6865 }
6866 this.setAttr('strokeweight', value);
6867 },
6868 titleSetter: function(value, key) {
6869 this.setAttr(key, value);
6870 },
6871 visibilitySetter: function(value, key, element) {
6872
6873 // Handle inherited visibility
6874 if (value === 'inherit') {
6875 value = 'visible';
6876 }
6877
6878 // Let the shadow follow the main element
6879 if (this.shadows) {
6880 each(this.shadows, function(shadow) {
6881 shadow.style[key] = value;
6882 });
6883 }
6884
6885 // Instead of toggling the visibility CSS property, move the div out of the viewport.
6886 // This works around #61 and #586
6887 if (element.nodeName === 'DIV') {
6888 value = value === 'hidden' ? '-999em' : 0;
6889
6890 // In order to redraw, IE7 needs the div to be visible when tucked away
6891 // outside the viewport. So the visibility is actually opposite of
6892 // the expected value. This applies to the tooltip only.
6893 if (!this.docMode8) {
6894 element.style[key] = value ? 'visible' : 'hidden';
6895 }
6896 key = 'top';
6897 }
6898 element.style[key] = value;
6899 },
6900 xSetter: function(value, key, element) {
6901 this[key] = value; // used in getter
6902
6903 if (key === 'x') {
6904 key = 'left';
6905 } else if (key === 'y') {
6906 key = 'top';
6907 }
6908 /* else {
6909 value = Math.max(0, value); // don't set width or height below zero (#311)
6910 }*/
6911
6912 // clipping rectangle special
6913 if (this.updateClipping) {
6914 this[key] = value; // the key is now 'left' or 'top' for 'x' and 'y'
6915 this.updateClipping();
6916 } else {
6917 // normal
6918 element.style[key] = value;
6919 }
6920 },
6921 zIndexSetter: function(value, key, element) {
6922 element.style[key] = value;
6923 }
6924 };
6925 VMLElement['stroke-opacitySetter'] = VMLElement['fill-opacitySetter'];
6926 H.VMLElement = VMLElement = extendClass(SVGElement, VMLElement);
6927
6928 // Some shared setters
6929 VMLElement.prototype.ySetter =
6930 VMLElement.prototype.widthSetter =
6931 VMLElement.prototype.heightSetter =
6932 VMLElement.prototype.xSetter;
6933
6934
6935 /**
6936 * The VML renderer
6937 */
6938 VMLRendererExtension = { // inherit SVGRenderer
6939
6940 Element: VMLElement,
6941 isIE8: win.navigator.userAgent.indexOf('MSIE 8.0') > -1,
6942
6943
6944 /**
6945 * Initialize the VMLRenderer
6946 * @param {Object} container
6947 * @param {Number} width
6948 * @param {Number} height
6949 */
6950 init: function(container, width, height) {
6951 var renderer = this,
6952 boxWrapper,
6953 box,
6954 css;
6955
6956 renderer.alignedObjects = [];
6957
6958 boxWrapper = renderer.createElement('div')
6959 .css({
6960 position: 'relative'
6961 });
6962 box = boxWrapper.element;
6963 container.appendChild(boxWrapper.element);
6964
6965
6966 // generate the containing box
6967 renderer.isVML = true;
6968 renderer.box = box;
6969 renderer.boxWrapper = boxWrapper;
6970 renderer.gradients = {};
6971 renderer.cache = {}; // Cache for numerical bounding boxes
6972 renderer.cacheKeys = [];
6973 renderer.imgCount = 0;
6974
6975
6976 renderer.setSize(width, height, false);
6977
6978 // The only way to make IE6 and IE7 print is to use a global namespace. However,
6979 // with IE8 the only way to make the dynamic shapes visible in screen and print mode
6980 // seems to be to add the xmlns attribute and the behaviour style inline.
6981 if (!doc.namespaces.hcv) {
6982
6983 doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml');
6984
6985 // Setup default CSS (#2153, #2368, #2384)
6986 css = 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke' +
6987 '{ behavior:url(#default#VML); display: inline-block; } ';
6988 try {
6989 doc.createStyleSheet().cssText = css;
6990 } catch (e) {
6991 doc.styleSheets[0].cssText += css;
6992 }
6993
6994 }
6995 },
6996
6997
6998 /**
6999 * Detect whether the renderer is hidden. This happens when one of the parent elements
7000 * has display: none
7001 */
7002 isHidden: function() {
7003 return !this.box.offsetWidth;
7004 },
7005
7006 /**
7007 * Define a clipping rectangle. In VML it is accomplished by storing the values
7008 * for setting the CSS style to all associated members.
7009 *
7010 * @param {Number} x
7011 * @param {Number} y
7012 * @param {Number} width
7013 * @param {Number} height
7014 */
7015 clipRect: function(x, y, width, height) {
7016
7017 // create a dummy element
7018 var clipRect = this.createElement(),
7019 isObj = isObject(x);
7020
7021 // mimic a rectangle with its style object for automatic updating in attr
7022 return extend(clipRect, {
7023 members: [],
7024 count: 0,
7025 left: (isObj ? x.x : x) + 1,
7026 top: (isObj ? x.y : y) + 1,
7027 width: (isObj ? x.width : width) - 1,
7028 height: (isObj ? x.height : height) - 1,
7029 getCSS: function(wrapper) {
7030 var element = wrapper.element,
7031 nodeName = element.nodeName,
7032 isShape = nodeName === 'shape',
7033 inverted = wrapper.inverted,
7034 rect = this,
7035 top = rect.top - (isShape ? element.offsetTop : 0),
7036 left = rect.left,
7037 right = left + rect.width,
7038 bottom = top + rect.height,
7039 ret = {
7040 clip: 'rect(' +
7041 Math.round(inverted ? left : top) + 'px,' +
7042 Math.round(inverted ? bottom : right) + 'px,' +
7043 Math.round(inverted ? right : bottom) + 'px,' +
7044 Math.round(inverted ? top : left) + 'px)'
7045 };
7046
7047 // issue 74 workaround
7048 if (!inverted && wrapper.docMode8 && nodeName === 'DIV') {
7049 extend(ret, {
7050 width: right + 'px',
7051 height: bottom + 'px'
7052 });
7053 }
7054 return ret;
7055 },
7056
7057 // used in attr and animation to update the clipping of all members
7058 updateClipping: function() {
7059 each(clipRect.members, function(member) {
7060 // Member.element is falsy on deleted series, like in
7061 // stock/members/series-remove demo. Should be removed
7062 // from members, but this will do.
7063 if (member.element) {
7064 member.css(clipRect.getCSS(member));
7065 }
7066 });
7067 }
7068 });
7069
7070 },
7071
7072
7073 /**
7074 * Take a color and return it if it's a string, make it a gradient if it's a
7075 * gradient configuration object, and apply opacity.
7076 *
7077 * @param {Object} color The color or config object
7078 */
7079 color: function(color, elem, prop, wrapper) {
7080 var renderer = this,
7081 colorObject,
7082 regexRgba = /^rgba/,
7083 markup,
7084 fillType,
7085 ret = 'none';
7086
7087 // Check for linear or radial gradient
7088 if (color && color.linearGradient) {
7089 fillType = 'gradient';
7090 } else if (color && color.radialGradient) {
7091 fillType = 'pattern';
7092 }
7093
7094
7095 if (fillType) {
7096
7097 var stopColor,
7098 stopOpacity,
7099 gradient = color.linearGradient || color.radialGradient,
7100 x1,
7101 y1,
7102 x2,
7103 y2,
7104 opacity1,
7105 opacity2,
7106 color1,
7107 color2,
7108 fillAttr = '',
7109 stops = color.stops,
7110 firstStop,
7111 lastStop,
7112 colors = [],
7113 addFillNode = function() {
7114 // Add the fill subnode. When colors attribute is used, the meanings of opacity and o:opacity2
7115 // are reversed.
7116 markup = ['<fill colors="' + colors.join(',') +
7117 '" opacity="', opacity2, '" o:opacity2="',
7118 opacity1, '" type="', fillType, '" ', fillAttr,
7119 'focus="100%" method="any" />'
7120 ];
7121 createElement(renderer.prepVML(markup), null, null, elem);
7122 };
7123
7124 // Extend from 0 to 1
7125 firstStop = stops[0];
7126 lastStop = stops[stops.length - 1];
7127 if (firstStop[0] > 0) {
7128 stops.unshift([
7129 0,
7130 firstStop[1]
7131 ]);
7132 }
7133 if (lastStop[0] < 1) {
7134 stops.push([
7135 1,
7136 lastStop[1]
7137 ]);
7138 }
7139
7140 // Compute the stops
7141 each(stops, function(stop, i) {
7142 if (regexRgba.test(stop[1])) {
7143 colorObject = H.color(stop[1]);
7144 stopColor = colorObject.get('rgb');
7145 stopOpacity = colorObject.get('a');
7146 } else {
7147 stopColor = stop[1];
7148 stopOpacity = 1;
7149 }
7150
7151 // Build the color attribute
7152 colors.push((stop[0] * 100) + '% ' + stopColor);
7153
7154 // Only start and end opacities are allowed, so we use the first and the last
7155 if (!i) {
7156 opacity1 = stopOpacity;
7157 color2 = stopColor;
7158 } else {
7159 opacity2 = stopOpacity;
7160 color1 = stopColor;
7161 }
7162 });
7163
7164 // Apply the gradient to fills only.
7165 if (prop === 'fill') {
7166
7167 // Handle linear gradient angle
7168 if (fillType === 'gradient') {
7169 x1 = gradient.x1 || gradient[0] || 0;
7170 y1 = gradient.y1 || gradient[1] || 0;
7171 x2 = gradient.x2 || gradient[2] || 0;
7172 y2 = gradient.y2 || gradient[3] || 0;
7173 fillAttr = 'angle="' + (90 - Math.atan(
7174 (y2 - y1) / // y vector
7175 (x2 - x1) // x vector
7176 ) * 180 / Math.PI) + '"';
7177
7178 addFillNode();
7179
7180 // Radial (circular) gradient
7181 } else {
7182
7183 var r = gradient.r,
7184 sizex = r * 2,
7185 sizey = r * 2,
7186 cx = gradient.cx,
7187 cy = gradient.cy,
7188 radialReference = elem.radialReference,
7189 bBox,
7190 applyRadialGradient = function() {
7191 if (radialReference) {
7192 bBox = wrapper.getBBox();
7193 cx += (radialReference[0] - bBox.x) / bBox.width - 0.5;
7194 cy += (radialReference[1] - bBox.y) / bBox.height - 0.5;
7195 sizex *= radialReference[2] / bBox.width;
7196 sizey *= radialReference[2] / bBox.height;
7197 }
7198 fillAttr = 'src="' + H.getOptions().global.VMLRadialGradientURL + '" ' +
7199 'size="' + sizex + ',' + sizey + '" ' +
7200 'origin="0.5,0.5" ' +
7201 'position="' + cx + ',' + cy + '" ' +
7202 'color2="' + color2 + '" ';
7203
7204 addFillNode();
7205 };
7206
7207 // Apply radial gradient
7208 if (wrapper.added) {
7209 applyRadialGradient();
7210 } else {
7211 // We need to know the bounding box to get the size and position right
7212 wrapper.onAdd = applyRadialGradient;
7213 }
7214
7215 // The fill element's color attribute is broken in IE8 standards mode, so we
7216 // need to set the parent shape's fillcolor attribute instead.
7217 ret = color1;
7218 }
7219
7220 // Gradients are not supported for VML stroke, return the first color. #722.
7221 } else {
7222 ret = stopColor;
7223 }
7224
7225 // If the color is an rgba color, split it and add a fill node
7226 // to hold the opacity component
7227 } else if (regexRgba.test(color) && elem.tagName !== 'IMG') {
7228
7229 colorObject = H.color(color);
7230
7231 wrapper[prop + '-opacitySetter'](colorObject.get('a'), prop, elem);
7232
7233 ret = colorObject.get('rgb');
7234
7235
7236 } else {
7237 var propNodes = elem.getElementsByTagName(prop); // 'stroke' or 'fill' node
7238 if (propNodes.length) {
7239 propNodes[0].opacity = 1;
7240 propNodes[0].type = 'solid';
7241 }
7242 ret = color;
7243 }
7244
7245 return ret;
7246 },
7247
7248 /**
7249 * Take a VML string and prepare it for either IE8 or IE6/IE7.
7250 * @param {Array} markup A string array of the VML markup to prepare
7251 */
7252 prepVML: function(markup) {
7253 var vmlStyle = 'display:inline-block;behavior:url(#default#VML);',
7254 isIE8 = this.isIE8;
7255
7256 markup = markup.join('');
7257
7258 if (isIE8) { // add xmlns and style inline
7259 markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />');
7260 if (markup.indexOf('style="') === -1) {
7261 markup = markup.replace('/>', ' style="' + vmlStyle + '" />');
7262 } else {
7263 markup = markup.replace('style="', 'style="' + vmlStyle);
7264 }
7265
7266 } else { // add namespace
7267 markup = markup.replace('<', '<hcv:');
7268 }
7269
7270 return markup;
7271 },
7272
7273 /**
7274 * Create rotated and aligned text
7275 * @param {String} str
7276 * @param {Number} x
7277 * @param {Number} y
7278 */
7279 text: SVGRenderer.prototype.html,
7280
7281 /**
7282 * Create and return a path element
7283 * @param {Array} path
7284 */
7285 path: function(path) {
7286 var attr = {
7287 // subpixel precision down to 0.1 (width and height = 1px)
7288 coordsize: '10 10'
7289 };
7290 if (isArray(path)) {
7291 attr.d = path;
7292 } else if (isObject(path)) { // attributes
7293 extend(attr, path);
7294 }
7295 // create the shape
7296 return this.createElement('shape').attr(attr);
7297 },
7298
7299 /**
7300 * Create and return a circle element. In VML circles are implemented as
7301 * shapes, which is faster than v:oval
7302 * @param {Number} x
7303 * @param {Number} y
7304 * @param {Number} r
7305 */
7306 circle: function(x, y, r) {
7307 var circle = this.symbol('circle');
7308 if (isObject(x)) {
7309 r = x.r;
7310 y = x.y;
7311 x = x.x;
7312 }
7313 circle.isCircle = true; // Causes x and y to mean center (#1682)
7314 circle.r = r;
7315 return circle.attr({
7316 x: x,
7317 y: y
7318 });
7319 },
7320
7321 /**
7322 * Create a group using an outer div and an inner v:group to allow rotating
7323 * and flipping. A simple v:group would have problems with positioning
7324 * child HTML elements and CSS clip.
7325 *
7326 * @param {String} name The name of the group
7327 */
7328 g: function(name) {
7329 var wrapper,
7330 attribs;
7331
7332 // set the class name
7333 if (name) {
7334 attribs = {
7335 'className': 'highcharts-' + name,
7336 'class': 'highcharts-' + name
7337 };
7338 }
7339
7340 // the div to hold HTML and clipping
7341 wrapper = this.createElement('div').attr(attribs);
7342
7343 return wrapper;
7344 },
7345
7346 /**
7347 * VML override to create a regular HTML image
7348 * @param {String} src
7349 * @param {Number} x
7350 * @param {Number} y
7351 * @param {Number} width
7352 * @param {Number} height
7353 */
7354 image: function(src, x, y, width, height) {
7355 var obj = this.createElement('img')
7356 .attr({
7357 src: src
7358 });
7359
7360 if (arguments.length > 1) {
7361 obj.attr({
7362 x: x,
7363 y: y,
7364 width: width,
7365 height: height
7366 });
7367 }
7368 return obj;
7369 },
7370
7371 /**
7372 * For rectangles, VML uses a shape for rect to overcome bugs and rotation problems
7373 */
7374 createElement: function(nodeName) {
7375 return nodeName === 'rect' ?
7376 this.symbol(nodeName) :
7377 SVGRenderer.prototype.createElement.call(this, nodeName);
7378 },
7379
7380 /**
7381 * In the VML renderer, each child of an inverted div (group) is inverted
7382 * @param {Object} element
7383 * @param {Object} parentNode
7384 */
7385 invertChild: function(element, parentNode) {
7386 var ren = this,
7387 parentStyle = parentNode.style,
7388 imgStyle = element.tagName === 'IMG' && element.style; // #1111
7389
7390 css(element, {
7391 flip: 'x',
7392 left: pInt(parentStyle.width) - (imgStyle ? pInt(imgStyle.top) : 1),
7393 top: pInt(parentStyle.height) - (imgStyle ? pInt(imgStyle.left) : 1),
7394 rotation: -90
7395 });
7396
7397 // Recursively invert child elements, needed for nested composite
7398 // shapes like box plots and error bars. #1680, #1806.
7399 each(element.childNodes, function(child) {
7400 ren.invertChild(child, element);
7401 });
7402 },
7403
7404 /**
7405 * Symbol definitions that override the parent SVG renderer's symbols
7406 *
7407 */
7408 symbols: {
7409 // VML specific arc function
7410 arc: function(x, y, w, h, options) {
7411 var start = options.start,
7412 end = options.end,
7413 radius = options.r || w || h,
7414 innerRadius = options.innerR,
7415 cosStart = Math.cos(start),
7416 sinStart = Math.sin(start),
7417 cosEnd = Math.cos(end),
7418 sinEnd = Math.sin(end),
7419 ret;
7420
7421 if (end - start === 0) { // no angle, don't show it.
7422 return ['x'];
7423 }
7424
7425 ret = [
7426 'wa', // clockwise arc to
7427 x - radius, // left
7428 y - radius, // top
7429 x + radius, // right
7430 y + radius, // bottom
7431 x + radius * cosStart, // start x
7432 y + radius * sinStart, // start y
7433 x + radius * cosEnd, // end x
7434 y + radius * sinEnd // end y
7435 ];
7436
7437 if (options.open && !innerRadius) {
7438 ret.push(
7439 'e',
7440 'M',
7441 x, // - innerRadius,
7442 y // - innerRadius
7443 );
7444 }
7445
7446 ret.push(
7447 'at', // anti clockwise arc to
7448 x - innerRadius, // left
7449 y - innerRadius, // top
7450 x + innerRadius, // right
7451 y + innerRadius, // bottom
7452 x + innerRadius * cosEnd, // start x
7453 y + innerRadius * sinEnd, // start y
7454 x + innerRadius * cosStart, // end x
7455 y + innerRadius * sinStart, // end y
7456 'x', // finish path
7457 'e' // close
7458 );
7459
7460 ret.isArc = true;
7461 return ret;
7462
7463 },
7464 // Add circle symbol path. This performs significantly faster than v:oval.
7465 circle: function(x, y, w, h, wrapper) {
7466
7467 if (wrapper && defined(wrapper.r)) {
7468 w = h = 2 * wrapper.r;
7469 }
7470
7471 // Center correction, #1682
7472 if (wrapper && wrapper.isCircle) {
7473 x -= w / 2;
7474 y -= h / 2;
7475 }
7476
7477 // Return the path
7478 return [
7479 'wa', // clockwisearcto
7480 x, // left
7481 y, // top
7482 x + w, // right
7483 y + h, // bottom
7484 x + w, // start x
7485 y + h / 2, // start y
7486 x + w, // end x
7487 y + h / 2, // end y
7488 //'x', // finish path
7489 'e' // close
7490 ];
7491 },
7492 /**
7493 * Add rectangle symbol path which eases rotation and omits arcsize problems
7494 * compared to the built-in VML roundrect shape. When borders are not rounded,
7495 * use the simpler square path, else use the callout path without the arrow.
7496 */
7497 rect: function(x, y, w, h, options) {
7498 return SVGRenderer.prototype.symbols[!defined(options) || !options.r ? 'square' : 'callout'].call(0, x, y, w, h, options);
7499 }
7500 }
7501 };
7502 H.VMLRenderer = VMLRenderer = function() {
7503 this.init.apply(this, arguments);
7504 };
7505 VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension);
7506
7507 // general renderer
7508 H.Renderer = VMLRenderer;
7509 }
7510
7511 // This method is used with exporting in old IE, when emulating SVG (see #2314)
7512 SVGRenderer.prototype.measureSpanWidth = function(text, styles) {
7513 var measuringSpan = doc.createElement('span'),
7514 offsetWidth,
7515 textNode = doc.createTextNode(text);
7516
7517 measuringSpan.appendChild(textNode);
7518 css(measuringSpan, styles);
7519 this.box.appendChild(measuringSpan);
7520 offsetWidth = measuringSpan.offsetWidth;
7521 discardElement(measuringSpan); // #2463
7522 return offsetWidth;
7523 };
7524
7525
7526 /* ****************************************************************************
7527 * *
7528 * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE *
7529 * *
7530 *****************************************************************************/
7531
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 correctFloat = H.correctFloat,
7542 defined = H.defined,
7543 destroyObjectProperties = H.destroyObjectProperties,
7544 isNumber = H.isNumber,
7545 merge = H.merge,
7546 pick = H.pick,
7547 deg2rad = H.deg2rad;
7548
7549 /**
7550 * The Tick class
7551 */
7552 H.Tick = function(axis, pos, type, noLabel) {
7553 this.axis = axis;
7554 this.pos = pos;
7555 this.type = type || '';
7556 this.isNew = true;
7557
7558 if (!type && !noLabel) {
7559 this.addLabel();
7560 }
7561 };
7562
7563 H.Tick.prototype = {
7564 /**
7565 * Write the tick label
7566 */
7567 addLabel: function() {
7568 var tick = this,
7569 axis = tick.axis,
7570 options = axis.options,
7571 chart = axis.chart,
7572 categories = axis.categories,
7573 names = axis.names,
7574 pos = tick.pos,
7575 labelOptions = options.labels,
7576 str,
7577 tickPositions = axis.tickPositions,
7578 isFirst = pos === tickPositions[0],
7579 isLast = pos === tickPositions[tickPositions.length - 1],
7580 value = categories ?
7581 pick(categories[pos], names[pos], pos) :
7582 pos,
7583 label = tick.label,
7584 tickPositionInfo = tickPositions.info,
7585 dateTimeLabelFormat;
7586
7587 // Set the datetime label format. If a higher rank is set for this position, use that. If not,
7588 // use the general format.
7589 if (axis.isDatetimeAxis && tickPositionInfo) {
7590 dateTimeLabelFormat =
7591 options.dateTimeLabelFormats[
7592 tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName
7593 ];
7594 }
7595 // set properties for access in render method
7596 tick.isFirst = isFirst;
7597 tick.isLast = isLast;
7598
7599 // get the string
7600 str = axis.labelFormatter.call({
7601 axis: axis,
7602 chart: chart,
7603 isFirst: isFirst,
7604 isLast: isLast,
7605 dateTimeLabelFormat: dateTimeLabelFormat,
7606 value: axis.isLog ? correctFloat(axis.lin2log(value)) : value
7607 });
7608
7609 // prepare CSS
7610 //css = width && { width: Math.max(1, Math.round(width - 2 * (labelOptions.padding || 10))) + 'px' };
7611
7612 // first call
7613 if (!defined(label)) {
7614
7615 tick.label = label =
7616 defined(str) && labelOptions.enabled ?
7617 chart.renderer.text(
7618 str,
7619 0,
7620 0,
7621 labelOptions.useHTML
7622 )
7623
7624 // without position absolute, IE export sometimes is wrong
7625 .css(merge(labelOptions.style))
7626
7627 .add(axis.labelGroup):
7628 null;
7629 tick.labelLength = label && label.getBBox().width; // Un-rotated length
7630 tick.rotation = 0; // Base value to detect change for new calls to getBBox
7631
7632 // update
7633 } else if (label) {
7634 label.attr({
7635 text: str
7636 });
7637 }
7638 },
7639
7640 /**
7641 * Get the offset height or width of the label
7642 */
7643 getLabelSize: function() {
7644 return this.label ?
7645 this.label.getBBox()[this.axis.horiz ? 'height' : 'width'] :
7646 0;
7647 },
7648
7649 /**
7650 * Handle the label overflow by adjusting the labels to the left and right edge, or
7651 * hide them if they collide into the neighbour label.
7652 */
7653 handleOverflow: function(xy) {
7654 var axis = this.axis,
7655 pxPos = xy.x,
7656 chartWidth = axis.chart.chartWidth,
7657 spacing = axis.chart.spacing,
7658 leftBound = pick(axis.labelLeft, Math.min(axis.pos, spacing[3])),
7659 rightBound = pick(axis.labelRight, Math.max(axis.pos + axis.len, chartWidth - spacing[1])),
7660 label = this.label,
7661 rotation = this.rotation,
7662 factor = {
7663 left: 0,
7664 center: 0.5,
7665 right: 1
7666 }[axis.labelAlign],
7667 labelWidth = label.getBBox().width,
7668 slotWidth = axis.getSlotWidth(),
7669 modifiedSlotWidth = slotWidth,
7670 xCorrection = factor,
7671 goRight = 1,
7672 leftPos,
7673 rightPos,
7674 textWidth,
7675 css = {};
7676
7677 // Check if the label overshoots the chart spacing box. If it does, move it.
7678 // If it now overshoots the slotWidth, add ellipsis.
7679 if (!rotation) {
7680 leftPos = pxPos - factor * labelWidth;
7681 rightPos = pxPos + (1 - factor) * labelWidth;
7682
7683 if (leftPos < leftBound) {
7684 modifiedSlotWidth = xy.x + modifiedSlotWidth * (1 - factor) - leftBound;
7685 } else if (rightPos > rightBound) {
7686 modifiedSlotWidth = rightBound - xy.x + modifiedSlotWidth * factor;
7687 goRight = -1;
7688 }
7689
7690 modifiedSlotWidth = Math.min(slotWidth, modifiedSlotWidth); // #4177
7691 if (modifiedSlotWidth < slotWidth && axis.labelAlign === 'center') {
7692 xy.x += goRight * (slotWidth - modifiedSlotWidth - xCorrection *
7693 (slotWidth - Math.min(labelWidth, modifiedSlotWidth)));
7694 }
7695 // If the label width exceeds the available space, set a text width to be
7696 // picked up below. Also, if a width has been set before, we need to set a new
7697 // one because the reported labelWidth will be limited by the box (#3938).
7698 if (labelWidth > modifiedSlotWidth || (axis.autoRotation && (label.styles || {}).width)) {
7699 textWidth = modifiedSlotWidth;
7700 }
7701
7702 // Add ellipsis to prevent rotated labels to be clipped against the edge of the chart
7703 } else if (rotation < 0 && pxPos - factor * labelWidth < leftBound) {
7704 textWidth = Math.round(pxPos / Math.cos(rotation * deg2rad) - leftBound);
7705 } else if (rotation > 0 && pxPos + factor * labelWidth > rightBound) {
7706 textWidth = Math.round((chartWidth - pxPos) / Math.cos(rotation * deg2rad));
7707 }
7708
7709 if (textWidth) {
7710 css.width = textWidth;
7711 if (!(axis.options.labels.style || {}).textOverflow) {
7712 css.textOverflow = 'ellipsis';
7713 }
7714 label.css(css);
7715 }
7716 },
7717
7718 /**
7719 * Get the x and y position for ticks and labels
7720 */
7721 getPosition: function(horiz, pos, tickmarkOffset, old) {
7722 var axis = this.axis,
7723 chart = axis.chart,
7724 cHeight = (old && chart.oldChartHeight) || chart.chartHeight;
7725
7726 return {
7727 x: horiz ?
7728 axis.translate(pos + tickmarkOffset, null, null, old) + axis.transB : axis.left + axis.offset +
7729 (axis.opposite ?
7730 ((old && chart.oldChartWidth) || chart.chartWidth) - axis.right - axis.left :
7731 0
7732 ),
7733
7734 y: horiz ?
7735 cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) : cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB
7736 };
7737
7738 },
7739
7740 /**
7741 * Get the x, y position of the tick label
7742 */
7743 getLabelPosition: function(x, y, label, horiz, labelOptions, tickmarkOffset, index, step) {
7744 var axis = this.axis,
7745 transA = axis.transA,
7746 reversed = axis.reversed,
7747 staggerLines = axis.staggerLines,
7748 rotCorr = axis.tickRotCorr || {
7749 x: 0,
7750 y: 0
7751 },
7752 yOffset = labelOptions.y,
7753 line;
7754
7755 if (!defined(yOffset)) {
7756 if (axis.side === 0) {
7757 yOffset = label.rotation ? -8 : -label.getBBox().height;
7758 } else if (axis.side === 2) {
7759 yOffset = rotCorr.y + 8;
7760 } else {
7761 // #3140, #3140
7762 yOffset = Math.cos(label.rotation * deg2rad) * (rotCorr.y - label.getBBox(false, 0).height / 2);
7763 }
7764 }
7765
7766 x = x + labelOptions.x + rotCorr.x - (tickmarkOffset && horiz ?
7767 tickmarkOffset * transA * (reversed ? -1 : 1) : 0);
7768 y = y + yOffset - (tickmarkOffset && !horiz ?
7769 tickmarkOffset * transA * (reversed ? 1 : -1) : 0);
7770
7771 // Correct for staggered labels
7772 if (staggerLines) {
7773 line = (index / (step || 1) % staggerLines);
7774 if (axis.opposite) {
7775 line = staggerLines - line - 1;
7776 }
7777 y += line * (axis.labelOffset / staggerLines);
7778 }
7779
7780 return {
7781 x: x,
7782 y: Math.round(y)
7783 };
7784 },
7785
7786 /**
7787 * Extendible method to return the path of the marker
7788 */
7789 getMarkPath: function(x, y, tickLength, tickWidth, horiz, renderer) {
7790 return renderer.crispLine([
7791 'M',
7792 x,
7793 y,
7794 'L',
7795 x + (horiz ? 0 : -tickLength),
7796 y + (horiz ? tickLength : 0)
7797 ], tickWidth);
7798 },
7799
7800 /**
7801 * Put everything in place
7802 *
7803 * @param index {Number}
7804 * @param old {Boolean} Use old coordinates to prepare an animation into new position
7805 */
7806 render: function(index, old, opacity) {
7807 var tick = this,
7808 axis = tick.axis,
7809 options = axis.options,
7810 chart = axis.chart,
7811 renderer = chart.renderer,
7812 horiz = axis.horiz,
7813 type = tick.type,
7814 label = tick.label,
7815 pos = tick.pos,
7816 labelOptions = options.labels,
7817 gridLine = tick.gridLine,
7818 tickPrefix = type ? type + 'Tick' : 'tick',
7819 tickSize = axis.tickSize(tickPrefix),
7820 gridLinePath,
7821 mark = tick.mark,
7822 isNewMark = !mark,
7823 step = labelOptions.step,
7824 attribs = {},
7825 show = true,
7826 tickmarkOffset = axis.tickmarkOffset,
7827 xy = tick.getPosition(horiz, pos, tickmarkOffset, old),
7828 x = xy.x,
7829 y = xy.y,
7830 reverseCrisp = ((horiz && x === axis.pos + axis.len) ||
7831 (!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687
7832
7833
7834 var gridPrefix = type ? type + 'Grid' : 'grid',
7835 gridLineWidth = options[gridPrefix + 'LineWidth'],
7836 gridLineColor = options[gridPrefix + 'LineColor'],
7837 dashStyle = options[gridPrefix + 'LineDashStyle'],
7838 tickWidth = pick(options[tickPrefix + 'Width'], !type && axis.isXAxis ? 1 : 0), // X axis defaults to 1
7839 tickColor = options[tickPrefix + 'Color'];
7840
7841
7842 opacity = pick(opacity, 1);
7843 this.isActive = true;
7844
7845 // Create the grid line
7846 if (!gridLine) {
7847
7848 attribs.stroke = gridLineColor;
7849 attribs['stroke-width'] = gridLineWidth;
7850 if (dashStyle) {
7851 attribs.dashstyle = dashStyle;
7852 }
7853
7854 if (!type) {
7855 attribs.zIndex = 1;
7856 }
7857 if (old) {
7858 attribs.opacity = 0;
7859 }
7860 tick.gridLine = gridLine = renderer.path()
7861 .attr(attribs)
7862 .addClass('highcharts-' + (type ? type + '-' : '') + 'grid-line')
7863 .add(axis.gridGroup);
7864 }
7865
7866 // If the parameter 'old' is set, the current call will be followed
7867 // by another call, therefore do not do any animations this time
7868 if (!old && gridLine) {
7869 gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLine.strokeWidth() * reverseCrisp, old, true);
7870 if (gridLinePath) {
7871 gridLine[tick.isNew ? 'attr' : 'animate']({
7872 d: gridLinePath,
7873 opacity: opacity
7874 });
7875 }
7876 }
7877
7878 // create the tick mark
7879 if (tickSize) {
7880
7881 // negate the length
7882 if (axis.opposite) {
7883 tickSize[0] = -tickSize[0];
7884 }
7885
7886 // First time, create it
7887 if (isNewMark) {
7888 tick.mark = mark = renderer.path()
7889 .addClass('highcharts-' + (type ? type + '-' : '') + 'tick')
7890 .add(axis.axisGroup);
7891
7892
7893 mark.attr({
7894 stroke: tickColor,
7895 'stroke-width': tickWidth
7896 });
7897
7898 }
7899 mark[isNewMark ? 'attr' : 'animate']({
7900 d: tick.getMarkPath(x, y, tickSize[0], mark.strokeWidth() * reverseCrisp, horiz, renderer),
7901 opacity: opacity
7902 });
7903
7904 }
7905
7906 // the label is created on init - now move it into place
7907 if (label && isNumber(x)) {
7908 label.xy = xy = tick.getLabelPosition(x, y, label, horiz, labelOptions, tickmarkOffset, index, step);
7909
7910 // Apply show first and show last. If the tick is both first and last, it is
7911 // a single centered tick, in which case we show the label anyway (#2100).
7912 if ((tick.isFirst && !tick.isLast && !pick(options.showFirstLabel, 1)) ||
7913 (tick.isLast && !tick.isFirst && !pick(options.showLastLabel, 1))) {
7914 show = false;
7915
7916 // Handle label overflow and show or hide accordingly
7917 } else if (horiz && !axis.isRadial && !labelOptions.step &&
7918 !labelOptions.rotation && !old && opacity !== 0) {
7919 tick.handleOverflow(xy);
7920 }
7921
7922 // apply step
7923 if (step && index % step) {
7924 // show those indices dividable by step
7925 show = false;
7926 }
7927
7928 // Set the new position, and show or hide
7929 if (show && isNumber(xy.y)) {
7930 xy.opacity = opacity;
7931 label[tick.isNew ? 'attr' : 'animate'](xy);
7932 } else {
7933 label.attr('y', -9999); // #1338
7934 }
7935 tick.isNew = false;
7936 }
7937 },
7938
7939 /**
7940 * Destructor for the tick prototype
7941 */
7942 destroy: function() {
7943 destroyObjectProperties(this, this.axis);
7944 }
7945 };
7946
7947 }(Highcharts));
7948 (function(H) {
7949 /**
7950 * (c) 2010-2016 Torstein Honsi
7951 *
7952 * License: www.highcharts.com/license
7953 */
7954 'use strict';
7955 var arrayMax = H.arrayMax,
7956 arrayMin = H.arrayMin,
7957 defined = H.defined,
7958 destroyObjectProperties = H.destroyObjectProperties,
7959 each = H.each,
7960 erase = H.erase,
7961 merge = H.merge,
7962 pick = H.pick;
7963 /*
7964 * The object wrapper for plot lines and plot bands
7965 * @param {Object} options
7966 */
7967 H.PlotLineOrBand = function(axis, options) {
7968 this.axis = axis;
7969
7970 if (options) {
7971 this.options = options;
7972 this.id = options.id;
7973 }
7974 };
7975
7976 H.PlotLineOrBand.prototype = {
7977
7978 /**
7979 * Render the plot line or plot band. If it is already existing,
7980 * move it.
7981 */
7982 render: function() {
7983 var plotLine = this,
7984 axis = plotLine.axis,
7985 horiz = axis.horiz,
7986 options = plotLine.options,
7987 optionsLabel = options.label,
7988 label = plotLine.label,
7989 to = options.to,
7990 from = options.from,
7991 value = options.value,
7992 isBand = defined(from) && defined(to),
7993 isLine = defined(value),
7994 svgElem = plotLine.svgElem,
7995 isNew = !svgElem,
7996 path = [],
7997 addEvent,
7998 eventType,
7999 color = options.color,
8000 zIndex = pick(options.zIndex, 0),
8001 events = options.events,
8002 attribs = {
8003 'class': 'highcharts-plot-' + (isBand ? 'band ' : 'line ') + (options.className || '')
8004 },
8005 groupAttribs = {},
8006 renderer = axis.chart.renderer,
8007 groupName = isBand ? 'bands' : 'lines',
8008 group,
8009 log2lin = axis.log2lin;
8010
8011 // logarithmic conversion
8012 if (axis.isLog) {
8013 from = log2lin(from);
8014 to = log2lin(to);
8015 value = log2lin(value);
8016 }
8017
8018
8019 // Set the presentational attributes
8020 if (isLine) {
8021 attribs = {
8022 stroke: color,
8023 'stroke-width': options.width
8024 };
8025 if (options.dashStyle) {
8026 attribs.dashstyle = options.dashStyle;
8027 }
8028
8029 } else if (isBand) { // plot band
8030 if (color) {
8031 attribs.fill = color;
8032 }
8033 if (options.borderWidth) {
8034 attribs.stroke = options.borderColor;
8035 attribs['stroke-width'] = options.borderWidth;
8036 }
8037 }
8038
8039
8040 // Grouping and zIndex
8041 groupAttribs.zIndex = zIndex;
8042 groupName += '-' + zIndex;
8043
8044 group = axis[groupName];
8045 if (!group) {
8046 axis[groupName] = group = renderer.g('plot-' + groupName)
8047 .attr(groupAttribs).add();
8048 }
8049
8050 // Create the path
8051 if (isNew) {
8052 plotLine.svgElem = svgElem =
8053 renderer
8054 .path()
8055 .attr(attribs).add(group);
8056 }
8057
8058
8059 // Set the path or return
8060 if (isLine) {
8061 path = axis.getPlotLinePath(value, svgElem.strokeWidth());
8062 } else if (isBand) { // plot band
8063 path = axis.getPlotBandPath(from, to, options);
8064 } else {
8065 return;
8066 }
8067
8068 // common for lines and bands
8069 if (isNew && path && path.length) {
8070 svgElem.attr({
8071 d: path
8072 });
8073
8074 // events
8075 if (events) {
8076 addEvent = function(eventType) {
8077 svgElem.on(eventType, function(e) {
8078 events[eventType].apply(plotLine, [e]);
8079 });
8080 };
8081 for (eventType in events) {
8082 addEvent(eventType);
8083 }
8084 }
8085 } else if (svgElem) {
8086 if (path) {
8087 svgElem.show();
8088 svgElem.animate({
8089 d: path
8090 });
8091 } else {
8092 svgElem.hide();
8093 if (label) {
8094 plotLine.label = label = label.destroy();
8095 }
8096 }
8097 }
8098
8099 // the plot band/line label
8100 if (optionsLabel && defined(optionsLabel.text) && path && path.length &&
8101 axis.width > 0 && axis.height > 0 && !path.flat) {
8102 // apply defaults
8103 optionsLabel = merge({
8104 align: horiz && isBand && 'center',
8105 x: horiz ? !isBand && 4 : 10,
8106 verticalAlign: !horiz && isBand && 'middle',
8107 y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4,
8108 rotation: horiz && !isBand && 90
8109 }, optionsLabel);
8110
8111 this.renderLabel(optionsLabel, path, isBand, zIndex);
8112
8113 } else if (label) { // move out of sight
8114 label.hide();
8115 }
8116
8117 // chainable
8118 return plotLine;
8119 },
8120
8121 /**
8122 * Render and align label for plot line or band.
8123 */
8124 renderLabel: function(optionsLabel, path, isBand, zIndex) {
8125 var plotLine = this,
8126 label = plotLine.label,
8127 renderer = plotLine.axis.chart.renderer,
8128 attribs,
8129 xs,
8130 ys,
8131 x,
8132 y;
8133
8134 // add the SVG element
8135 if (!label) {
8136 attribs = {
8137 align: optionsLabel.textAlign || optionsLabel.align,
8138 rotation: optionsLabel.rotation,
8139 'class': 'highcharts-plot-' + (isBand ? 'band' : 'line') + '-label ' + (optionsLabel.className || '')
8140 };
8141
8142 attribs.zIndex = zIndex;
8143
8144 plotLine.label = label = renderer.text(
8145 optionsLabel.text,
8146 0,
8147 0,
8148 optionsLabel.useHTML
8149 )
8150 .attr(attribs)
8151 .add();
8152
8153
8154 label.css(optionsLabel.style);
8155
8156 }
8157
8158 // get the bounding box and align the label
8159 // #3000 changed to better handle choice between plotband or plotline
8160 xs = [path[1], path[4], (isBand ? path[6] : path[1])];
8161 ys = [path[2], path[5], (isBand ? path[7] : path[2])];
8162 x = arrayMin(xs);
8163 y = arrayMin(ys);
8164
8165 label.align(optionsLabel, false, {
8166 x: x,
8167 y: y,
8168 width: arrayMax(xs) - x,
8169 height: arrayMax(ys) - y
8170 });
8171 label.show();
8172 },
8173
8174 /**
8175 * Remove the plot line or band
8176 */
8177 destroy: function() {
8178 // remove it from the lookup
8179 erase(this.axis.plotLinesAndBands, this);
8180
8181 delete this.axis;
8182 destroyObjectProperties(this);
8183 }
8184 };
8185
8186 /**
8187 * Object with members for extending the Axis prototype
8188 * @todo Extend directly instead of adding object to Highcharts first
8189 */
8190
8191 H.AxisPlotLineOrBandExtension = {
8192
8193 /**
8194 * Create the path for a plot band
8195 */
8196 getPlotBandPath: function(from, to) {
8197 var toPath = this.getPlotLinePath(to, null, null, true),
8198 path = this.getPlotLinePath(from, null, null, true);
8199
8200 if (path && toPath) {
8201
8202 // Flat paths don't need labels (#3836)
8203 path.flat = path.toString() === toPath.toString();
8204
8205 path.push(
8206 toPath[4],
8207 toPath[5],
8208 toPath[1],
8209 toPath[2],
8210 'z' // #5909
8211 );
8212 } else { // outside the axis area
8213 path = null;
8214 }
8215
8216 return path;
8217 },
8218
8219 addPlotBand: function(options) {
8220 return this.addPlotBandOrLine(options, 'plotBands');
8221 },
8222
8223 addPlotLine: function(options) {
8224 return this.addPlotBandOrLine(options, 'plotLines');
8225 },
8226
8227 /**
8228 * Add a plot band or plot line after render time
8229 *
8230 * @param options {Object} The plotBand or plotLine configuration object
8231 */
8232 addPlotBandOrLine: function(options, coll) {
8233 var obj = new H.PlotLineOrBand(this, options).render(),
8234 userOptions = this.userOptions;
8235
8236 if (obj) { // #2189
8237 // Add it to the user options for exporting and Axis.update
8238 if (coll) {
8239 userOptions[coll] = userOptions[coll] || [];
8240 userOptions[coll].push(options);
8241 }
8242 this.plotLinesAndBands.push(obj);
8243 }
8244
8245 return obj;
8246 },
8247
8248 /**
8249 * Remove a plot band or plot line from the chart by id
8250 * @param {Object} id
8251 */
8252 removePlotBandOrLine: function(id) {
8253 var plotLinesAndBands = this.plotLinesAndBands,
8254 options = this.options,
8255 userOptions = this.userOptions,
8256 i = plotLinesAndBands.length;
8257 while (i--) {
8258 if (plotLinesAndBands[i].id === id) {
8259 plotLinesAndBands[i].destroy();
8260 }
8261 }
8262 each([options.plotLines || [], userOptions.plotLines || [], options.plotBands || [], userOptions.plotBands || []], function(arr) {
8263 i = arr.length;
8264 while (i--) {
8265 if (arr[i].id === id) {
8266 erase(arr, arr[i]);
8267 }
8268 }
8269 });
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 getMagnitude = H.getMagnitude,
10862 map = H.map,
10863 normalizeTickInterval = H.normalizeTickInterval,
10864 pick = H.pick;
10865 /**
10866 * Methods defined on the Axis prototype
10867 */
10868
10869 /**
10870 * Set the tick positions of a logarithmic axis
10871 */
10872 Axis.prototype.getLogTickPositions = function(interval, min, max, minor) {
10873 var axis = this,
10874 options = axis.options,
10875 axisLength = axis.len,
10876 lin2log = axis.lin2log,
10877 log2lin = axis.log2lin,
10878 // Since we use this method for both major and minor ticks,
10879 // use a local variable and return the result
10880 positions = [];
10881
10882 // Reset
10883 if (!minor) {
10884 axis._minorAutoInterval = null;
10885 }
10886
10887 // First case: All ticks fall on whole logarithms: 1, 10, 100 etc.
10888 if (interval >= 0.5) {
10889 interval = Math.round(interval);
10890 positions = axis.getLinearTickPositions(interval, min, max);
10891
10892 // Second case: We need intermediary ticks. For example
10893 // 1, 2, 4, 6, 8, 10, 20, 40 etc.
10894 } else if (interval >= 0.08) {
10895 var roundedMin = Math.floor(min),
10896 intermediate,
10897 i,
10898 j,
10899 len,
10900 pos,
10901 lastPos,
10902 break2;
10903
10904 if (interval > 0.3) {
10905 intermediate = [1, 2, 4];
10906 } else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc
10907 intermediate = [1, 2, 4, 6, 8];
10908 } else { // 0.1 equals ten minor ticks per 1, 10, 100 etc
10909 intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
10910 }
10911
10912 for (i = roundedMin; i < max + 1 && !break2; i++) {
10913 len = intermediate.length;
10914 for (j = 0; j < len && !break2; j++) {
10915 pos = log2lin(lin2log(i) * intermediate[j]);
10916 if (pos > min && (!minor || lastPos <= max) && lastPos !== undefined) { // #1670, lastPos is #3113
10917 positions.push(lastPos);
10918 }
10919
10920 if (lastPos > max) {
10921 break2 = true;
10922 }
10923 lastPos = pos;
10924 }
10925 }
10926
10927 // Third case: We are so deep in between whole logarithmic values that
10928 // we might as well handle the tick positions like a linear axis. For
10929 // example 1.01, 1.02, 1.03, 1.04.
10930 } else {
10931 var realMin = lin2log(min),
10932 realMax = lin2log(max),
10933 tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'],
10934 filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption,
10935 tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1),
10936 totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength;
10937
10938 interval = pick(
10939 filteredTickIntervalOption,
10940 axis._minorAutoInterval,
10941 (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1)
10942 );
10943
10944 interval = normalizeTickInterval(
10945 interval,
10946 null,
10947 getMagnitude(interval)
10948 );
10949
10950 positions = map(axis.getLinearTickPositions(
10951 interval,
10952 realMin,
10953 realMax
10954 ), log2lin);
10955
10956 if (!minor) {
10957 axis._minorAutoInterval = interval / 5;
10958 }
10959 }
10960
10961 // Set the axis-level tickInterval variable
10962 if (!minor) {
10963 axis.tickInterval = interval;
10964 }
10965 return positions;
10966 };
10967
10968 Axis.prototype.log2lin = function(num) {
10969 return Math.log(num) / Math.LN10;
10970 };
10971
10972 Axis.prototype.lin2log = function(num) {
10973 return Math.pow(10, num);
10974 };
10975
10976 }(Highcharts));
10977 (function(H) {
10978 /**
10979 * (c) 2010-2016 Torstein Honsi
10980 *
10981 * License: www.highcharts.com/license
10982 */
10983 'use strict';
10984 var dateFormat = H.dateFormat,
10985 each = H.each,
10986 extend = H.extend,
10987 format = H.format,
10988 isNumber = H.isNumber,
10989 map = H.map,
10990 merge = H.merge,
10991 pick = H.pick,
10992 splat = H.splat,
10993 syncTimeout = H.syncTimeout,
10994 timeUnits = H.timeUnits;
10995 /**
10996 * The tooltip object
10997 * @param {Object} chart The chart instance
10998 * @param {Object} options Tooltip options
10999 */
11000 H.Tooltip = function() {
11001 this.init.apply(this, arguments);
11002 };
11003
11004 H.Tooltip.prototype = {
11005
11006 init: function(chart, options) {
11007
11008 // Save the chart and options
11009 this.chart = chart;
11010 this.options = options;
11011
11012 // Keep track of the current series
11013 //this.currentSeries = undefined;
11014
11015 // List of crosshairs
11016 this.crosshairs = [];
11017
11018 // Current values of x and y when animating
11019 this.now = {
11020 x: 0,
11021 y: 0
11022 };
11023
11024 // The tooltip is initially hidden
11025 this.isHidden = true;
11026
11027
11028
11029 // Public property for getting the shared state.
11030 this.split = options.split && !chart.inverted;
11031 this.shared = options.shared || this.split;
11032
11033 },
11034
11035 /**
11036 * Destroy the single tooltips in a split tooltip.
11037 * If the tooltip is active then it is not destroyed, unless forced to.
11038 * @param {boolean} force Force destroy all tooltips.
11039 * @return {undefined}
11040 */
11041 cleanSplit: function(force) {
11042 each(this.chart.series, function(series) {
11043 var tt = series && series.tt;
11044 if (tt) {
11045 if (!tt.isActive || force) {
11046 series.tt = tt.destroy();
11047 } else {
11048 tt.isActive = false;
11049 }
11050 }
11051 });
11052 },
11053
11054
11055
11056
11057 /**
11058 * Create the Tooltip label element if it doesn't exist, then return the
11059 * label.
11060 */
11061 getLabel: function() {
11062
11063 var renderer = this.chart.renderer,
11064 options = this.options;
11065
11066 if (!this.label) {
11067 // Create the label
11068 if (this.split) {
11069 this.label = renderer.g('tooltip');
11070 } else {
11071 this.label = renderer.label(
11072 '',
11073 0,
11074 0,
11075 options.shape || 'callout',
11076 null,
11077 null,
11078 options.useHTML,
11079 null,
11080 'tooltip'
11081 )
11082 .attr({
11083 padding: options.padding,
11084 r: options.borderRadius
11085 });
11086
11087
11088 this.label
11089 .attr({
11090 'fill': options.backgroundColor,
11091 'stroke-width': options.borderWidth
11092 })
11093 // #2301, #2657
11094 .css(options.style)
11095 .shadow(options.shadow);
11096
11097 }
11098
11099
11100
11101 this.label
11102 .attr({
11103 zIndex: 8
11104 })
11105 .add();
11106 }
11107 return this.label;
11108 },
11109
11110 update: function(options) {
11111 this.destroy();
11112 this.init(this.chart, merge(true, this.options, options));
11113 },
11114
11115 /**
11116 * Destroy the tooltip and its elements.
11117 */
11118 destroy: function() {
11119 // Destroy and clear local variables
11120 if (this.label) {
11121 this.label = this.label.destroy();
11122 }
11123 if (this.split && this.tt) {
11124 this.cleanSplit(this.chart, true);
11125 this.tt = this.tt.destroy();
11126 }
11127 clearTimeout(this.hideTimer);
11128 clearTimeout(this.tooltipTimeout);
11129 },
11130
11131 /**
11132 * Provide a soft movement for the tooltip
11133 *
11134 * @param {Number} x
11135 * @param {Number} y
11136 * @private
11137 */
11138 move: function(x, y, anchorX, anchorY) {
11139 var tooltip = this,
11140 now = tooltip.now,
11141 animate = tooltip.options.animation !== false && !tooltip.isHidden &&
11142 // When we get close to the target position, abort animation and land on the right place (#3056)
11143 (Math.abs(x - now.x) > 1 || Math.abs(y - now.y) > 1),
11144 skipAnchor = tooltip.followPointer || tooltip.len > 1;
11145
11146 // Get intermediate values for animation
11147 extend(now, {
11148 x: animate ? (2 * now.x + x) / 3 : x,
11149 y: animate ? (now.y + y) / 2 : y,
11150 anchorX: skipAnchor ? undefined : animate ? (2 * now.anchorX + anchorX) / 3 : anchorX,
11151 anchorY: skipAnchor ? undefined : animate ? (now.anchorY + anchorY) / 2 : anchorY
11152 });
11153
11154 // Move to the intermediate value
11155 tooltip.getLabel().attr(now);
11156
11157
11158 // Run on next tick of the mouse tracker
11159 if (animate) {
11160
11161 // Never allow two timeouts
11162 clearTimeout(this.tooltipTimeout);
11163
11164 // Set the fixed interval ticking for the smooth tooltip
11165 this.tooltipTimeout = setTimeout(function() {
11166 // The interval function may still be running during destroy,
11167 // so check that the chart is really there before calling.
11168 if (tooltip) {
11169 tooltip.move(x, y, anchorX, anchorY);
11170 }
11171 }, 32);
11172
11173 }
11174 },
11175
11176 /**
11177 * Hide the tooltip
11178 */
11179 hide: function(delay) {
11180 var tooltip = this;
11181 clearTimeout(this.hideTimer); // disallow duplicate timers (#1728, #1766)
11182 delay = pick(delay, this.options.hideDelay, 500);
11183 if (!this.isHidden) {
11184 this.hideTimer = syncTimeout(function() {
11185 tooltip.getLabel()[delay ? 'fadeOut' : 'hide']();
11186 tooltip.isHidden = true;
11187 }, delay);
11188 }
11189 },
11190
11191 /**
11192 * Extendable method to get the anchor position of the tooltip
11193 * from a point or set of points
11194 */
11195 getAnchor: function(points, mouseEvent) {
11196 var ret,
11197 chart = this.chart,
11198 inverted = chart.inverted,
11199 plotTop = chart.plotTop,
11200 plotLeft = chart.plotLeft,
11201 plotX = 0,
11202 plotY = 0,
11203 yAxis,
11204 xAxis;
11205
11206 points = splat(points);
11207
11208 // Pie uses a special tooltipPos
11209 ret = points[0].tooltipPos;
11210
11211 // When tooltip follows mouse, relate the position to the mouse
11212 if (this.followPointer && mouseEvent) {
11213 if (mouseEvent.chartX === undefined) {
11214 mouseEvent = chart.pointer.normalize(mouseEvent);
11215 }
11216 ret = [
11217 mouseEvent.chartX - chart.plotLeft,
11218 mouseEvent.chartY - plotTop
11219 ];
11220 }
11221 // When shared, use the average position
11222 if (!ret) {
11223 each(points, function(point) {
11224 yAxis = point.series.yAxis;
11225 xAxis = point.series.xAxis;
11226 plotX += point.plotX + (!inverted && xAxis ? xAxis.left - plotLeft : 0);
11227 plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) +
11228 (!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151
11229 });
11230
11231 plotX /= points.length;
11232 plotY /= points.length;
11233
11234 ret = [
11235 inverted ? chart.plotWidth - plotY : plotX,
11236 this.shared && !inverted && points.length > 1 && mouseEvent ?
11237 mouseEvent.chartY - plotTop : // place shared tooltip next to the mouse (#424)
11238 inverted ? chart.plotHeight - plotX : plotY
11239 ];
11240 }
11241
11242 return map(ret, Math.round);
11243 },
11244
11245 /**
11246 * Place the tooltip in a chart without spilling over
11247 * and not covering the point it self.
11248 */
11249 getPosition: function(boxWidth, boxHeight, point) {
11250
11251 var chart = this.chart,
11252 distance = this.distance,
11253 ret = {},
11254 h = point.h || 0, // #4117
11255 swapped,
11256 first = ['y', chart.chartHeight, boxHeight,
11257 point.plotY + chart.plotTop, chart.plotTop,
11258 chart.plotTop + chart.plotHeight
11259 ],
11260 second = ['x', chart.chartWidth, boxWidth,
11261 point.plotX + chart.plotLeft, chart.plotLeft,
11262 chart.plotLeft + chart.plotWidth
11263 ],
11264 // The far side is right or bottom
11265 preferFarSide = !this.followPointer && pick(point.ttBelow, !chart.inverted === !!point.negative), // #4984
11266 /**
11267 * Handle the preferred dimension. When the preferred dimension is tooltip
11268 * on top or bottom of the point, it will look for space there.
11269 */
11270 firstDimension = function(dim, outerSize, innerSize, point, min, max) {
11271 var roomLeft = innerSize < point - distance,
11272 roomRight = point + distance + innerSize < outerSize,
11273 alignedLeft = point - distance - innerSize,
11274 alignedRight = point + distance;
11275
11276 if (preferFarSide && roomRight) {
11277 ret[dim] = alignedRight;
11278 } else if (!preferFarSide && roomLeft) {
11279 ret[dim] = alignedLeft;
11280 } else if (roomLeft) {
11281 ret[dim] = Math.min(max - innerSize, alignedLeft - h < 0 ? alignedLeft : alignedLeft - h);
11282 } else if (roomRight) {
11283 ret[dim] = Math.max(
11284 min,
11285 alignedRight + h + innerSize > outerSize ?
11286 alignedRight :
11287 alignedRight + h
11288 );
11289 } else {
11290 return false;
11291 }
11292 },
11293 /**
11294 * Handle the secondary dimension. If the preferred dimension is tooltip
11295 * on top or bottom of the point, the second dimension is to align the tooltip
11296 * above the point, trying to align center but allowing left or right
11297 * align within the chart box.
11298 */
11299 secondDimension = function(dim, outerSize, innerSize, point) {
11300 var retVal;
11301
11302 // Too close to the edge, return false and swap dimensions
11303 if (point < distance || point > outerSize - distance) {
11304 retVal = false;
11305 // Align left/top
11306 } else if (point < innerSize / 2) {
11307 ret[dim] = 1;
11308 // Align right/bottom
11309 } else if (point > outerSize - innerSize / 2) {
11310 ret[dim] = outerSize - innerSize - 2;
11311 // Align center
11312 } else {
11313 ret[dim] = point - innerSize / 2;
11314 }
11315 return retVal;
11316 },
11317 /**
11318 * Swap the dimensions
11319 */
11320 swap = function(count) {
11321 var temp = first;
11322 first = second;
11323 second = temp;
11324 swapped = count;
11325 },
11326 run = function() {
11327 if (firstDimension.apply(0, first) !== false) {
11328 if (secondDimension.apply(0, second) === false && !swapped) {
11329 swap(true);
11330 run();
11331 }
11332 } else if (!swapped) {
11333 swap(true);
11334 run();
11335 } else {
11336 ret.x = ret.y = 0;
11337 }
11338 };
11339
11340 // Under these conditions, prefer the tooltip on the side of the point
11341 if (chart.inverted || this.len > 1) {
11342 swap();
11343 }
11344 run();
11345
11346 return ret;
11347
11348 },
11349
11350 /**
11351 * In case no user defined formatter is given, this will be used. Note that the context
11352 * here is an object holding point, series, x, y etc.
11353 *
11354 * @returns {String|Array<String>}
11355 */
11356 defaultFormatter: function(tooltip) {
11357 var items = this.points || splat(this),
11358 s;
11359
11360 // Build the header
11361 s = [tooltip.tooltipFooterHeaderFormatter(items[0])];
11362
11363 // build the values
11364 s = s.concat(tooltip.bodyFormatter(items));
11365
11366 // footer
11367 s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true));
11368
11369 return s;
11370 },
11371
11372 /**
11373 * Refresh the tooltip's text and position.
11374 * @param {Object} point
11375 */
11376 refresh: function(point, mouseEvent) {
11377 var tooltip = this,
11378 chart = tooltip.chart,
11379 label,
11380 options = tooltip.options,
11381 x,
11382 y,
11383 anchor,
11384 textConfig = {},
11385 text,
11386 pointConfig = [],
11387 formatter = options.formatter || tooltip.defaultFormatter,
11388 hoverPoints = chart.hoverPoints,
11389 shared = tooltip.shared,
11390 currentSeries;
11391
11392 clearTimeout(this.hideTimer);
11393
11394 // get the reference point coordinates (pie charts use tooltipPos)
11395 tooltip.followPointer = splat(point)[0].series.tooltipOptions.followPointer;
11396 anchor = tooltip.getAnchor(point, mouseEvent);
11397 x = anchor[0];
11398 y = anchor[1];
11399
11400 // shared tooltip, array is sent over
11401 if (shared && !(point.series && point.series.noSharedTooltip)) {
11402
11403 // hide previous hoverPoints and set new
11404
11405 chart.hoverPoints = point;
11406 if (hoverPoints) {
11407 each(hoverPoints, function(point) {
11408 point.setState();
11409 });
11410 }
11411
11412 each(point, function(item) {
11413 item.setState('hover');
11414
11415 pointConfig.push(item.getLabelConfig());
11416 });
11417
11418 textConfig = {
11419 x: point[0].category,
11420 y: point[0].y
11421 };
11422 textConfig.points = pointConfig;
11423 this.len = pointConfig.length;
11424 point = point[0];
11425
11426 // single point tooltip
11427 } else {
11428 textConfig = point.getLabelConfig();
11429 }
11430 text = formatter.call(textConfig, tooltip);
11431
11432 // register the current series
11433 currentSeries = point.series;
11434 this.distance = pick(currentSeries.tooltipOptions.distance, 16);
11435
11436 // update the inner HTML
11437 if (text === false) {
11438 this.hide();
11439 } else {
11440
11441 label = tooltip.getLabel();
11442
11443 // show it
11444 if (tooltip.isHidden) {
11445 label.attr({
11446 opacity: 1
11447 }).show();
11448 }
11449
11450 // update text
11451 if (tooltip.split) {
11452 this.renderSplit(text, chart.hoverPoints);
11453 } else {
11454 label.attr({
11455 text: text && text.join ? text.join('') : text
11456 });
11457
11458 // Set the stroke color of the box to reflect the point
11459 label.removeClass(/highcharts-color-[\d]+/g)
11460 .addClass('highcharts-color-' + pick(point.colorIndex, currentSeries.colorIndex));
11461
11462
11463 label.attr({
11464 stroke: options.borderColor || point.color || currentSeries.color || '#666666'
11465 });
11466
11467
11468 tooltip.updatePosition({
11469 plotX: x,
11470 plotY: y,
11471 negative: point.negative,
11472 ttBelow: point.ttBelow,
11473 h: anchor[2] || 0
11474 });
11475 }
11476
11477 this.isHidden = false;
11478 }
11479 },
11480
11481 /**
11482 * Render the split tooltip. Loops over each point's text and adds
11483 * a label next to the point, then uses the distribute function to
11484 * find best non-overlapping positions.
11485 */
11486 renderSplit: function(labels, points) {
11487 var tooltip = this,
11488 boxes = [],
11489 chart = this.chart,
11490 ren = chart.renderer,
11491 rightAligned = true,
11492 options = this.options,
11493 headerHeight,
11494 tooltipLabel = this.getLabel();
11495
11496 // Create the individual labels
11497 each(labels.slice(0, labels.length - 1), function(str, i) {
11498 var point = points[i - 1] ||
11499 // Item 0 is the header. Instead of this, we could also use the crosshair label
11500 {
11501 isHeader: true,
11502 plotX: points[0].plotX
11503 },
11504 owner = point.series || tooltip,
11505 tt = owner.tt,
11506 series = point.series || {},
11507 colorClass = 'highcharts-color-' + pick(point.colorIndex, series.colorIndex, 'none'),
11508 target,
11509 x,
11510 bBox,
11511 boxWidth;
11512
11513 // Store the tooltip referance on the series
11514 if (!tt) {
11515 owner.tt = tt = ren.label(null, null, null, 'callout')
11516 .addClass('highcharts-tooltip-box ' + colorClass)
11517 .attr({
11518 'padding': options.padding,
11519 'r': options.borderRadius,
11520
11521 'fill': options.backgroundColor,
11522 'stroke': point.color || series.color || '#333333',
11523 'stroke-width': options.borderWidth
11524
11525 })
11526 .add(tooltipLabel);
11527 }
11528
11529 tt.isActive = true;
11530 tt.attr({
11531 text: str
11532 });
11533
11534 tt.css(options.style);
11535
11536
11537 // Get X position now, so we can move all to the other side in case of overflow
11538 bBox = tt.getBBox();
11539 boxWidth = bBox.width + tt.strokeWidth();
11540 if (point.isHeader) {
11541 headerHeight = bBox.height;
11542 x = Math.max(
11543 0, // No left overflow
11544 Math.min(
11545 point.plotX + chart.plotLeft - boxWidth / 2,
11546 chart.chartWidth - boxWidth // No right overflow (#5794)
11547 )
11548 );
11549 } else {
11550 x = point.plotX + chart.plotLeft - pick(options.distance, 16) -
11551 boxWidth;
11552 }
11553
11554
11555 // If overflow left, we don't use this x in the next loop
11556 if (x < 0) {
11557 rightAligned = false;
11558 }
11559
11560 // Prepare for distribution
11561 target = (point.series && point.series.yAxis && point.series.yAxis.pos) + (point.plotY || 0);
11562 target -= chart.plotTop;
11563 boxes.push({
11564 target: point.isHeader ? chart.plotHeight + headerHeight : target,
11565 rank: point.isHeader ? 1 : 0,
11566 size: owner.tt.getBBox().height + 1,
11567 point: point,
11568 x: x,
11569 tt: tt
11570 });
11571 });
11572
11573 // Clean previous run (for missing points)
11574 this.cleanSplit();
11575
11576 // Distribute and put in place
11577 H.distribute(boxes, chart.plotHeight + headerHeight);
11578 each(boxes, function(box) {
11579 var point = box.point;
11580
11581 // Put the label in place
11582 box.tt.attr({
11583 visibility: box.pos === undefined ? 'hidden' : 'inherit',
11584 x: (rightAligned || point.isHeader ?
11585 box.x :
11586 point.plotX + chart.plotLeft + pick(options.distance, 16)),
11587 y: box.pos + chart.plotTop,
11588 anchorX: point.plotX + chart.plotLeft,
11589 anchorY: point.isHeader ?
11590 box.pos + chart.plotTop - 15 : point.plotY + chart.plotTop
11591 });
11592 });
11593 },
11594
11595 /**
11596 * Find the new position and perform the move
11597 */
11598 updatePosition: function(point) {
11599 var chart = this.chart,
11600 label = this.getLabel(),
11601 pos = (this.options.positioner || this.getPosition).call(
11602 this,
11603 label.width,
11604 label.height,
11605 point
11606 );
11607
11608 // do the move
11609 this.move(
11610 Math.round(pos.x),
11611 Math.round(pos.y || 0), // can be undefined (#3977)
11612 point.plotX + chart.plotLeft,
11613 point.plotY + chart.plotTop
11614 );
11615 },
11616
11617 /**
11618 * Get the best X date format based on the closest point range on the axis.
11619 */
11620 getXDateFormat: function(point, options, xAxis) {
11621 var xDateFormat,
11622 dateTimeLabelFormats = options.dateTimeLabelFormats,
11623 closestPointRange = xAxis && xAxis.closestPointRange,
11624 n,
11625 blank = '01-01 00:00:00.000',
11626 strpos = {
11627 millisecond: 15,
11628 second: 12,
11629 minute: 9,
11630 hour: 6,
11631 day: 3
11632 },
11633 date,
11634 lastN = 'millisecond'; // for sub-millisecond data, #4223
11635
11636 if (closestPointRange) {
11637 date = dateFormat('%m-%d %H:%M:%S.%L', point.x);
11638 for (n in timeUnits) {
11639
11640 // If the range is exactly one week and we're looking at a Sunday/Monday, go for the week format
11641 if (closestPointRange === timeUnits.week && +dateFormat('%w', point.x) === xAxis.options.startOfWeek &&
11642 date.substr(6) === blank.substr(6)) {
11643 n = 'week';
11644 break;
11645 }
11646
11647 // The first format that is too great for the range
11648 if (timeUnits[n] > closestPointRange) {
11649 n = lastN;
11650 break;
11651 }
11652
11653 // If the point is placed every day at 23:59, we need to show
11654 // the minutes as well. #2637.
11655 if (strpos[n] && date.substr(strpos[n]) !== blank.substr(strpos[n])) {
11656 break;
11657 }
11658
11659 // Weeks are outside the hierarchy, only apply them on Mondays/Sundays like in the first condition
11660 if (n !== 'week') {
11661 lastN = n;
11662 }
11663 }
11664
11665 if (n) {
11666 xDateFormat = dateTimeLabelFormats[n];
11667 }
11668 } else {
11669 xDateFormat = dateTimeLabelFormats.day;
11670 }
11671
11672 return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581
11673 },
11674
11675 /**
11676 * Format the footer/header of the tooltip
11677 * #3397: abstraction to enable formatting of footer and header
11678 */
11679 tooltipFooterHeaderFormatter: function(labelConfig, isFooter) {
11680 var footOrHead = isFooter ? 'footer' : 'header',
11681 series = labelConfig.series,
11682 tooltipOptions = series.tooltipOptions,
11683 xDateFormat = tooltipOptions.xDateFormat,
11684 xAxis = series.xAxis,
11685 isDateTime = xAxis && xAxis.options.type === 'datetime' && isNumber(labelConfig.key),
11686 formatString = tooltipOptions[footOrHead + 'Format'];
11687
11688 // Guess the best date format based on the closest point distance (#568, #3418)
11689 if (isDateTime && !xDateFormat) {
11690 xDateFormat = this.getXDateFormat(labelConfig, tooltipOptions, xAxis);
11691 }
11692
11693 // Insert the footer date format if any
11694 if (isDateTime && xDateFormat) {
11695 formatString = formatString.replace('{point.key}', '{point.key:' + xDateFormat + '}');
11696 }
11697
11698 return format(formatString, {
11699 point: labelConfig,
11700 series: series
11701 });
11702 },
11703
11704 /**
11705 * Build the body (lines) of the tooltip by iterating over the items and returning one entry for each item,
11706 * abstracting this functionality allows to easily overwrite and extend it.
11707 */
11708 bodyFormatter: function(items) {
11709 return map(items, function(item) {
11710 var tooltipOptions = item.series.tooltipOptions;
11711 return (tooltipOptions.pointFormatter || item.point.tooltipFormatter)
11712 .call(item.point, tooltipOptions.pointFormat);
11713 });
11714 }
11715
11716 };
11717
11718 }(Highcharts));
11719 (function(H) {
11720 /**
11721 * (c) 2010-2016 Torstein Honsi
11722 *
11723 * License: www.highcharts.com/license
11724 */
11725 'use strict';
11726 var addEvent = H.addEvent,
11727 attr = H.attr,
11728 charts = H.charts,
11729 color = H.color,
11730 css = H.css,
11731 defined = H.defined,
11732 doc = H.doc,
11733 each = H.each,
11734 extend = H.extend,
11735 fireEvent = H.fireEvent,
11736 offset = H.offset,
11737 pick = H.pick,
11738 removeEvent = H.removeEvent,
11739 splat = H.splat,
11740 Tooltip = H.Tooltip,
11741 win = H.win;
11742
11743 /**
11744 * The mouse tracker object. All methods starting with "on" are primary DOM
11745 * event handlers. Subsequent methods should be named differently from what they
11746 * are doing.
11747 *
11748 * @constructor Pointer
11749 * @param {Object} chart The Chart instance
11750 * @param {Object} options The root options object
11751 */
11752 H.Pointer = function(chart, options) {
11753 this.init(chart, options);
11754 };
11755
11756 H.Pointer.prototype = {
11757 /**
11758 * Initialize Pointer
11759 */
11760 init: function(chart, options) {
11761
11762 // Store references
11763 this.options = options;
11764 this.chart = chart;
11765
11766 // Do we need to handle click on a touch device?
11767 this.runChartClick = options.chart.events && !!options.chart.events.click;
11768
11769 this.pinchDown = [];
11770 this.lastValidTouch = {};
11771
11772 if (Tooltip && options.tooltip.enabled) {
11773 chart.tooltip = new Tooltip(chart, options.tooltip);
11774 this.followTouchMove = pick(options.tooltip.followTouchMove, true);
11775 }
11776
11777 this.setDOMEvents();
11778 },
11779
11780 /**
11781 * Resolve the zoomType option, this is reset on all touch start and mouse
11782 * down events.
11783 */
11784 zoomOption: function(e) {
11785 var chart = this.chart,
11786 options = chart.options.chart,
11787 zoomType = options.zoomType || '',
11788 inverted = chart.inverted,
11789 zoomX,
11790 zoomY;
11791
11792 // Look for the pinchType option
11793 if (/touch/.test(e.type)) {
11794 zoomType = pick(options.pinchType, zoomType);
11795 }
11796
11797 this.zoomX = zoomX = /x/.test(zoomType);
11798 this.zoomY = zoomY = /y/.test(zoomType);
11799 this.zoomHor = (zoomX && !inverted) || (zoomY && inverted);
11800 this.zoomVert = (zoomY && !inverted) || (zoomX && inverted);
11801 this.hasZoom = zoomX || zoomY;
11802 },
11803
11804 /**
11805 * Add crossbrowser support for chartX and chartY
11806 * @param {Object} e The event object in standard browsers
11807 */
11808 normalize: function(e, chartPosition) {
11809 var chartX,
11810 chartY,
11811 ePos;
11812
11813 // IE normalizing
11814 e = e || win.event;
11815 if (!e.target) {
11816 e.target = e.srcElement;
11817 }
11818
11819 // iOS (#2757)
11820 ePos = e.touches ? (e.touches.length ? e.touches.item(0) : e.changedTouches[0]) : e;
11821
11822 // Get mouse position
11823 if (!chartPosition) {
11824 this.chartPosition = chartPosition = offset(this.chart.container);
11825 }
11826
11827 // chartX and chartY
11828 if (ePos.pageX === undefined) { // IE < 9. #886.
11829 chartX = Math.max(e.x, e.clientX - chartPosition.left); // #2005, #2129: the second case is
11830 // for IE10 quirks mode within framesets
11831 chartY = e.y;
11832 } else {
11833 chartX = ePos.pageX - chartPosition.left;
11834 chartY = ePos.pageY - chartPosition.top;
11835 }
11836
11837 return extend(e, {
11838 chartX: Math.round(chartX),
11839 chartY: Math.round(chartY)
11840 });
11841 },
11842
11843 /**
11844 * Get the click position in terms of axis values.
11845 *
11846 * @param {Object} e A pointer event
11847 */
11848 getCoordinates: function(e) {
11849 var coordinates = {
11850 xAxis: [],
11851 yAxis: []
11852 };
11853
11854 each(this.chart.axes, function(axis) {
11855 coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({
11856 axis: axis,
11857 value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY'])
11858 });
11859 });
11860 return coordinates;
11861 },
11862
11863 /**
11864 * With line type charts with a single tracker, get the point closest to the mouse.
11865 * Run Point.onMouseOver and display tooltip for the point or points.
11866 */
11867 runPointActions: function(e) {
11868
11869 var pointer = this,
11870 chart = pointer.chart,
11871 series = chart.series,
11872 tooltip = chart.tooltip,
11873 shared = tooltip ? tooltip.shared : false,
11874 followPointer,
11875 updatePosition = true,
11876 hoverPoint = chart.hoverPoint,
11877 hoverSeries = chart.hoverSeries,
11878 i,
11879 anchor,
11880 noSharedTooltip,
11881 stickToHoverSeries,
11882 directTouch,
11883 kdpoints = [],
11884 kdpointT;
11885
11886 // For hovering over the empty parts of the plot area (hoverSeries is undefined).
11887 // If there is one series with point tracking (combo chart), don't go to nearest neighbour.
11888 if (!shared && !hoverSeries) {
11889 for (i = 0; i < series.length; i++) {
11890 if (series[i].directTouch || !series[i].options.stickyTracking) {
11891 series = [];
11892 }
11893 }
11894 }
11895
11896 // If it has a hoverPoint and that series requires direct touch (like columns, #3899), or we're on
11897 // a noSharedTooltip series among shared tooltip series (#4546), use the hoverPoint . Otherwise,
11898 // search the k-d tree.
11899 stickToHoverSeries = hoverSeries && (shared ? hoverSeries.noSharedTooltip : hoverSeries.directTouch);
11900 if (stickToHoverSeries && hoverPoint) {
11901 kdpoints = [hoverPoint];
11902
11903 // Handle shared tooltip or cases where a series is not yet hovered
11904 } else {
11905 // When we have non-shared tooltip and sticky tracking is disabled,
11906 // search for the closest point only on hovered series: #5533, #5476
11907 if (!shared && hoverSeries && !hoverSeries.options.stickyTracking) {
11908 series = [hoverSeries];
11909 }
11910 // Find nearest points on all series
11911 each(series, function(s) {
11912 // Skip hidden series
11913 noSharedTooltip = s.noSharedTooltip && shared;
11914 directTouch = !shared && s.directTouch;
11915 if (s.visible && !noSharedTooltip && !directTouch && pick(s.options.enableMouseTracking, true)) { // #3821
11916 kdpointT = s.searchPoint(e, !noSharedTooltip && s.kdDimensions === 1); // #3828
11917 if (kdpointT && kdpointT.series) { // Point.series becomes null when reset and before redraw (#5197)
11918 kdpoints.push(kdpointT);
11919 }
11920 }
11921 });
11922
11923 // Sort kdpoints by distance to mouse pointer
11924 kdpoints.sort(function(p1, p2) {
11925 var isCloserX = p1.distX - p2.distX,
11926 isCloser = p1.dist - p2.dist,
11927 isAbove = p2.series.group.zIndex - p1.series.group.zIndex;
11928
11929 // We have two points which are not in the same place on xAxis and shared tooltip:
11930 if (isCloserX !== 0 && shared) { // #5721
11931 return isCloserX;
11932 }
11933 // Points are not exactly in the same place on x/yAxis:
11934 if (isCloser !== 0) {
11935 return isCloser;
11936 }
11937 // The same xAxis and yAxis position, sort by z-index:
11938 if (isAbove !== 0) {
11939 return isAbove;
11940 }
11941
11942 // The same zIndex, sort by array index:
11943 return p1.series.index > p2.series.index ? -1 : 1;
11944 });
11945 }
11946
11947 // Remove points with different x-positions, required for shared tooltip and crosshairs (#4645):
11948 if (shared) {
11949 i = kdpoints.length;
11950 while (i--) {
11951 if (kdpoints[i].x !== kdpoints[0].x || kdpoints[i].series.noSharedTooltip) {
11952 kdpoints.splice(i, 1);
11953 }
11954 }
11955 }
11956
11957 // Refresh tooltip for kdpoint if new hover point or tooltip was hidden // #3926, #4200
11958 if (kdpoints[0] && (kdpoints[0] !== this.prevKDPoint || (tooltip && tooltip.isHidden))) {
11959 // Draw tooltip if necessary
11960 if (shared && !kdpoints[0].series.noSharedTooltip) {
11961 // Do mouseover on all points (#3919, #3985, #4410, #5622)
11962 for (i = 0; i < kdpoints.length; i++) {
11963 kdpoints[i].onMouseOver(e, kdpoints[i] !== ((hoverSeries && hoverSeries.directTouch && hoverPoint) || kdpoints[0]));
11964 }
11965
11966 if (kdpoints.length && tooltip) {
11967 // Keep the order of series in tooltip:
11968 tooltip.refresh(kdpoints.sort(function(p1, p2) {
11969 return p1.series.index - p2.series.index;
11970 }), e);
11971 }
11972 } else {
11973 if (tooltip) {
11974 tooltip.refresh(kdpoints[0], e);
11975 }
11976 if (!hoverSeries || !hoverSeries.directTouch) { // #4448
11977 kdpoints[0].onMouseOver(e);
11978 }
11979 }
11980 this.prevKDPoint = kdpoints[0];
11981 updatePosition = false;
11982 }
11983 // Update positions (regardless of kdpoint or hoverPoint)
11984 if (updatePosition) {
11985 followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer;
11986 if (tooltip && followPointer && !tooltip.isHidden) {
11987 anchor = tooltip.getAnchor([{}], e);
11988 tooltip.updatePosition({
11989 plotX: anchor[0],
11990 plotY: anchor[1]
11991 });
11992 }
11993 }
11994
11995 // Start the event listener to pick up the tooltip and crosshairs
11996 if (!pointer.unDocMouseMove) {
11997 pointer.unDocMouseMove = addEvent(doc, 'mousemove', function(e) {
11998 if (charts[H.hoverChartIndex]) {
11999 charts[H.hoverChartIndex].pointer.onDocumentMouseMove(e);
12000 }
12001 });
12002 }
12003
12004 // Crosshair. For each hover point, loop over axes and draw cross if that point
12005 // belongs to the axis (#4927).
12006 each(shared ? kdpoints : [pick(hoverPoint, kdpoints[0])], function drawPointCrosshair(point) { // #5269
12007 each(chart.axes, function drawAxisCrosshair(axis) {
12008 // In case of snap = false, point is undefined, and we draw the crosshair anyway (#5066)
12009 if (!point || point.series && point.series[axis.coll] === axis) { // #5658
12010 axis.drawCrosshair(e, point);
12011 }
12012 });
12013 });
12014 },
12015
12016 /**
12017 * Reset the tracking by hiding the tooltip, the hover series state and the hover point
12018 *
12019 * @param allowMove {Boolean} Instead of destroying the tooltip altogether, allow moving it if possible
12020 */
12021 reset: function(allowMove, delay) {
12022 var pointer = this,
12023 chart = pointer.chart,
12024 hoverSeries = chart.hoverSeries,
12025 hoverPoint = chart.hoverPoint,
12026 hoverPoints = chart.hoverPoints,
12027 tooltip = chart.tooltip,
12028 tooltipPoints = tooltip && tooltip.shared ? hoverPoints : hoverPoint;
12029
12030 // Check if the points have moved outside the plot area (#1003, #4736, #5101)
12031 if (allowMove && tooltipPoints) {
12032 each(splat(tooltipPoints), function(point) {
12033 if (point.series.isCartesian && point.plotX === undefined) {
12034 allowMove = false;
12035 }
12036 });
12037 }
12038
12039 // Just move the tooltip, #349
12040 if (allowMove) {
12041 if (tooltip && tooltipPoints) {
12042 tooltip.refresh(tooltipPoints);
12043 if (hoverPoint) { // #2500
12044 hoverPoint.setState(hoverPoint.state, true);
12045 each(chart.axes, function(axis) {
12046 if (axis.crosshair) {
12047 axis.drawCrosshair(null, hoverPoint);
12048 }
12049 });
12050 }
12051 }
12052
12053 // Full reset
12054 } else {
12055
12056 if (hoverPoint) {
12057 hoverPoint.onMouseOut();
12058 }
12059
12060 if (hoverPoints) {
12061 each(hoverPoints, function(point) {
12062 point.setState();
12063 });
12064 }
12065
12066 if (hoverSeries) {
12067 hoverSeries.onMouseOut();
12068 }
12069
12070 if (tooltip) {
12071 tooltip.hide(delay);
12072 }
12073
12074 if (pointer.unDocMouseMove) {
12075 pointer.unDocMouseMove = pointer.unDocMouseMove();
12076 }
12077
12078 // Remove crosshairs
12079 each(chart.axes, function(axis) {
12080 axis.hideCrosshair();
12081 });
12082
12083 pointer.hoverX = pointer.prevKDPoint = chart.hoverPoints = chart.hoverPoint = null;
12084 }
12085 },
12086
12087 /**
12088 * Scale series groups to a certain scale and translation
12089 */
12090 scaleGroups: function(attribs, clip) {
12091
12092 var chart = this.chart,
12093 seriesAttribs;
12094
12095 // Scale each series
12096 each(chart.series, function(series) {
12097 seriesAttribs = attribs || series.getPlotBox(); // #1701
12098 if (series.xAxis && series.xAxis.zoomEnabled && series.group) {
12099 series.group.attr(seriesAttribs);
12100 if (series.markerGroup) {
12101 series.markerGroup.attr(seriesAttribs);
12102 series.markerGroup.clip(clip ? chart.clipRect : null);
12103 }
12104 if (series.dataLabelsGroup) {
12105 series.dataLabelsGroup.attr(seriesAttribs);
12106 }
12107 }
12108 });
12109
12110 // Clip
12111 chart.clipRect.attr(clip || chart.clipBox);
12112 },
12113
12114 /**
12115 * Start a drag operation
12116 */
12117 dragStart: function(e) {
12118 var chart = this.chart;
12119
12120 // Record the start position
12121 chart.mouseIsDown = e.type;
12122 chart.cancelClick = false;
12123 chart.mouseDownX = this.mouseDownX = e.chartX;
12124 chart.mouseDownY = this.mouseDownY = e.chartY;
12125 },
12126
12127 /**
12128 * Perform a drag operation in response to a mousemove event while the mouse is down
12129 */
12130 drag: function(e) {
12131
12132 var chart = this.chart,
12133 chartOptions = chart.options.chart,
12134 chartX = e.chartX,
12135 chartY = e.chartY,
12136 zoomHor = this.zoomHor,
12137 zoomVert = this.zoomVert,
12138 plotLeft = chart.plotLeft,
12139 plotTop = chart.plotTop,
12140 plotWidth = chart.plotWidth,
12141 plotHeight = chart.plotHeight,
12142 clickedInside,
12143 size,
12144 selectionMarker = this.selectionMarker,
12145 mouseDownX = this.mouseDownX,
12146 mouseDownY = this.mouseDownY,
12147 panKey = chartOptions.panKey && e[chartOptions.panKey + 'Key'];
12148
12149 // If the device supports both touch and mouse (like IE11), and we are touch-dragging
12150 // inside the plot area, don't handle the mouse event. #4339.
12151 if (selectionMarker && selectionMarker.touch) {
12152 return;
12153 }
12154
12155 // If the mouse is outside the plot area, adjust to cooordinates
12156 // inside to prevent the selection marker from going outside
12157 if (chartX < plotLeft) {
12158 chartX = plotLeft;
12159 } else if (chartX > plotLeft + plotWidth) {
12160 chartX = plotLeft + plotWidth;
12161 }
12162
12163 if (chartY < plotTop) {
12164 chartY = plotTop;
12165 } else if (chartY > plotTop + plotHeight) {
12166 chartY = plotTop + plotHeight;
12167 }
12168
12169 // determine if the mouse has moved more than 10px
12170 this.hasDragged = Math.sqrt(
12171 Math.pow(mouseDownX - chartX, 2) +
12172 Math.pow(mouseDownY - chartY, 2)
12173 );
12174
12175 if (this.hasDragged > 10) {
12176 clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop);
12177
12178 // make a selection
12179 if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside && !panKey) {
12180 if (!selectionMarker) {
12181 this.selectionMarker = selectionMarker = chart.renderer.rect(
12182 plotLeft,
12183 plotTop,
12184 zoomHor ? 1 : plotWidth,
12185 zoomVert ? 1 : plotHeight,
12186 0
12187 )
12188 .attr({
12189
12190 fill: chartOptions.selectionMarkerFill || color('#335cad').setOpacity(0.25).get(),
12191
12192 'class': 'highcharts-selection-marker',
12193 'zIndex': 7
12194 })
12195 .add();
12196 }
12197 }
12198
12199 // adjust the width of the selection marker
12200 if (selectionMarker && zoomHor) {
12201 size = chartX - mouseDownX;
12202 selectionMarker.attr({
12203 width: Math.abs(size),
12204 x: (size > 0 ? 0 : size) + mouseDownX
12205 });
12206 }
12207 // adjust the height of the selection marker
12208 if (selectionMarker && zoomVert) {
12209 size = chartY - mouseDownY;
12210 selectionMarker.attr({
12211 height: Math.abs(size),
12212 y: (size > 0 ? 0 : size) + mouseDownY
12213 });
12214 }
12215
12216 // panning
12217 if (clickedInside && !selectionMarker && chartOptions.panning) {
12218 chart.pan(e, chartOptions.panning);
12219 }
12220 }
12221 },
12222
12223 /**
12224 * On mouse up or touch end across the entire document, drop the selection.
12225 */
12226 drop: function(e) {
12227 var pointer = this,
12228 chart = this.chart,
12229 hasPinched = this.hasPinched;
12230
12231 if (this.selectionMarker) {
12232 var selectionData = {
12233 originalEvent: e, // #4890
12234 xAxis: [],
12235 yAxis: []
12236 },
12237 selectionBox = this.selectionMarker,
12238 selectionLeft = selectionBox.attr ? selectionBox.attr('x') : selectionBox.x,
12239 selectionTop = selectionBox.attr ? selectionBox.attr('y') : selectionBox.y,
12240 selectionWidth = selectionBox.attr ? selectionBox.attr('width') : selectionBox.width,
12241 selectionHeight = selectionBox.attr ? selectionBox.attr('height') : selectionBox.height,
12242 runZoom;
12243
12244 // a selection has been made
12245 if (this.hasDragged || hasPinched) {
12246
12247 // record each axis' min and max
12248 each(chart.axes, function(axis) {
12249 if (axis.zoomEnabled && defined(axis.min) && (hasPinched || pointer[{
12250 xAxis: 'zoomX',
12251 yAxis: 'zoomY'
12252 }[axis.coll]])) { // #859, #3569
12253 var horiz = axis.horiz,
12254 minPixelPadding = e.type === 'touchend' ? axis.minPixelPadding : 0, // #1207, #3075
12255 selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop) + minPixelPadding),
12256 selectionMax = axis.toValue((horiz ? selectionLeft + selectionWidth : selectionTop + selectionHeight) - minPixelPadding);
12257
12258 selectionData[axis.coll].push({
12259 axis: axis,
12260 min: Math.min(selectionMin, selectionMax), // for reversed axes
12261 max: Math.max(selectionMin, selectionMax)
12262 });
12263 runZoom = true;
12264 }
12265 });
12266 if (runZoom) {
12267 fireEvent(chart, 'selection', selectionData, function(args) {
12268 chart.zoom(extend(args, hasPinched ? {
12269 animation: false
12270 } : null));
12271 });
12272 }
12273
12274 }
12275 this.selectionMarker = this.selectionMarker.destroy();
12276
12277 // Reset scaling preview
12278 if (hasPinched) {
12279 this.scaleGroups();
12280 }
12281 }
12282
12283 // Reset all
12284 if (chart) { // it may be destroyed on mouse up - #877
12285 css(chart.container, {
12286 cursor: chart._cursor
12287 });
12288 chart.cancelClick = this.hasDragged > 10; // #370
12289 chart.mouseIsDown = this.hasDragged = this.hasPinched = false;
12290 this.pinchDown = [];
12291 }
12292 },
12293
12294 onContainerMouseDown: function(e) {
12295
12296 e = this.normalize(e);
12297
12298 this.zoomOption(e);
12299
12300 // issue #295, dragging not always working in Firefox
12301 if (e.preventDefault) {
12302 e.preventDefault();
12303 }
12304
12305 this.dragStart(e);
12306 },
12307
12308
12309
12310 onDocumentMouseUp: function(e) {
12311 if (charts[H.hoverChartIndex]) {
12312 charts[H.hoverChartIndex].pointer.drop(e);
12313 }
12314 },
12315
12316 /**
12317 * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea.
12318 * Issue #149 workaround. The mouseleave event does not always fire.
12319 */
12320 onDocumentMouseMove: function(e) {
12321 var chart = this.chart,
12322 chartPosition = this.chartPosition;
12323
12324 e = this.normalize(e, chartPosition);
12325
12326 // If we're outside, hide the tooltip
12327 if (chartPosition && !this.inClass(e.target, 'highcharts-tracker') &&
12328 !chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
12329 this.reset();
12330 }
12331 },
12332
12333 /**
12334 * When mouse leaves the container, hide the tooltip.
12335 */
12336 onContainerMouseLeave: function(e) {
12337 var chart = charts[H.hoverChartIndex];
12338 if (chart && (e.relatedTarget || e.toElement)) { // #4886, MS Touch end fires mouseleave but with no related target
12339 chart.pointer.reset();
12340 chart.pointer.chartPosition = null; // also reset the chart position, used in #149 fix
12341 }
12342 },
12343
12344 // The mousemove, touchmove and touchstart event handler
12345 onContainerMouseMove: function(e) {
12346
12347 var chart = this.chart;
12348
12349 if (!defined(H.hoverChartIndex) || !charts[H.hoverChartIndex] || !charts[H.hoverChartIndex].mouseIsDown) {
12350 H.hoverChartIndex = chart.index;
12351 }
12352
12353 e = this.normalize(e);
12354 e.returnValue = false; // #2251, #3224
12355
12356 if (chart.mouseIsDown === 'mousedown') {
12357 this.drag(e);
12358 }
12359
12360 // Show the tooltip and run mouse over events (#977)
12361 if ((this.inClass(e.target, 'highcharts-tracker') ||
12362 chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) && !chart.openMenu) {
12363 this.runPointActions(e);
12364 }
12365 },
12366
12367 /**
12368 * Utility to detect whether an element has, or has a parent with, a specific
12369 * class name. Used on detection of tracker objects and on deciding whether
12370 * hovering the tooltip should cause the active series to mouse out.
12371 */
12372 inClass: function(element, className) {
12373 var elemClassName;
12374 while (element) {
12375 elemClassName = attr(element, 'class');
12376 if (elemClassName) {
12377 if (elemClassName.indexOf(className) !== -1) {
12378 return true;
12379 }
12380 if (elemClassName.indexOf('highcharts-container') !== -1) {
12381 return false;
12382 }
12383 }
12384 element = element.parentNode;
12385 }
12386 },
12387
12388 onTrackerMouseOut: function(e) {
12389 var series = this.chart.hoverSeries,
12390 relatedTarget = e.relatedTarget || e.toElement;
12391
12392 if (series && relatedTarget && !series.options.stickyTracking &&
12393 !this.inClass(relatedTarget, 'highcharts-tooltip') &&
12394 (!this.inClass(relatedTarget, 'highcharts-series-' + series.index) || // #2499, #4465
12395 !this.inClass(relatedTarget, 'highcharts-tracker') // #5553
12396 )
12397 ) {
12398 series.onMouseOut();
12399 }
12400 },
12401
12402 onContainerClick: function(e) {
12403 var chart = this.chart,
12404 hoverPoint = chart.hoverPoint,
12405 plotLeft = chart.plotLeft,
12406 plotTop = chart.plotTop;
12407
12408 e = this.normalize(e);
12409
12410 if (!chart.cancelClick) {
12411
12412 // On tracker click, fire the series and point events. #783, #1583
12413 if (hoverPoint && this.inClass(e.target, 'highcharts-tracker')) {
12414
12415 // the series click event
12416 fireEvent(hoverPoint.series, 'click', extend(e, {
12417 point: hoverPoint
12418 }));
12419
12420 // the point click event
12421 if (chart.hoverPoint) { // it may be destroyed (#1844)
12422 hoverPoint.firePointEvent('click', e);
12423 }
12424
12425 // When clicking outside a tracker, fire a chart event
12426 } else {
12427 extend(e, this.getCoordinates(e));
12428
12429 // fire a click event in the chart
12430 if (chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) {
12431 fireEvent(chart, 'click', e);
12432 }
12433 }
12434
12435
12436 }
12437 },
12438
12439 /**
12440 * Set the JS DOM events on the container and document. This method should contain
12441 * a one-to-one assignment between methods and their handlers. Any advanced logic should
12442 * be moved to the handler reflecting the event's name.
12443 */
12444 setDOMEvents: function() {
12445
12446 var pointer = this,
12447 container = pointer.chart.container;
12448
12449 container.onmousedown = function(e) {
12450 pointer.onContainerMouseDown(e);
12451 };
12452 container.onmousemove = function(e) {
12453 pointer.onContainerMouseMove(e);
12454 };
12455 container.onclick = function(e) {
12456 pointer.onContainerClick(e);
12457 };
12458 addEvent(container, 'mouseleave', pointer.onContainerMouseLeave);
12459 if (H.chartCount === 1) {
12460 addEvent(doc, 'mouseup', pointer.onDocumentMouseUp);
12461 }
12462 if (H.hasTouch) {
12463 container.ontouchstart = function(e) {
12464 pointer.onContainerTouchStart(e);
12465 };
12466 container.ontouchmove = function(e) {
12467 pointer.onContainerTouchMove(e);
12468 };
12469 if (H.chartCount === 1) {
12470 addEvent(doc, 'touchend', pointer.onDocumentTouchEnd);
12471 }
12472 }
12473
12474 },
12475
12476 /**
12477 * Destroys the Pointer object and disconnects DOM events.
12478 */
12479 destroy: function() {
12480 var prop;
12481
12482 removeEvent(this.chart.container, 'mouseleave', this.onContainerMouseLeave);
12483 if (!H.chartCount) {
12484 removeEvent(doc, 'mouseup', this.onDocumentMouseUp);
12485 removeEvent(doc, 'touchend', this.onDocumentTouchEnd);
12486 }
12487
12488 // memory and CPU leak
12489 clearInterval(this.tooltipTimeout);
12490
12491 for (prop in this) {
12492 this[prop] = null;
12493 }
12494 }
12495 };
12496
12497 }(Highcharts));
12498 (function(H) {
12499 /**
12500 * (c) 2010-2016 Torstein Honsi
12501 *
12502 * License: www.highcharts.com/license
12503 */
12504 'use strict';
12505 var charts = H.charts,
12506 each = H.each,
12507 extend = H.extend,
12508 map = H.map,
12509 noop = H.noop,
12510 pick = H.pick,
12511 Pointer = H.Pointer;
12512
12513 /* Support for touch devices */
12514 extend(Pointer.prototype, /** @lends Pointer.prototype */ {
12515
12516 /**
12517 * Run translation operations
12518 */
12519 pinchTranslate: function(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) {
12520 if (this.zoomHor) {
12521 this.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
12522 }
12523 if (this.zoomVert) {
12524 this.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
12525 }
12526 },
12527
12528 /**
12529 * Run translation operations for each direction (horizontal and vertical) independently
12530 */
12531 pinchTranslateDirection: function(horiz, pinchDown, touches, transform,
12532 selectionMarker, clip, lastValidTouch, forcedScale) {
12533 var chart = this.chart,
12534 xy = horiz ? 'x' : 'y',
12535 XY = horiz ? 'X' : 'Y',
12536 sChartXY = 'chart' + XY,
12537 wh = horiz ? 'width' : 'height',
12538 plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')],
12539 selectionWH,
12540 selectionXY,
12541 clipXY,
12542 scale = forcedScale || 1,
12543 inverted = chart.inverted,
12544 bounds = chart.bounds[horiz ? 'h' : 'v'],
12545 singleTouch = pinchDown.length === 1,
12546 touch0Start = pinchDown[0][sChartXY],
12547 touch0Now = touches[0][sChartXY],
12548 touch1Start = !singleTouch && pinchDown[1][sChartXY],
12549 touch1Now = !singleTouch && touches[1][sChartXY],
12550 outOfBounds,
12551 transformScale,
12552 scaleKey,
12553 setScale = function() {
12554 // Don't zoom if fingers are too close on this axis
12555 if (!singleTouch && Math.abs(touch0Start - touch1Start) > 20) {
12556 scale = forcedScale || Math.abs(touch0Now - touch1Now) / Math.abs(touch0Start - touch1Start);
12557 }
12558
12559 clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start;
12560 selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale;
12561 };
12562
12563 // Set the scale, first pass
12564 setScale();
12565
12566 selectionXY = clipXY; // the clip position (x or y) is altered if out of bounds, the selection position is not
12567
12568 // Out of bounds
12569 if (selectionXY < bounds.min) {
12570 selectionXY = bounds.min;
12571 outOfBounds = true;
12572 } else if (selectionXY + selectionWH > bounds.max) {
12573 selectionXY = bounds.max - selectionWH;
12574 outOfBounds = true;
12575 }
12576
12577 // Is the chart dragged off its bounds, determined by dataMin and dataMax?
12578 if (outOfBounds) {
12579
12580 // Modify the touchNow position in order to create an elastic drag movement. This indicates
12581 // to the user that the chart is responsive but can't be dragged further.
12582 touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]);
12583 if (!singleTouch) {
12584 touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]);
12585 }
12586
12587 // Set the scale, second pass to adapt to the modified touchNow positions
12588 setScale();
12589
12590 } else {
12591 lastValidTouch[xy] = [touch0Now, touch1Now];
12592 }
12593
12594 // Set geometry for clipping, selection and transformation
12595 if (!inverted) {
12596 clip[xy] = clipXY - plotLeftTop;
12597 clip[wh] = selectionWH;
12598 }
12599 scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY;
12600 transformScale = inverted ? 1 / scale : scale;
12601
12602 selectionMarker[wh] = selectionWH;
12603 selectionMarker[xy] = selectionXY;
12604 transform[scaleKey] = scale;
12605 transform['translate' + XY] = (transformScale * plotLeftTop) + (touch0Now - (transformScale * touch0Start));
12606 },
12607
12608 /**
12609 * Handle touch events with two touches
12610 */
12611 pinch: function(e) {
12612
12613 var self = this,
12614 chart = self.chart,
12615 pinchDown = self.pinchDown,
12616 touches = e.touches,
12617 touchesLength = touches.length,
12618 lastValidTouch = self.lastValidTouch,
12619 hasZoom = self.hasZoom,
12620 selectionMarker = self.selectionMarker,
12621 transform = {},
12622 fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, 'highcharts-tracker') &&
12623 chart.runTrackerClick) || self.runChartClick),
12624 clip = {};
12625
12626 // Don't initiate panning until the user has pinched. This prevents us from
12627 // blocking page scrolling as users scroll down a long page (#4210).
12628 if (touchesLength > 1) {
12629 self.initiated = true;
12630 }
12631
12632 // On touch devices, only proceed to trigger click if a handler is defined
12633 if (hasZoom && self.initiated && !fireClickEvent) {
12634 e.preventDefault();
12635 }
12636
12637 // Normalize each touch
12638 map(touches, function(e) {
12639 return self.normalize(e);
12640 });
12641
12642 // Register the touch start position
12643 if (e.type === 'touchstart') {
12644 each(touches, function(e, i) {
12645 pinchDown[i] = {
12646 chartX: e.chartX,
12647 chartY: e.chartY
12648 };
12649 });
12650 lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && pinchDown[1].chartX];
12651 lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && pinchDown[1].chartY];
12652
12653 // Identify the data bounds in pixels
12654 each(chart.axes, function(axis) {
12655 if (axis.zoomEnabled) {
12656 var bounds = chart.bounds[axis.horiz ? 'h' : 'v'],
12657 minPixelPadding = axis.minPixelPadding,
12658 min = axis.toPixels(pick(axis.options.min, axis.dataMin)),
12659 max = axis.toPixels(pick(axis.options.max, axis.dataMax)),
12660 absMin = Math.min(min, max),
12661 absMax = Math.max(min, max);
12662
12663 // Store the bounds for use in the touchmove handler
12664 bounds.min = Math.min(axis.pos, absMin - minPixelPadding);
12665 bounds.max = Math.max(axis.pos + axis.len, absMax + minPixelPadding);
12666 }
12667 });
12668 self.res = true; // reset on next move
12669
12670 // Optionally move the tooltip on touchmove
12671 } else if (self.followTouchMove && touchesLength === 1) {
12672 this.runPointActions(self.normalize(e));
12673
12674 // Event type is touchmove, handle panning and pinching
12675 } else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first
12676
12677
12678 // Set the marker
12679 if (!selectionMarker) {
12680 self.selectionMarker = selectionMarker = extend({
12681 destroy: noop,
12682 touch: true
12683 }, chart.plotBox);
12684 }
12685
12686 self.pinchTranslate(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
12687
12688 self.hasPinched = hasZoom;
12689
12690 // Scale and translate the groups to provide visual feedback during pinching
12691 self.scaleGroups(transform, clip);
12692
12693 if (self.res) {
12694 self.res = false;
12695 this.reset(false, 0);
12696 }
12697 }
12698 },
12699
12700 /**
12701 * General touch handler shared by touchstart and touchmove.
12702 */
12703 touch: function(e, start) {
12704 var chart = this.chart,
12705 hasMoved,
12706 pinchDown,
12707 isInside;
12708
12709 if (chart.index !== H.hoverChartIndex) {
12710 this.onContainerMouseLeave({
12711 relatedTarget: true
12712 });
12713 }
12714 H.hoverChartIndex = chart.index;
12715
12716 if (e.touches.length === 1) {
12717
12718 e = this.normalize(e);
12719
12720 isInside = chart.isInsidePlot(
12721 e.chartX - chart.plotLeft,
12722 e.chartY - chart.plotTop
12723 );
12724 if (isInside && !chart.openMenu) {
12725
12726 // Run mouse events and display tooltip etc
12727 if (start) {
12728 this.runPointActions(e);
12729 }
12730
12731 // Android fires touchmove events after the touchstart even if the
12732 // finger hasn't moved, or moved only a pixel or two. In iOS however,
12733 // the touchmove doesn't fire unless the finger moves more than ~4px.
12734 // So we emulate this behaviour in Android by checking how much it
12735 // moved, and cancelling on small distances. #3450.
12736 if (e.type === 'touchmove') {
12737 pinchDown = this.pinchDown;
12738 hasMoved = pinchDown[0] ? Math.sqrt( // #5266
12739 Math.pow(pinchDown[0].chartX - e.chartX, 2) +
12740 Math.pow(pinchDown[0].chartY - e.chartY, 2)
12741 ) >= 4 : false;
12742 }
12743
12744 if (pick(hasMoved, true)) {
12745 this.pinch(e);
12746 }
12747
12748 } else if (start) {
12749 // Hide the tooltip on touching outside the plot area (#1203)
12750 this.reset();
12751 }
12752
12753 } else if (e.touches.length === 2) {
12754 this.pinch(e);
12755 }
12756 },
12757
12758 onContainerTouchStart: function(e) {
12759 this.zoomOption(e);
12760 this.touch(e, true);
12761 },
12762
12763 onContainerTouchMove: function(e) {
12764 this.touch(e);
12765 },
12766
12767 onDocumentTouchEnd: function(e) {
12768 if (charts[H.hoverChartIndex]) {
12769 charts[H.hoverChartIndex].pointer.drop(e);
12770 }
12771 }
12772
12773 });
12774
12775 }(Highcharts));
12776 (function(H) {
12777 /**
12778 * (c) 2010-2016 Torstein Honsi
12779 *
12780 * License: www.highcharts.com/license
12781 */
12782 'use strict';
12783 var addEvent = H.addEvent,
12784 charts = H.charts,
12785 css = H.css,
12786 doc = H.doc,
12787 extend = H.extend,
12788 noop = H.noop,
12789 Pointer = H.Pointer,
12790 removeEvent = H.removeEvent,
12791 win = H.win,
12792 wrap = H.wrap;
12793
12794 if (win.PointerEvent || win.MSPointerEvent) {
12795
12796 // The touches object keeps track of the points being touched at all times
12797 var touches = {},
12798 hasPointerEvent = !!win.PointerEvent,
12799 getWebkitTouches = function() {
12800 var key,
12801 fake = [];
12802 fake.item = function(i) {
12803 return this[i];
12804 };
12805 for (key in touches) {
12806 if (touches.hasOwnProperty(key)) {
12807 fake.push({
12808 pageX: touches[key].pageX,
12809 pageY: touches[key].pageY,
12810 target: touches[key].target
12811 });
12812 }
12813 }
12814 return fake;
12815 },
12816 translateMSPointer = function(e, method, wktype, func) {
12817 var p;
12818 if ((e.pointerType === 'touch' || e.pointerType === e.MSPOINTER_TYPE_TOUCH) && charts[H.hoverChartIndex]) {
12819 func(e);
12820 p = charts[H.hoverChartIndex].pointer;
12821 p[method]({
12822 type: wktype,
12823 target: e.currentTarget,
12824 preventDefault: noop,
12825 touches: getWebkitTouches()
12826 });
12827 }
12828 };
12829
12830 /**
12831 * Extend the Pointer prototype with methods for each event handler and more
12832 */
12833 extend(Pointer.prototype, /** @lends Pointer.prototype */ {
12834 onContainerPointerDown: function(e) {
12835 translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function(e) {
12836 touches[e.pointerId] = {
12837 pageX: e.pageX,
12838 pageY: e.pageY,
12839 target: e.currentTarget
12840 };
12841 });
12842 },
12843 onContainerPointerMove: function(e) {
12844 translateMSPointer(e, 'onContainerTouchMove', 'touchmove', function(e) {
12845 touches[e.pointerId] = {
12846 pageX: e.pageX,
12847 pageY: e.pageY
12848 };
12849 if (!touches[e.pointerId].target) {
12850 touches[e.pointerId].target = e.currentTarget;
12851 }
12852 });
12853 },
12854 onDocumentPointerUp: function(e) {
12855 translateMSPointer(e, 'onDocumentTouchEnd', 'touchend', function(e) {
12856 delete touches[e.pointerId];
12857 });
12858 },
12859
12860 /**
12861 * Add or remove the MS Pointer specific events
12862 */
12863 batchMSEvents: function(fn) {
12864 fn(this.chart.container, hasPointerEvent ? 'pointerdown' : 'MSPointerDown', this.onContainerPointerDown);
12865 fn(this.chart.container, hasPointerEvent ? 'pointermove' : 'MSPointerMove', this.onContainerPointerMove);
12866 fn(doc, hasPointerEvent ? 'pointerup' : 'MSPointerUp', this.onDocumentPointerUp);
12867 }
12868 });
12869
12870 // Disable default IE actions for pinch and such on chart element
12871 wrap(Pointer.prototype, 'init', function(proceed, chart, options) {
12872 proceed.call(this, chart, options);
12873 if (this.hasZoom) { // #4014
12874 css(chart.container, {
12875 '-ms-touch-action': 'none',
12876 'touch-action': 'none'
12877 });
12878 }
12879 });
12880
12881 // Add IE specific touch events to chart
12882 wrap(Pointer.prototype, 'setDOMEvents', function(proceed) {
12883 proceed.apply(this);
12884 if (this.hasZoom || this.followTouchMove) {
12885 this.batchMSEvents(addEvent);
12886 }
12887 });
12888 // Destroy MS events also
12889 wrap(Pointer.prototype, 'destroy', function(proceed) {
12890 this.batchMSEvents(removeEvent);
12891 proceed.call(this);
12892 });
12893 }
12894
12895 }(Highcharts));
12896 (function(H) {
12897 /**
12898 * (c) 2010-2016 Torstein Honsi
12899 *
12900 * License: www.highcharts.com/license
12901 */
12902 'use strict';
12903 var Legend,
12904
12905 addEvent = H.addEvent,
12906 css = H.css,
12907 discardElement = H.discardElement,
12908 defined = H.defined,
12909 each = H.each,
12910 extend = H.extend,
12911 isFirefox = H.isFirefox,
12912 marginNames = H.marginNames,
12913 merge = H.merge,
12914 pick = H.pick,
12915 setAnimation = H.setAnimation,
12916 stableSort = H.stableSort,
12917 win = H.win,
12918 wrap = H.wrap;
12919 /**
12920 * The overview of the chart's series.
12921 * @class
12922 */
12923 Legend = H.Legend = function(chart, options) {
12924 this.init(chart, options);
12925 };
12926
12927 Legend.prototype = {
12928
12929 /**
12930 * Initialize the legend
12931 */
12932 init: function(chart, options) {
12933
12934 this.chart = chart;
12935
12936 this.setOptions(options);
12937
12938 if (options.enabled) {
12939
12940 // Render it
12941 this.render();
12942
12943 // move checkboxes
12944 addEvent(this.chart, 'endResize', function() {
12945 this.legend.positionCheckboxes();
12946 });
12947 }
12948 },
12949
12950 setOptions: function(options) {
12951
12952 var padding = pick(options.padding, 8);
12953
12954 this.options = options;
12955
12956
12957 this.itemStyle = options.itemStyle;
12958 this.itemHiddenStyle = merge(this.itemStyle, options.itemHiddenStyle);
12959
12960 this.itemMarginTop = options.itemMarginTop || 0;
12961 this.padding = padding;
12962 this.initialItemX = padding;
12963 this.initialItemY = padding - 5; // 5 is the number of pixels above the text
12964 this.maxItemWidth = 0;
12965 this.itemHeight = 0;
12966 this.symbolWidth = pick(options.symbolWidth, 16);
12967 this.pages = [];
12968
12969 },
12970
12971 /**
12972 * Update the legend with new options. Equivalent to running chart.update with a legend
12973 * configuration option.
12974 * @param {Object} options Legend options
12975 * @param {Boolean} redraw Whether to redraw the chart, defaults to true.
12976 */
12977 update: function(options, redraw) {
12978 var chart = this.chart;
12979
12980 this.setOptions(merge(true, this.options, options));
12981 this.destroy();
12982 chart.isDirtyLegend = chart.isDirtyBox = true;
12983 if (pick(redraw, true)) {
12984 chart.redraw();
12985 }
12986 },
12987
12988 /**
12989 * Set the colors for the legend item
12990 * @param {Object} item A Series or Point instance
12991 * @param {Object} visible Dimmed or colored
12992 */
12993 colorizeItem: function(item, visible) {
12994 item.legendGroup[visible ? 'removeClass' : 'addClass']('highcharts-legend-item-hidden');
12995
12996
12997 var legend = this,
12998 options = legend.options,
12999 legendItem = item.legendItem,
13000 legendLine = item.legendLine,
13001 legendSymbol = item.legendSymbol,
13002 hiddenColor = legend.itemHiddenStyle.color,
13003 textColor = visible ? options.itemStyle.color : hiddenColor,
13004 symbolColor = visible ? (item.color || hiddenColor) : hiddenColor,
13005 markerOptions = item.options && item.options.marker,
13006 symbolAttr = {
13007 fill: symbolColor
13008 },
13009 key;
13010
13011 if (legendItem) {
13012 legendItem.css({
13013 fill: textColor,
13014 color: textColor
13015 }); // color for #1553, oldIE
13016 }
13017 if (legendLine) {
13018 legendLine.attr({
13019 stroke: symbolColor
13020 });
13021 }
13022
13023 if (legendSymbol) {
13024
13025 // Apply marker options
13026 if (markerOptions && legendSymbol.isMarker) { // #585
13027 //symbolAttr.stroke = symbolColor;
13028 symbolAttr = item.pointAttribs();
13029 if (!visible) {
13030 for (key in symbolAttr) {
13031 symbolAttr[key] = hiddenColor;
13032 }
13033 }
13034 }
13035
13036 legendSymbol.attr(symbolAttr);
13037 }
13038
13039 },
13040
13041 /**
13042 * Position the legend item
13043 * @param {Object} item A Series or Point instance
13044 */
13045 positionItem: function(item) {
13046 var legend = this,
13047 options = legend.options,
13048 symbolPadding = options.symbolPadding,
13049 ltr = !options.rtl,
13050 legendItemPos = item._legendItemPos,
13051 itemX = legendItemPos[0],
13052 itemY = legendItemPos[1],
13053 checkbox = item.checkbox,
13054 legendGroup = item.legendGroup;
13055
13056 if (legendGroup && legendGroup.element) {
13057 legendGroup.translate(
13058 ltr ? itemX : legend.legendWidth - itemX - 2 * symbolPadding - 4,
13059 itemY
13060 );
13061 }
13062
13063 if (checkbox) {
13064 checkbox.x = itemX;
13065 checkbox.y = itemY;
13066 }
13067 },
13068
13069 /**
13070 * Destroy a single legend item
13071 * @param {Object} item The series or point
13072 */
13073 destroyItem: function(item) {
13074 var checkbox = item.checkbox;
13075
13076 // destroy SVG elements
13077 each(['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], function(key) {
13078 if (item[key]) {
13079 item[key] = item[key].destroy();
13080 }
13081 });
13082
13083 if (checkbox) {
13084 discardElement(item.checkbox);
13085 }
13086 },
13087
13088 /**
13089 * Destroys the legend.
13090 */
13091 destroy: function() {
13092 var legend = this,
13093 legendGroup = legend.group,
13094 box = legend.box;
13095
13096 if (box) {
13097 legend.box = box.destroy();
13098 }
13099
13100 // Destroy items
13101 each(this.getAllItems(), function(item) {
13102 each(['legendItem', 'legendGroup'], function(key) {
13103 if (item[key]) {
13104 item[key] = item[key].destroy();
13105 }
13106 });
13107 });
13108
13109 if (legendGroup) {
13110 legend.group = legendGroup.destroy();
13111 }
13112 legend.display = null; // Reset in .render on update.
13113 },
13114
13115 /**
13116 * Position the checkboxes after the width is determined
13117 */
13118 positionCheckboxes: function(scrollOffset) {
13119 var alignAttr = this.group && this.group.alignAttr,
13120 translateY,
13121 clipHeight = this.clipHeight || this.legendHeight,
13122 titleHeight = this.titleHeight;
13123
13124 if (alignAttr) {
13125 translateY = alignAttr.translateY;
13126 each(this.allItems, function(item) {
13127 var checkbox = item.checkbox,
13128 top;
13129
13130 if (checkbox) {
13131 top = translateY + titleHeight + checkbox.y + (scrollOffset || 0) + 3;
13132 css(checkbox, {
13133 left: (alignAttr.translateX + item.checkboxOffset + checkbox.x - 20) + 'px',
13134 top: top + 'px',
13135 display: top > translateY - 6 && top < translateY + clipHeight - 6 ? '' : 'none'
13136 });
13137 }
13138 });
13139 }
13140 },
13141
13142 /**
13143 * Render the legend title on top of the legend
13144 */
13145 renderTitle: function() {
13146 var options = this.options,
13147 padding = this.padding,
13148 titleOptions = options.title,
13149 titleHeight = 0,
13150 bBox;
13151
13152 if (titleOptions.text) {
13153 if (!this.title) {
13154 this.title = this.chart.renderer.label(titleOptions.text, padding - 3, padding - 4, null, null, null, null, null, 'legend-title')
13155 .attr({
13156 zIndex: 1
13157 })
13158
13159 .css(titleOptions.style)
13160
13161 .add(this.group);
13162 }
13163 bBox = this.title.getBBox();
13164 titleHeight = bBox.height;
13165 this.offsetWidth = bBox.width; // #1717
13166 this.contentGroup.attr({
13167 translateY: titleHeight
13168 });
13169 }
13170 this.titleHeight = titleHeight;
13171 },
13172
13173 /**
13174 * Set the legend item text
13175 */
13176 setText: function(item) {
13177 var options = this.options;
13178 item.legendItem.attr({
13179 text: options.labelFormat ? H.format(options.labelFormat, item) : options.labelFormatter.call(item)
13180 });
13181 },
13182
13183 /**
13184 * Render a single specific legend item
13185 * @param {Object} item A series or point
13186 */
13187 renderItem: function(item) {
13188 var legend = this,
13189 chart = legend.chart,
13190 renderer = chart.renderer,
13191 options = legend.options,
13192 horizontal = options.layout === 'horizontal',
13193 symbolWidth = legend.symbolWidth,
13194 symbolPadding = options.symbolPadding,
13195
13196 itemStyle = legend.itemStyle,
13197 itemHiddenStyle = legend.itemHiddenStyle,
13198
13199 padding = legend.padding,
13200 itemDistance = horizontal ? pick(options.itemDistance, 20) : 0,
13201 ltr = !options.rtl,
13202 itemHeight,
13203 widthOption = options.width,
13204 itemMarginBottom = options.itemMarginBottom || 0,
13205 itemMarginTop = legend.itemMarginTop,
13206 initialItemX = legend.initialItemX,
13207 bBox,
13208 itemWidth,
13209 li = item.legendItem,
13210 isSeries = !item.series,
13211 series = !isSeries && item.series.drawLegendSymbol ? item.series : item,
13212 seriesOptions = series.options,
13213 showCheckbox = legend.createCheckboxForItem && seriesOptions && seriesOptions.showCheckbox,
13214 useHTML = options.useHTML,
13215 fontSize = 12;
13216
13217 if (!li) { // generate it once, later move it
13218
13219 // Generate the group box
13220 // A group to hold the symbol and text. Text is to be appended in Legend class.
13221 item.legendGroup = renderer.g('legend-item')
13222 .addClass('highcharts-' + series.type + '-series highcharts-color-' + item.colorIndex +
13223 (item.options.className ? ' ' + item.options.className : '') +
13224 (isSeries ? ' highcharts-series-' + item.index : '')
13225 )
13226 .attr({
13227 zIndex: 1
13228 })
13229 .add(legend.scrollGroup);
13230
13231 // Generate the list item text and add it to the group
13232 item.legendItem = li = renderer.text(
13233 '',
13234 ltr ? symbolWidth + symbolPadding : -symbolPadding,
13235 legend.baseline || 0,
13236 useHTML
13237 )
13238
13239 .css(merge(item.visible ? itemStyle : itemHiddenStyle)) // merge to prevent modifying original (#1021)
13240
13241 .attr({
13242 align: ltr ? 'left' : 'right',
13243 zIndex: 2
13244 })
13245 .add(item.legendGroup);
13246
13247 // Get the baseline for the first item - the font size is equal for all
13248 if (!legend.baseline) {
13249
13250 fontSize = itemStyle.fontSize;
13251
13252 legend.fontMetrics = renderer.fontMetrics(
13253 fontSize,
13254 li
13255 );
13256 legend.baseline = legend.fontMetrics.f + 3 + itemMarginTop;
13257 li.attr('y', legend.baseline);
13258 }
13259
13260 // Draw the legend symbol inside the group box
13261 series.drawLegendSymbol(legend, item);
13262
13263 if (legend.setItemEvents) {
13264 legend.setItemEvents(item, li, useHTML);
13265 }
13266
13267 // add the HTML checkbox on top
13268 if (showCheckbox) {
13269 legend.createCheckboxForItem(item);
13270 }
13271 }
13272
13273 // Colorize the items
13274 legend.colorizeItem(item, item.visible);
13275
13276 // Always update the text
13277 legend.setText(item);
13278
13279 // calculate the positions for the next line
13280 bBox = li.getBBox();
13281
13282 itemWidth = item.checkboxOffset =
13283 options.itemWidth ||
13284 item.legendItemWidth ||
13285 symbolWidth + symbolPadding + bBox.width + itemDistance + (showCheckbox ? 20 : 0);
13286 legend.itemHeight = itemHeight = Math.round(item.legendItemHeight || bBox.height);
13287
13288 // if the item exceeds the width, start a new line
13289 if (horizontal && legend.itemX - initialItemX + itemWidth >
13290 (widthOption || (chart.chartWidth - 2 * padding - initialItemX - options.x))) {
13291 legend.itemX = initialItemX;
13292 legend.itemY += itemMarginTop + legend.lastLineHeight + itemMarginBottom;
13293 legend.lastLineHeight = 0; // reset for next line (#915, #3976)
13294 }
13295
13296 // If the item exceeds the height, start a new column
13297 /*if (!horizontal && legend.itemY + options.y + itemHeight > chart.chartHeight - spacingTop - spacingBottom) {
13298 legend.itemY = legend.initialItemY;
13299 legend.itemX += legend.maxItemWidth;
13300 legend.maxItemWidth = 0;
13301 }*/
13302
13303 // Set the edge positions
13304 legend.maxItemWidth = Math.max(legend.maxItemWidth, itemWidth);
13305 legend.lastItemY = itemMarginTop + legend.itemY + itemMarginBottom;
13306 legend.lastLineHeight = Math.max(itemHeight, legend.lastLineHeight); // #915
13307
13308 // cache the position of the newly generated or reordered items
13309 item._legendItemPos = [legend.itemX, legend.itemY];
13310
13311 // advance
13312 if (horizontal) {
13313 legend.itemX += itemWidth;
13314
13315 } else {
13316 legend.itemY += itemMarginTop + itemHeight + itemMarginBottom;
13317 legend.lastLineHeight = itemHeight;
13318 }
13319
13320 // the width of the widest item
13321 legend.offsetWidth = widthOption || Math.max(
13322 (horizontal ? legend.itemX - initialItemX - itemDistance : itemWidth) + padding,
13323 legend.offsetWidth
13324 );
13325 },
13326
13327 /**
13328 * Get all items, which is one item per series for normal series and one item per point
13329 * for pie series.
13330 */
13331 getAllItems: function() {
13332 var allItems = [];
13333 each(this.chart.series, function(series) {
13334 var seriesOptions = series && series.options;
13335
13336 // Handle showInLegend. If the series is linked to another series, defaults to false.
13337 if (series && pick(seriesOptions.showInLegend, !defined(seriesOptions.linkedTo) ? undefined : false, true)) {
13338
13339 // Use points or series for the legend item depending on legendType
13340 allItems = allItems.concat(
13341 series.legendItems ||
13342 (seriesOptions.legendType === 'point' ?
13343 series.data :
13344 series)
13345 );
13346 }
13347 });
13348 return allItems;
13349 },
13350
13351 /**
13352 * Adjust the chart margins by reserving space for the legend on only one side
13353 * of the chart. If the position is set to a corner, top or bottom is reserved
13354 * for horizontal legends and left or right for vertical ones.
13355 */
13356 adjustMargins: function(margin, spacing) {
13357 var chart = this.chart,
13358 options = this.options,
13359 // Use the first letter of each alignment option in order to detect the side
13360 alignment = options.align.charAt(0) + options.verticalAlign.charAt(0) + options.layout.charAt(0); // #4189 - use charAt(x) notation instead of [x] for IE7
13361
13362 if (!options.floating) {
13363
13364 each([
13365 /(lth|ct|rth)/,
13366 /(rtv|rm|rbv)/,
13367 /(rbh|cb|lbh)/,
13368 /(lbv|lm|ltv)/
13369 ], function(alignments, side) {
13370 if (alignments.test(alignment) && !defined(margin[side])) {
13371 // Now we have detected on which side of the chart we should reserve space for the legend
13372 chart[marginNames[side]] = Math.max(
13373 chart[marginNames[side]],
13374 chart.legend[(side + 1) % 2 ? 'legendHeight' : 'legendWidth'] + [1, -1, -1, 1][side] * options[(side % 2) ? 'x' : 'y'] +
13375 pick(options.margin, 12) +
13376 spacing[side]
13377 );
13378 }
13379 });
13380 }
13381 },
13382
13383 /**
13384 * Render the legend. This method can be called both before and after
13385 * chart.render. If called after, it will only rearrange items instead
13386 * of creating new ones.
13387 */
13388 render: function() {
13389 var legend = this,
13390 chart = legend.chart,
13391 renderer = chart.renderer,
13392 legendGroup = legend.group,
13393 allItems,
13394 display,
13395 legendWidth,
13396 legendHeight,
13397 box = legend.box,
13398 options = legend.options,
13399 padding = legend.padding;
13400
13401 legend.itemX = legend.initialItemX;
13402 legend.itemY = legend.initialItemY;
13403 legend.offsetWidth = 0;
13404 legend.lastItemY = 0;
13405
13406 if (!legendGroup) {
13407 legend.group = legendGroup = renderer.g('legend')
13408 .attr({
13409 zIndex: 7
13410 })
13411 .add();
13412 legend.contentGroup = renderer.g()
13413 .attr({
13414 zIndex: 1
13415 }) // above background
13416 .add(legendGroup);
13417 legend.scrollGroup = renderer.g()
13418 .add(legend.contentGroup);
13419 }
13420
13421 legend.renderTitle();
13422
13423 // add each series or point
13424 allItems = legend.getAllItems();
13425
13426 // sort by legendIndex
13427 stableSort(allItems, function(a, b) {
13428 return ((a.options && a.options.legendIndex) || 0) - ((b.options && b.options.legendIndex) || 0);
13429 });
13430
13431 // reversed legend
13432 if (options.reversed) {
13433 allItems.reverse();
13434 }
13435
13436 legend.allItems = allItems;
13437 legend.display = display = !!allItems.length;
13438
13439 // render the items
13440 legend.lastLineHeight = 0;
13441 each(allItems, function(item) {
13442 legend.renderItem(item);
13443 });
13444
13445 // Get the box
13446 legendWidth = (options.width || legend.offsetWidth) + padding;
13447 legendHeight = legend.lastItemY + legend.lastLineHeight + legend.titleHeight;
13448 legendHeight = legend.handleOverflow(legendHeight);
13449 legendHeight += padding;
13450
13451 // Draw the border and/or background
13452 if (!box) {
13453 legend.box = box = renderer.rect()
13454 .addClass('highcharts-legend-box')
13455 .attr({
13456 r: options.borderRadius
13457 })
13458 .add(legendGroup);
13459 box.isNew = true;
13460 }
13461
13462
13463 // Presentational
13464 box
13465 .attr({
13466 stroke: options.borderColor,
13467 'stroke-width': options.borderWidth || 0,
13468 fill: options.backgroundColor || 'none'
13469 })
13470 .shadow(options.shadow);
13471
13472
13473 if (legendWidth > 0 && legendHeight > 0) {
13474 box[box.isNew ? 'attr' : 'animate'](
13475 box.crisp({
13476 x: 0,
13477 y: 0,
13478 width: legendWidth,
13479 height: legendHeight
13480 }, box.strokeWidth())
13481 );
13482 box.isNew = false;
13483 }
13484
13485 // hide the border if no items
13486 box[display ? 'show' : 'hide']();
13487
13488
13489
13490 legend.legendWidth = legendWidth;
13491 legend.legendHeight = legendHeight;
13492
13493 // Now that the legend width and height are established, put the items in the
13494 // final position
13495 each(allItems, function(item) {
13496 legend.positionItem(item);
13497 });
13498
13499 // 1.x compatibility: positioning based on style
13500 /*var props = ['left', 'right', 'top', 'bottom'],
13501 prop,
13502 i = 4;
13503 while (i--) {
13504 prop = props[i];
13505 if (options.style[prop] && options.style[prop] !== 'auto') {
13506 options[i < 2 ? 'align' : 'verticalAlign'] = prop;
13507 options[i < 2 ? 'x' : 'y'] = pInt(options.style[prop]) * (i % 2 ? -1 : 1);
13508 }
13509 }*/
13510
13511 if (display) {
13512 legendGroup.align(extend({
13513 width: legendWidth,
13514 height: legendHeight
13515 }, options), true, 'spacingBox');
13516 }
13517
13518 if (!chart.isResizing) {
13519 this.positionCheckboxes();
13520 }
13521 },
13522
13523 /**
13524 * Set up the overflow handling by adding navigation with up and down arrows below the
13525 * legend.
13526 */
13527 handleOverflow: function(legendHeight) {
13528 var legend = this,
13529 chart = this.chart,
13530 renderer = chart.renderer,
13531 options = this.options,
13532 optionsY = options.y,
13533 alignTop = options.verticalAlign === 'top',
13534 spaceHeight = chart.spacingBox.height + (alignTop ? -optionsY : optionsY) - this.padding,
13535 maxHeight = options.maxHeight,
13536 clipHeight,
13537 clipRect = this.clipRect,
13538 navOptions = options.navigation,
13539 animation = pick(navOptions.animation, true),
13540 arrowSize = navOptions.arrowSize || 12,
13541 nav = this.nav,
13542 pages = this.pages,
13543 padding = this.padding,
13544 lastY,
13545 allItems = this.allItems,
13546 clipToHeight = function(height) {
13547 if (height) {
13548 clipRect.attr({
13549 height: height
13550 });
13551 } else if (clipRect) { // Reset (#5912)
13552 legend.clipRect = clipRect.destroy();
13553 legend.contentGroup.clip();
13554 }
13555
13556 // useHTML
13557 if (legend.contentGroup.div) {
13558 legend.contentGroup.div.style.clip = height ?
13559 'rect(' + padding + 'px,9999px,' +
13560 (padding + height) + 'px,0)' :
13561 'auto';
13562 }
13563 };
13564
13565
13566 // Adjust the height
13567 if (options.layout === 'horizontal' && options.verticalAlign !== 'middle' && !options.floating) {
13568 spaceHeight /= 2;
13569 }
13570 if (maxHeight) {
13571 spaceHeight = Math.min(spaceHeight, maxHeight);
13572 }
13573
13574 // Reset the legend height and adjust the clipping rectangle
13575 pages.length = 0;
13576 if (legendHeight > spaceHeight && navOptions.enabled !== false) {
13577
13578 this.clipHeight = clipHeight = Math.max(spaceHeight - 20 - this.titleHeight - padding, 0);
13579 this.currentPage = pick(this.currentPage, 1);
13580 this.fullHeight = legendHeight;
13581
13582 // Fill pages with Y positions so that the top of each a legend item defines
13583 // the scroll top for each page (#2098)
13584 each(allItems, function(item, i) {
13585 var y = item._legendItemPos[1],
13586 h = Math.round(item.legendItem.getBBox().height),
13587 len = pages.length;
13588
13589 if (!len || (y - pages[len - 1] > clipHeight && (lastY || y) !== pages[len - 1])) {
13590 pages.push(lastY || y);
13591 len++;
13592 }
13593
13594 if (i === allItems.length - 1 && y + h - pages[len - 1] > clipHeight) {
13595 pages.push(y);
13596 }
13597 if (y !== lastY) {
13598 lastY = y;
13599 }
13600 });
13601
13602 // Only apply clipping if needed. Clipping causes blurred legend in PDF export (#1787)
13603 if (!clipRect) {
13604 clipRect = legend.clipRect = renderer.clipRect(0, padding, 9999, 0);
13605 legend.contentGroup.clip(clipRect);
13606 }
13607
13608 clipToHeight(clipHeight);
13609
13610 // Add navigation elements
13611 if (!nav) {
13612 this.nav = nav = renderer.g().attr({
13613 zIndex: 1
13614 }).add(this.group);
13615 this.up = renderer.symbol('triangle', 0, 0, arrowSize, arrowSize)
13616 .on('click', function() {
13617 legend.scroll(-1, animation);
13618 })
13619 .add(nav);
13620 this.pager = renderer.text('', 15, 10)
13621 .addClass('highcharts-legend-navigation')
13622
13623 .css(navOptions.style)
13624
13625 .add(nav);
13626 this.down = renderer.symbol('triangle-down', 0, 0, arrowSize, arrowSize)
13627 .on('click', function() {
13628 legend.scroll(1, animation);
13629 })
13630 .add(nav);
13631 }
13632
13633 // Set initial position
13634 legend.scroll(0);
13635
13636 legendHeight = spaceHeight;
13637
13638 // Reset
13639 } else if (nav) {
13640 clipToHeight();
13641 nav.hide();
13642 this.scrollGroup.attr({
13643 translateY: 1
13644 });
13645 this.clipHeight = 0; // #1379
13646 }
13647
13648 return legendHeight;
13649 },
13650
13651 /**
13652 * Scroll the legend by a number of pages
13653 * @param {Object} scrollBy
13654 * @param {Object} animation
13655 */
13656 scroll: function(scrollBy, animation) {
13657 var pages = this.pages,
13658 pageCount = pages.length,
13659 currentPage = this.currentPage + scrollBy,
13660 clipHeight = this.clipHeight,
13661 navOptions = this.options.navigation,
13662 pager = this.pager,
13663 padding = this.padding,
13664 scrollOffset;
13665
13666 // When resizing while looking at the last page
13667 if (currentPage > pageCount) {
13668 currentPage = pageCount;
13669 }
13670
13671 if (currentPage > 0) {
13672
13673 if (animation !== undefined) {
13674 setAnimation(animation, this.chart);
13675 }
13676
13677 this.nav.attr({
13678 translateX: padding,
13679 translateY: clipHeight + this.padding + 7 + this.titleHeight,
13680 visibility: 'visible'
13681 });
13682 this.up.attr({
13683 'class': currentPage === 1 ? 'highcharts-legend-nav-inactive' : 'highcharts-legend-nav-active'
13684 });
13685 pager.attr({
13686 text: currentPage + '/' + pageCount
13687 });
13688 this.down.attr({
13689 'x': 18 + this.pager.getBBox().width, // adjust to text width
13690 'class': currentPage === pageCount ? 'highcharts-legend-nav-inactive' : 'highcharts-legend-nav-active'
13691 });
13692
13693
13694 this.up
13695 .attr({
13696 fill: currentPage === 1 ? navOptions.inactiveColor : navOptions.activeColor
13697 })
13698 .css({
13699 cursor: currentPage === 1 ? 'default' : 'pointer'
13700 });
13701 this.down
13702 .attr({
13703 fill: currentPage === pageCount ? navOptions.inactiveColor : navOptions.activeColor
13704 })
13705 .css({
13706 cursor: currentPage === pageCount ? 'default' : 'pointer'
13707 });
13708
13709
13710 scrollOffset = -pages[currentPage - 1] + this.initialItemY;
13711
13712 this.scrollGroup.animate({
13713 translateY: scrollOffset
13714 });
13715
13716 this.currentPage = currentPage;
13717 this.positionCheckboxes(scrollOffset);
13718 }
13719
13720 }
13721
13722 };
13723
13724 /*
13725 * LegendSymbolMixin
13726 */
13727
13728 H.LegendSymbolMixin = {
13729
13730 /**
13731 * Get the series' symbol in the legend
13732 *
13733 * @param {Object} legend The legend object
13734 * @param {Object} item The series (this) or point
13735 */
13736 drawRectangle: function(legend, item) {
13737 var options = legend.options,
13738 symbolHeight = options.symbolHeight || legend.fontMetrics.f,
13739 square = options.squareSymbol,
13740 symbolWidth = square ? symbolHeight : legend.symbolWidth;
13741
13742 item.legendSymbol = this.chart.renderer.rect(
13743 square ? (legend.symbolWidth - symbolHeight) / 2 : 0,
13744 legend.baseline - symbolHeight + 1, // #3988
13745 symbolWidth,
13746 symbolHeight,
13747 pick(legend.options.symbolRadius, symbolHeight / 2)
13748 )
13749 .addClass('highcharts-point')
13750 .attr({
13751 zIndex: 3
13752 }).add(item.legendGroup);
13753
13754 },
13755
13756 /**
13757 * Get the series' symbol in the legend. This method should be overridable to create custom
13758 * symbols through Highcharts.seriesTypes[type].prototype.drawLegendSymbols.
13759 *
13760 * @param {Object} legend The legend object
13761 */
13762 drawLineMarker: function(legend) {
13763
13764 var options = this.options,
13765 markerOptions = options.marker,
13766 radius,
13767 legendSymbol,
13768 symbolWidth = legend.symbolWidth,
13769 renderer = this.chart.renderer,
13770 legendItemGroup = this.legendGroup,
13771 verticalCenter = legend.baseline - Math.round(legend.fontMetrics.b * 0.3),
13772 attr = {};
13773
13774 // Draw the line
13775
13776 attr = {
13777 'stroke-width': options.lineWidth || 0
13778 };
13779 if (options.dashStyle) {
13780 attr.dashstyle = options.dashStyle;
13781 }
13782
13783
13784 this.legendLine = renderer.path([
13785 'M',
13786 0,
13787 verticalCenter,
13788 'L',
13789 symbolWidth,
13790 verticalCenter
13791 ])
13792 .addClass('highcharts-graph')
13793 .attr(attr)
13794 .add(legendItemGroup);
13795
13796 // Draw the marker
13797 if (markerOptions && markerOptions.enabled !== false) {
13798 radius = this.symbol.indexOf('url') === 0 ? 0 : markerOptions.radius;
13799 this.legendSymbol = legendSymbol = renderer.symbol(
13800 this.symbol,
13801 (symbolWidth / 2) - radius,
13802 verticalCenter - radius,
13803 2 * radius,
13804 2 * radius,
13805 markerOptions
13806 )
13807 .addClass('highcharts-point')
13808 .add(legendItemGroup);
13809 legendSymbol.isMarker = true;
13810 }
13811 }
13812 };
13813
13814 // Workaround for #2030, horizontal legend items not displaying in IE11 Preview,
13815 // and for #2580, a similar drawing flaw in Firefox 26.
13816 // Explore if there's a general cause for this. The problem may be related
13817 // to nested group elements, as the legend item texts are within 4 group elements.
13818 if (/Trident\/7\.0/.test(win.navigator.userAgent) || isFirefox) {
13819 wrap(Legend.prototype, 'positionItem', function(proceed, item) {
13820 var legend = this,
13821 runPositionItem = function() { // If chart destroyed in sync, this is undefined (#2030)
13822 if (item._legendItemPos) {
13823 proceed.call(legend, item);
13824 }
13825 };
13826
13827 // Do it now, for export and to get checkbox placement
13828 runPositionItem();
13829
13830 // Do it after to work around the core issue
13831 setTimeout(runPositionItem);
13832 });
13833 }
13834
13835 }(Highcharts));
13836 (function(H) {
13837 /**
13838 * (c) 2010-2016 Torstein Honsi
13839 *
13840 * License: www.highcharts.com/license
13841 */
13842 'use strict';
13843 var addEvent = H.addEvent,
13844 animate = H.animate,
13845 animObject = H.animObject,
13846 attr = H.attr,
13847 doc = H.doc,
13848 Axis = H.Axis, // @todo add as requirement
13849 createElement = H.createElement,
13850 defaultOptions = H.defaultOptions,
13851 discardElement = H.discardElement,
13852 charts = H.charts,
13853 css = H.css,
13854 defined = H.defined,
13855 each = H.each,
13856 error = H.error,
13857 extend = H.extend,
13858 fireEvent = H.fireEvent,
13859 getStyle = H.getStyle,
13860 grep = H.grep,
13861 isNumber = H.isNumber,
13862 isObject = H.isObject,
13863 isString = H.isString,
13864 Legend = H.Legend, // @todo add as requirement
13865 marginNames = H.marginNames,
13866 merge = H.merge,
13867 Pointer = H.Pointer, // @todo add as requirement
13868 pick = H.pick,
13869 pInt = H.pInt,
13870 removeEvent = H.removeEvent,
13871 seriesTypes = H.seriesTypes,
13872 splat = H.splat,
13873 svg = H.svg,
13874 syncTimeout = H.syncTimeout,
13875 win = H.win,
13876 Renderer = H.Renderer;
13877 /**
13878 * The Chart class.
13879 * @class Highcharts.Chart
13880 * @memberOf Highcharts
13881 * @param {String|HTMLDOMElement} renderTo - The DOM element to render to, or its
13882 * id.
13883 * @param {ChartOptions} options - The chart options structure.
13884 * @param {Function} callback - Function to run when the chart has loaded.
13885 */
13886 var Chart = H.Chart = function() {
13887 this.getArgs.apply(this, arguments);
13888 };
13889
13890 H.chart = function(a, b, c) {
13891 return new Chart(a, b, c);
13892 };
13893
13894 Chart.prototype = {
13895
13896 /**
13897 * Hook for modules
13898 */
13899 callbacks: [],
13900
13901 /**
13902 * Handle the arguments passed to the constructor
13903 * @returns {Array} Arguments without renderTo
13904 */
13905 getArgs: function() {
13906 var args = [].slice.call(arguments);
13907
13908 // Remove the optional first argument, renderTo, and
13909 // set it on this.
13910 if (isString(args[0]) || args[0].nodeName) {
13911 this.renderTo = args.shift();
13912 }
13913 this.init(args[0], args[1]);
13914 },
13915
13916 /**
13917 * Initialize the chart
13918 */
13919 init: function(userOptions, callback) {
13920
13921 // Handle regular options
13922 var options,
13923 seriesOptions = userOptions.series; // skip merging data points to increase performance
13924
13925 userOptions.series = null;
13926 options = merge(defaultOptions, userOptions); // do the merge
13927 options.series = userOptions.series = seriesOptions; // set back the series data
13928 this.userOptions = userOptions;
13929 this.respRules = [];
13930
13931 var optionsChart = options.chart;
13932
13933 var chartEvents = optionsChart.events;
13934
13935 this.margin = [];
13936 this.spacing = [];
13937
13938 //this.runChartClick = chartEvents && !!chartEvents.click;
13939 this.bounds = {
13940 h: {},
13941 v: {}
13942 }; // Pixel data bounds for touch zoom
13943
13944 this.callback = callback;
13945 this.isResizing = 0;
13946 this.options = options;
13947 //chartTitleOptions = undefined;
13948 //chartSubtitleOptions = undefined;
13949
13950 this.axes = [];
13951 this.series = [];
13952 this.hasCartesianSeries = optionsChart.showAxes;
13953 //this.axisOffset = undefined;
13954 //this.inverted = undefined;
13955 //this.loadingShown = undefined;
13956 //this.container = undefined;
13957 //this.chartWidth = undefined;
13958 //this.chartHeight = undefined;
13959 //this.marginRight = undefined;
13960 //this.marginBottom = undefined;
13961 //this.containerWidth = undefined;
13962 //this.containerHeight = undefined;
13963 //this.oldChartWidth = undefined;
13964 //this.oldChartHeight = undefined;
13965
13966 //this.renderTo = undefined;
13967 //this.renderToClone = undefined;
13968
13969 //this.spacingBox = undefined
13970
13971 //this.legend = undefined;
13972
13973 // Elements
13974 //this.chartBackground = undefined;
13975 //this.plotBackground = undefined;
13976 //this.plotBGImage = undefined;
13977 //this.plotBorder = undefined;
13978 //this.loadingDiv = undefined;
13979 //this.loadingSpan = undefined;
13980
13981 var chart = this,
13982 eventType;
13983
13984 // Add the chart to the global lookup
13985 chart.index = charts.length;
13986 charts.push(chart);
13987 H.chartCount++;
13988
13989 // Chart event handlers
13990 if (chartEvents) {
13991 for (eventType in chartEvents) {
13992 addEvent(chart, eventType, chartEvents[eventType]);
13993 }
13994 }
13995
13996 chart.xAxis = [];
13997 chart.yAxis = [];
13998
13999 chart.pointCount = chart.colorCounter = chart.symbolCounter = 0;
14000
14001 chart.firstRender();
14002 },
14003
14004 /**
14005 * Initialize an individual series, called internally before render time
14006 */
14007 initSeries: function(options) {
14008 var chart = this,
14009 optionsChart = chart.options.chart,
14010 type = options.type || optionsChart.type || optionsChart.defaultSeriesType,
14011 series,
14012 Constr = seriesTypes[type];
14013
14014 // No such series type
14015 if (!Constr) {
14016 error(17, true);
14017 }
14018
14019 series = new Constr();
14020 series.init(this, options);
14021 return series;
14022 },
14023
14024 /**
14025 * Check whether a given point is within the plot area
14026 *
14027 * @param {Number} plotX Pixel x relative to the plot area
14028 * @param {Number} plotY Pixel y relative to the plot area
14029 * @param {Boolean} inverted Whether the chart is inverted
14030 */
14031 isInsidePlot: function(plotX, plotY, inverted) {
14032 var x = inverted ? plotY : plotX,
14033 y = inverted ? plotX : plotY;
14034
14035 return x >= 0 &&
14036 x <= this.plotWidth &&
14037 y >= 0 &&
14038 y <= this.plotHeight;
14039 },
14040
14041 /**
14042 * Redraw legend, axes or series based on updated data
14043 *
14044 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
14045 * configuration
14046 */
14047 redraw: function(animation) {
14048 var chart = this,
14049 axes = chart.axes,
14050 series = chart.series,
14051 pointer = chart.pointer,
14052 legend = chart.legend,
14053 redrawLegend = chart.isDirtyLegend,
14054 hasStackedSeries,
14055 hasDirtyStacks,
14056 hasCartesianSeries = chart.hasCartesianSeries,
14057 isDirtyBox = chart.isDirtyBox,
14058 seriesLength = series.length,
14059 i = seriesLength,
14060 serie,
14061 renderer = chart.renderer,
14062 isHiddenChart = renderer.isHidden(),
14063 afterRedraw = [];
14064
14065 H.setAnimation(animation, chart);
14066
14067 if (isHiddenChart) {
14068 chart.cloneRenderTo();
14069 }
14070
14071 // Adjust title layout (reflow multiline text)
14072 chart.layOutTitles();
14073
14074 // link stacked series
14075 while (i--) {
14076 serie = series[i];
14077
14078 if (serie.options.stacking) {
14079 hasStackedSeries = true;
14080
14081 if (serie.isDirty) {
14082 hasDirtyStacks = true;
14083 break;
14084 }
14085 }
14086 }
14087 if (hasDirtyStacks) { // mark others as dirty
14088 i = seriesLength;
14089 while (i--) {
14090 serie = series[i];
14091 if (serie.options.stacking) {
14092 serie.isDirty = true;
14093 }
14094 }
14095 }
14096
14097 // Handle updated data in the series
14098 each(series, function(serie) {
14099 if (serie.isDirty) {
14100 if (serie.options.legendType === 'point') {
14101 if (serie.updateTotals) {
14102 serie.updateTotals();
14103 }
14104 redrawLegend = true;
14105 }
14106 }
14107 if (serie.isDirtyData) {
14108 fireEvent(serie, 'updatedData');
14109 }
14110 });
14111
14112 // handle added or removed series
14113 if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed
14114 // draw legend graphics
14115 legend.render();
14116
14117 chart.isDirtyLegend = false;
14118 }
14119
14120 // reset stacks
14121 if (hasStackedSeries) {
14122 chart.getStacks();
14123 }
14124
14125
14126 if (hasCartesianSeries) {
14127 // set axes scales
14128 each(axes, function(axis) {
14129 axis.updateNames();
14130 axis.setScale();
14131 });
14132 }
14133
14134 chart.getMargins(); // #3098
14135
14136 if (hasCartesianSeries) {
14137 // If one axis is dirty, all axes must be redrawn (#792, #2169)
14138 each(axes, function(axis) {
14139 if (axis.isDirty) {
14140 isDirtyBox = true;
14141 }
14142 });
14143
14144 // redraw axes
14145 each(axes, function(axis) {
14146
14147 // Fire 'afterSetExtremes' only if extremes are set
14148 var key = axis.min + ',' + axis.max;
14149 if (axis.extKey !== key) { // #821, #4452
14150 axis.extKey = key;
14151 afterRedraw.push(function() { // prevent a recursive call to chart.redraw() (#1119)
14152 fireEvent(axis, 'afterSetExtremes', extend(axis.eventArgs, axis.getExtremes())); // #747, #751
14153 delete axis.eventArgs;
14154 });
14155 }
14156 if (isDirtyBox || hasStackedSeries) {
14157 axis.redraw();
14158 }
14159 });
14160 }
14161
14162 // the plot areas size has changed
14163 if (isDirtyBox) {
14164 chart.drawChartBox();
14165 }
14166
14167
14168 // redraw affected series
14169 each(series, function(serie) {
14170 if ((isDirtyBox || serie.isDirty) && serie.visible) {
14171 serie.redraw();
14172 }
14173 });
14174
14175 // move tooltip or reset
14176 if (pointer) {
14177 pointer.reset(true);
14178 }
14179
14180 // redraw if canvas
14181 renderer.draw();
14182
14183 // fire the event
14184 fireEvent(chart, 'redraw');
14185
14186 if (isHiddenChart) {
14187 chart.cloneRenderTo(true);
14188 }
14189
14190 // Fire callbacks that are put on hold until after the redraw
14191 each(afterRedraw, function(callback) {
14192 callback.call();
14193 });
14194 },
14195
14196 /**
14197 * Get an axis, series or point object by id.
14198 * @param id {String} The id as given in the configuration options
14199 */
14200 get: function(id) {
14201 var chart = this,
14202 axes = chart.axes,
14203 series = chart.series;
14204
14205 var i,
14206 j,
14207 points;
14208
14209 // search axes
14210 for (i = 0; i < axes.length; i++) {
14211 if (axes[i].options.id === id) {
14212 return axes[i];
14213 }
14214 }
14215
14216 // search series
14217 for (i = 0; i < series.length; i++) {
14218 if (series[i].options.id === id) {
14219 return series[i];
14220 }
14221 }
14222
14223 // search points
14224 for (i = 0; i < series.length; i++) {
14225 points = series[i].points || [];
14226 for (j = 0; j < points.length; j++) {
14227 if (points[j].id === id) {
14228 return points[j];
14229 }
14230 }
14231 }
14232 return null;
14233 },
14234
14235 /**
14236 * Create the Axis instances based on the config options
14237 */
14238 getAxes: function() {
14239 var chart = this,
14240 options = this.options,
14241 xAxisOptions = options.xAxis = splat(options.xAxis || {}),
14242 yAxisOptions = options.yAxis = splat(options.yAxis || {}),
14243 optionsArray;
14244
14245 // make sure the options are arrays and add some members
14246 each(xAxisOptions, function(axis, i) {
14247 axis.index = i;
14248 axis.isX = true;
14249 });
14250
14251 each(yAxisOptions, function(axis, i) {
14252 axis.index = i;
14253 });
14254
14255 // concatenate all axis options into one array
14256 optionsArray = xAxisOptions.concat(yAxisOptions);
14257
14258 each(optionsArray, function(axisOptions) {
14259 new Axis(chart, axisOptions); // eslint-disable-line no-new
14260 });
14261 },
14262
14263
14264 /**
14265 * Get the currently selected points from all series
14266 */
14267 getSelectedPoints: function() {
14268 var points = [];
14269 each(this.series, function(serie) {
14270 points = points.concat(grep(serie.points || [], function(point) {
14271 return point.selected;
14272 }));
14273 });
14274 return points;
14275 },
14276
14277 /**
14278 * Get the currently selected series
14279 */
14280 getSelectedSeries: function() {
14281 return grep(this.series, function(serie) {
14282 return serie.selected;
14283 });
14284 },
14285
14286 /**
14287 * Show the title and subtitle of the chart
14288 *
14289 * @param titleOptions {Object} New title options
14290 * @param subtitleOptions {Object} New subtitle options
14291 *
14292 */
14293 setTitle: function(titleOptions, subtitleOptions, redraw) {
14294 var chart = this,
14295 options = chart.options,
14296 chartTitleOptions,
14297 chartSubtitleOptions;
14298
14299 chartTitleOptions = options.title = merge(
14300
14301 // Default styles
14302 {
14303 style: {
14304 color: '#333333',
14305 fontSize: options.isStock ? '16px' : '18px' // #2944
14306 }
14307 },
14308
14309 options.title,
14310 titleOptions
14311 );
14312 chartSubtitleOptions = options.subtitle = merge(
14313
14314 // Default styles
14315 {
14316 style: {
14317 color: '#666666'
14318 }
14319 },
14320
14321 options.subtitle,
14322 subtitleOptions
14323 );
14324
14325 // add title and subtitle
14326 each([
14327 ['title', titleOptions, chartTitleOptions],
14328 ['subtitle', subtitleOptions, chartSubtitleOptions]
14329 ], function(arr, i) {
14330 var name = arr[0],
14331 title = chart[name],
14332 titleOptions = arr[1],
14333 chartTitleOptions = arr[2];
14334
14335 if (title && titleOptions) {
14336 chart[name] = title = title.destroy(); // remove old
14337 }
14338
14339 if (chartTitleOptions && chartTitleOptions.text && !title) {
14340 chart[name] = chart.renderer.text(
14341 chartTitleOptions.text,
14342 0,
14343 0,
14344 chartTitleOptions.useHTML
14345 )
14346 .attr({
14347 align: chartTitleOptions.align,
14348 'class': 'highcharts-' + name,
14349 zIndex: chartTitleOptions.zIndex || 4
14350 })
14351 .add();
14352
14353 // Update methods, shortcut to Chart.setTitle
14354 chart[name].update = function(o) {
14355 chart.setTitle(!i && o, i && o);
14356 };
14357
14358
14359 // Presentational
14360 chart[name].css(chartTitleOptions.style);
14361
14362
14363 }
14364 });
14365 chart.layOutTitles(redraw);
14366 },
14367
14368 /**
14369 * Lay out the chart titles and cache the full offset height for use in getMargins
14370 */
14371 layOutTitles: function(redraw) {
14372 var titleOffset = 0,
14373 requiresDirtyBox,
14374 renderer = this.renderer,
14375 spacingBox = this.spacingBox;
14376
14377 // Lay out the title and the subtitle respectively
14378 each(['title', 'subtitle'], function(key) {
14379 var title = this[key],
14380 titleOptions = this.options[key],
14381 titleSize;
14382
14383 if (title) {
14384
14385 titleSize = titleOptions.style.fontSize;
14386
14387 titleSize = renderer.fontMetrics(titleSize, title).b;
14388
14389 title
14390 .css({
14391 width: (titleOptions.width || spacingBox.width + titleOptions.widthAdjust) + 'px'
14392 })
14393 .align(extend({
14394 y: titleOffset + titleSize + (key === 'title' ? -3 : 2)
14395 }, titleOptions), false, 'spacingBox');
14396
14397 if (!titleOptions.floating && !titleOptions.verticalAlign) {
14398 titleOffset = Math.ceil(titleOffset + title.getBBox().height);
14399 }
14400 }
14401 }, this);
14402
14403 requiresDirtyBox = this.titleOffset !== titleOffset;
14404 this.titleOffset = titleOffset; // used in getMargins
14405
14406 if (!this.isDirtyBox && requiresDirtyBox) {
14407 this.isDirtyBox = requiresDirtyBox;
14408 // Redraw if necessary (#2719, #2744)
14409 if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) {
14410 this.redraw();
14411 }
14412 }
14413 },
14414
14415 /**
14416 * Get chart width and height according to options and container size
14417 */
14418 getChartSize: function() {
14419 var chart = this,
14420 optionsChart = chart.options.chart,
14421 widthOption = optionsChart.width,
14422 heightOption = optionsChart.height,
14423 renderTo = chart.renderToClone || chart.renderTo;
14424
14425 // Get inner width and height
14426 if (!defined(widthOption)) {
14427 chart.containerWidth = getStyle(renderTo, 'width');
14428 }
14429 if (!defined(heightOption)) {
14430 chart.containerHeight = getStyle(renderTo, 'height');
14431 }
14432
14433 chart.chartWidth = Math.max(0, widthOption || chart.containerWidth || 600); // #1393, 1460
14434 chart.chartHeight = Math.max(0, pick(heightOption,
14435 // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7:
14436 chart.containerHeight > 19 ? chart.containerHeight : 400));
14437 },
14438
14439 /**
14440 * Create a clone of the chart's renderTo div and place it outside the viewport to allow
14441 * size computation on chart.render and chart.redraw
14442 */
14443 cloneRenderTo: function(revert) {
14444 var clone = this.renderToClone,
14445 container = this.container;
14446
14447 // Destroy the clone and bring the container back to the real renderTo div
14448 if (revert) {
14449 if (clone) {
14450 while (clone.childNodes.length) { // #5231
14451 this.renderTo.appendChild(clone.firstChild);
14452 }
14453 discardElement(clone);
14454 delete this.renderToClone;
14455 }
14456
14457 // Set up the clone
14458 } else {
14459 if (container && container.parentNode === this.renderTo) {
14460 this.renderTo.removeChild(container); // do not clone this
14461 }
14462 this.renderToClone = clone = this.renderTo.cloneNode(0);
14463 css(clone, {
14464 position: 'absolute',
14465 top: '-9999px',
14466 display: 'block' // #833
14467 });
14468 if (clone.style.setProperty) { // #2631
14469 clone.style.setProperty('display', 'block', 'important');
14470 }
14471 doc.body.appendChild(clone);
14472 if (container) {
14473 clone.appendChild(container);
14474 }
14475 }
14476 },
14477
14478 /**
14479 * Setter for the chart class name
14480 */
14481 setClassName: function(className) {
14482 this.container.className = 'highcharts-container ' + (className || '');
14483 },
14484
14485 /**
14486 * Get the containing element, determine the size and create the inner container
14487 * div to hold the chart
14488 */
14489 getContainer: function() {
14490 var chart = this,
14491 container,
14492 options = chart.options,
14493 optionsChart = options.chart,
14494 chartWidth,
14495 chartHeight,
14496 renderTo = chart.renderTo,
14497 indexAttrName = 'data-highcharts-chart',
14498 oldChartIndex,
14499 Ren,
14500 containerId = H.uniqueKey(),
14501 containerStyle,
14502 key;
14503
14504 if (!renderTo) {
14505 chart.renderTo = renderTo = optionsChart.renderTo;
14506 }
14507
14508 if (isString(renderTo)) {
14509 chart.renderTo = renderTo = doc.getElementById(renderTo);
14510 }
14511
14512 // Display an error if the renderTo is wrong
14513 if (!renderTo) {
14514 error(13, true);
14515 }
14516
14517 // If the container already holds a chart, destroy it. The check for hasRendered is there
14518 // because web pages that are saved to disk from the browser, will preserve the data-highcharts-chart
14519 // attribute and the SVG contents, but not an interactive chart. So in this case,
14520 // charts[oldChartIndex] will point to the wrong chart if any (#2609).
14521 oldChartIndex = pInt(attr(renderTo, indexAttrName));
14522 if (isNumber(oldChartIndex) && charts[oldChartIndex] && charts[oldChartIndex].hasRendered) {
14523 charts[oldChartIndex].destroy();
14524 }
14525
14526 // Make a reference to the chart from the div
14527 attr(renderTo, indexAttrName, chart.index);
14528
14529 // remove previous chart
14530 renderTo.innerHTML = '';
14531
14532 // If the container doesn't have an offsetWidth, it has or is a child of
14533 // a node that has display:none. We need to temporarily move it out to a
14534 // visible state to determine the size, else the legend and tooltips
14535 // won't render properly. The skipClone option is used in sparklines as
14536 // a micro optimization, saving about 1-2 ms each chart.
14537 if (!optionsChart.skipClone && !renderTo.offsetWidth) {
14538 chart.cloneRenderTo();
14539 }
14540
14541 // get the width and height
14542 chart.getChartSize();
14543 chartWidth = chart.chartWidth;
14544 chartHeight = chart.chartHeight;
14545
14546 // Create the inner container
14547
14548 containerStyle = extend({
14549 position: 'relative',
14550 overflow: 'hidden', // needed for context menu (avoid scrollbars) and
14551 // content overflow in IE
14552 width: chartWidth + 'px',
14553 height: chartHeight + 'px',
14554 textAlign: 'left',
14555 lineHeight: 'normal', // #427
14556 zIndex: 0, // #1072
14557 '-webkit-tap-highlight-color': 'rgba(0,0,0,0)'
14558 }, optionsChart.style);
14559
14560 chart.container = container = createElement(
14561 'div', {
14562 id: containerId
14563 },
14564 containerStyle,
14565 chart.renderToClone || renderTo
14566 );
14567
14568 // cache the cursor (#1650)
14569 chart._cursor = container.style.cursor;
14570
14571 // Initialize the renderer
14572 Ren = H[optionsChart.renderer] || Renderer;
14573 chart.renderer = new Ren(
14574 container,
14575 chartWidth,
14576 chartHeight,
14577 null,
14578 optionsChart.forExport,
14579 options.exporting && options.exporting.allowHTML
14580 );
14581
14582
14583 chart.setClassName(optionsChart.className);
14584
14585 chart.renderer.setStyle(optionsChart.style);
14586
14587
14588 // Add a reference to the charts index
14589 chart.renderer.chartIndex = chart.index;
14590 },
14591
14592 /**
14593 * Calculate margins by rendering axis labels in a preliminary position. Title,
14594 * subtitle and legend have already been rendered at this stage, but will be
14595 * moved into their final positions
14596 */
14597 getMargins: function(skipAxes) {
14598 var chart = this,
14599 spacing = chart.spacing,
14600 margin = chart.margin,
14601 titleOffset = chart.titleOffset;
14602
14603 chart.resetMargins();
14604
14605 // Adjust for title and subtitle
14606 if (titleOffset && !defined(margin[0])) {
14607 chart.plotTop = Math.max(chart.plotTop, titleOffset + chart.options.title.margin + spacing[0]);
14608 }
14609
14610 // Adjust for legend
14611 if (chart.legend.display) {
14612 chart.legend.adjustMargins(margin, spacing);
14613 }
14614
14615 // adjust for scroller
14616 if (chart.extraBottomMargin) {
14617 chart.marginBottom += chart.extraBottomMargin;
14618 }
14619 if (chart.extraTopMargin) {
14620 chart.plotTop += chart.extraTopMargin;
14621 }
14622 if (!skipAxes) {
14623 this.getAxisMargins();
14624 }
14625 },
14626
14627 getAxisMargins: function() {
14628
14629 var chart = this,
14630 axisOffset = chart.axisOffset = [0, 0, 0, 0], // top, right, bottom, left
14631 margin = chart.margin;
14632
14633 // pre-render axes to get labels offset width
14634 if (chart.hasCartesianSeries) {
14635 each(chart.axes, function(axis) {
14636 if (axis.visible) {
14637 axis.getOffset();
14638 }
14639 });
14640 }
14641
14642 // Add the axis offsets
14643 each(marginNames, function(m, side) {
14644 if (!defined(margin[side])) {
14645 chart[m] += axisOffset[side];
14646 }
14647 });
14648
14649 chart.setChartSize();
14650
14651 },
14652
14653 /**
14654 * Resize the chart to its container if size is not explicitly set
14655 */
14656 reflow: function(e) {
14657 var chart = this,
14658 optionsChart = chart.options.chart,
14659 renderTo = chart.renderTo,
14660 hasUserWidth = defined(optionsChart.width),
14661 width = optionsChart.width || getStyle(renderTo, 'width'),
14662 height = optionsChart.height || getStyle(renderTo, 'height'),
14663 target = e ? e.target : win;
14664
14665 // Width and height checks for display:none. Target is doc in IE8 and Opera,
14666 // win in Firefox, Chrome and IE9.
14667 if (!hasUserWidth && !chart.isPrinting && width && height && (target === win || target === doc)) { // #1093
14668 if (width !== chart.containerWidth || height !== chart.containerHeight) {
14669 clearTimeout(chart.reflowTimeout);
14670 // When called from window.resize, e is set, else it's called directly (#2224)
14671 chart.reflowTimeout = syncTimeout(function() {
14672 if (chart.container) { // It may have been destroyed in the meantime (#1257)
14673 chart.setSize(undefined, undefined, false);
14674 }
14675 }, e ? 100 : 0);
14676 }
14677 chart.containerWidth = width;
14678 chart.containerHeight = height;
14679 }
14680 },
14681
14682 /**
14683 * Add the event handlers necessary for auto resizing
14684 */
14685 initReflow: function() {
14686 var chart = this,
14687 unbind;
14688
14689 unbind = addEvent(win, 'resize', function(e) {
14690 chart.reflow(e);
14691 });
14692 addEvent(chart, 'destroy', unbind);
14693
14694 // The following will add listeners to re-fit the chart before and after
14695 // printing (#2284). However it only works in WebKit. Should have worked
14696 // in Firefox, but not supported in IE.
14697 /*
14698 if (win.matchMedia) {
14699 win.matchMedia('print').addListener(function reflow() {
14700 chart.reflow();
14701 });
14702 }
14703 */
14704 },
14705
14706 /**
14707 * Resize the chart to a given width and height
14708 * @param {Number} width
14709 * @param {Number} height
14710 * @param {Object|Boolean} animation
14711 */
14712 setSize: function(width, height, animation) {
14713 var chart = this,
14714 renderer = chart.renderer,
14715 globalAnimation;
14716
14717 // Handle the isResizing counter
14718 chart.isResizing += 1;
14719
14720 // set the animation for the current process
14721 H.setAnimation(animation, chart);
14722
14723 chart.oldChartHeight = chart.chartHeight;
14724 chart.oldChartWidth = chart.chartWidth;
14725 if (width !== undefined) {
14726 chart.options.chart.width = width;
14727 }
14728 if (height !== undefined) {
14729 chart.options.chart.height = height;
14730 }
14731 chart.getChartSize();
14732
14733 // Resize the container with the global animation applied if enabled (#2503)
14734
14735 globalAnimation = renderer.globalAnimation;
14736 (globalAnimation ? animate : css)(chart.container, {
14737 width: chart.chartWidth + 'px',
14738 height: chart.chartHeight + 'px'
14739 }, globalAnimation);
14740
14741
14742 chart.setChartSize(true);
14743 renderer.setSize(chart.chartWidth, chart.chartHeight, animation);
14744
14745 // handle axes
14746 each(chart.axes, function(axis) {
14747 axis.isDirty = true;
14748 axis.setScale();
14749 });
14750
14751 chart.isDirtyLegend = true; // force legend redraw
14752 chart.isDirtyBox = true; // force redraw of plot and chart border
14753
14754 chart.layOutTitles(); // #2857
14755 chart.getMargins();
14756
14757 if (chart.setResponsive) {
14758 chart.setResponsive(false);
14759 }
14760 chart.redraw(animation);
14761
14762
14763 chart.oldChartHeight = null;
14764 fireEvent(chart, 'resize');
14765
14766 // Fire endResize and set isResizing back. If animation is disabled, fire without delay
14767 syncTimeout(function() {
14768 if (chart) {
14769 fireEvent(chart, 'endResize', null, function() {
14770 chart.isResizing -= 1;
14771 });
14772 }
14773 }, animObject(globalAnimation).duration);
14774 },
14775
14776 /**
14777 * Set the public chart properties. This is done before and after the pre-render
14778 * to determine margin sizes
14779 */
14780 setChartSize: function(skipAxes) {
14781 var chart = this,
14782 inverted = chart.inverted,
14783 renderer = chart.renderer,
14784 chartWidth = chart.chartWidth,
14785 chartHeight = chart.chartHeight,
14786 optionsChart = chart.options.chart,
14787 spacing = chart.spacing,
14788 clipOffset = chart.clipOffset,
14789 clipX,
14790 clipY,
14791 plotLeft,
14792 plotTop,
14793 plotWidth,
14794 plotHeight,
14795 plotBorderWidth;
14796
14797 chart.plotLeft = plotLeft = Math.round(chart.plotLeft);
14798 chart.plotTop = plotTop = Math.round(chart.plotTop);
14799 chart.plotWidth = plotWidth = Math.max(0, Math.round(chartWidth - plotLeft - chart.marginRight));
14800 chart.plotHeight = plotHeight = Math.max(0, Math.round(chartHeight - plotTop - chart.marginBottom));
14801
14802 chart.plotSizeX = inverted ? plotHeight : plotWidth;
14803 chart.plotSizeY = inverted ? plotWidth : plotHeight;
14804
14805 chart.plotBorderWidth = optionsChart.plotBorderWidth || 0;
14806
14807 // Set boxes used for alignment
14808 chart.spacingBox = renderer.spacingBox = {
14809 x: spacing[3],
14810 y: spacing[0],
14811 width: chartWidth - spacing[3] - spacing[1],
14812 height: chartHeight - spacing[0] - spacing[2]
14813 };
14814 chart.plotBox = renderer.plotBox = {
14815 x: plotLeft,
14816 y: plotTop,
14817 width: plotWidth,
14818 height: plotHeight
14819 };
14820
14821 plotBorderWidth = 2 * Math.floor(chart.plotBorderWidth / 2);
14822 clipX = Math.ceil(Math.max(plotBorderWidth, clipOffset[3]) / 2);
14823 clipY = Math.ceil(Math.max(plotBorderWidth, clipOffset[0]) / 2);
14824 chart.clipBox = {
14825 x: clipX,
14826 y: clipY,
14827 width: Math.floor(chart.plotSizeX - Math.max(plotBorderWidth, clipOffset[1]) / 2 - clipX),
14828 height: Math.max(0, Math.floor(chart.plotSizeY - Math.max(plotBorderWidth, clipOffset[2]) / 2 - clipY))
14829 };
14830
14831 if (!skipAxes) {
14832 each(chart.axes, function(axis) {
14833 axis.setAxisSize();
14834 axis.setAxisTranslation();
14835 });
14836 }
14837 },
14838
14839 /**
14840 * Initial margins before auto size margins are applied
14841 */
14842 resetMargins: function() {
14843 var chart = this,
14844 chartOptions = chart.options.chart;
14845
14846 // Create margin and spacing array
14847 each(['margin', 'spacing'], function splashArrays(target) {
14848 var value = chartOptions[target],
14849 values = isObject(value) ? value : [value, value, value, value];
14850
14851 each(['Top', 'Right', 'Bottom', 'Left'], function(sideName, side) {
14852 chart[target][side] = pick(chartOptions[target + sideName], values[side]);
14853 });
14854 });
14855
14856 // Set margin names like chart.plotTop, chart.plotLeft, chart.marginRight, chart.marginBottom.
14857 each(marginNames, function(m, side) {
14858 chart[m] = pick(chart.margin[side], chart.spacing[side]);
14859 });
14860 chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left
14861 chart.clipOffset = [0, 0, 0, 0];
14862 },
14863
14864 /**
14865 * Draw the borders and backgrounds for chart and plot area
14866 */
14867 drawChartBox: function() {
14868 var chart = this,
14869 optionsChart = chart.options.chart,
14870 renderer = chart.renderer,
14871 chartWidth = chart.chartWidth,
14872 chartHeight = chart.chartHeight,
14873 chartBackground = chart.chartBackground,
14874 plotBackground = chart.plotBackground,
14875 plotBorder = chart.plotBorder,
14876 chartBorderWidth,
14877
14878 plotBGImage = chart.plotBGImage,
14879 chartBackgroundColor = optionsChart.backgroundColor,
14880 plotBackgroundColor = optionsChart.plotBackgroundColor,
14881 plotBackgroundImage = optionsChart.plotBackgroundImage,
14882
14883 mgn,
14884 bgAttr,
14885 plotLeft = chart.plotLeft,
14886 plotTop = chart.plotTop,
14887 plotWidth = chart.plotWidth,
14888 plotHeight = chart.plotHeight,
14889 plotBox = chart.plotBox,
14890 clipRect = chart.clipRect,
14891 clipBox = chart.clipBox,
14892 verb = 'animate';
14893
14894 // Chart area
14895 if (!chartBackground) {
14896 chart.chartBackground = chartBackground = renderer.rect()
14897 .addClass('highcharts-background')
14898 .add();
14899 verb = 'attr';
14900 }
14901
14902
14903 // Presentational
14904 chartBorderWidth = optionsChart.borderWidth || 0;
14905 mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0);
14906
14907 bgAttr = {
14908 fill: chartBackgroundColor || 'none'
14909 };
14910
14911 if (chartBorderWidth || chartBackground['stroke-width']) { // #980
14912 bgAttr.stroke = optionsChart.borderColor;
14913 bgAttr['stroke-width'] = chartBorderWidth;
14914 }
14915 chartBackground
14916 .attr(bgAttr)
14917 .shadow(optionsChart.shadow);
14918
14919 chartBackground[verb]({
14920 x: mgn / 2,
14921 y: mgn / 2,
14922 width: chartWidth - mgn - chartBorderWidth % 2,
14923 height: chartHeight - mgn - chartBorderWidth % 2,
14924 r: optionsChart.borderRadius
14925 });
14926
14927 // Plot background
14928 verb = 'animate';
14929 if (!plotBackground) {
14930 verb = 'attr';
14931 chart.plotBackground = plotBackground = renderer.rect()
14932 .addClass('highcharts-plot-background')
14933 .add();
14934 }
14935 plotBackground[verb](plotBox);
14936
14937
14938 // Presentational attributes for the background
14939 plotBackground
14940 .attr({
14941 fill: plotBackgroundColor || 'none'
14942 })
14943 .shadow(optionsChart.plotShadow);
14944
14945 // Create the background image
14946 if (plotBackgroundImage) {
14947 if (!plotBGImage) {
14948 chart.plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight)
14949 .add();
14950 } else {
14951 plotBGImage.animate(plotBox);
14952 }
14953 }
14954
14955
14956 // Plot clip
14957 if (!clipRect) {
14958 chart.clipRect = renderer.clipRect(clipBox);
14959 } else {
14960 clipRect.animate({
14961 width: clipBox.width,
14962 height: clipBox.height
14963 });
14964 }
14965
14966 // Plot area border
14967 verb = 'animate';
14968 if (!plotBorder) {
14969 verb = 'attr';
14970 chart.plotBorder = plotBorder = renderer.rect()
14971 .addClass('highcharts-plot-border')
14972 .attr({
14973 zIndex: 1 // Above the grid
14974 })
14975 .add();
14976 }
14977
14978
14979 // Presentational
14980 plotBorder.attr({
14981 stroke: optionsChart.plotBorderColor,
14982 'stroke-width': optionsChart.plotBorderWidth || 0,
14983 fill: 'none'
14984 });
14985
14986
14987 plotBorder[verb](plotBorder.crisp({
14988 x: plotLeft,
14989 y: plotTop,
14990 width: plotWidth,
14991 height: plotHeight
14992 }, -plotBorder.strokeWidth())); //#3282 plotBorder should be negative;
14993
14994 // reset
14995 chart.isDirtyBox = false;
14996 },
14997
14998 /**
14999 * Detect whether a certain chart property is needed based on inspecting its options
15000 * and series. This mainly applies to the chart.inverted property, and in extensions to
15001 * the chart.angular and chart.polar properties.
15002 */
15003 propFromSeries: function() {
15004 var chart = this,
15005 optionsChart = chart.options.chart,
15006 klass,
15007 seriesOptions = chart.options.series,
15008 i,
15009 value;
15010
15011
15012 each(['inverted', 'angular', 'polar'], function(key) {
15013
15014 // The default series type's class
15015 klass = seriesTypes[optionsChart.type || optionsChart.defaultSeriesType];
15016
15017 // Get the value from available chart-wide properties
15018 value =
15019 optionsChart[key] || // It is set in the options
15020 (klass && klass.prototype[key]); // The default series class requires it
15021
15022 // 4. Check if any the chart's series require it
15023 i = seriesOptions && seriesOptions.length;
15024 while (!value && i--) {
15025 klass = seriesTypes[seriesOptions[i].type];
15026 if (klass && klass.prototype[key]) {
15027 value = true;
15028 }
15029 }
15030
15031 // Set the chart property
15032 chart[key] = value;
15033 });
15034
15035 },
15036
15037 /**
15038 * Link two or more series together. This is done initially from Chart.render,
15039 * and after Chart.addSeries and Series.remove.
15040 */
15041 linkSeries: function() {
15042 var chart = this,
15043 chartSeries = chart.series;
15044
15045 // Reset links
15046 each(chartSeries, function(series) {
15047 series.linkedSeries.length = 0;
15048 });
15049
15050 // Apply new links
15051 each(chartSeries, function(series) {
15052 var linkedTo = series.options.linkedTo;
15053 if (isString(linkedTo)) {
15054 if (linkedTo === ':previous') {
15055 linkedTo = chart.series[series.index - 1];
15056 } else {
15057 linkedTo = chart.get(linkedTo);
15058 }
15059 if (linkedTo && linkedTo.linkedParent !== series) { // #3341 avoid mutual linking
15060 linkedTo.linkedSeries.push(series);
15061 series.linkedParent = linkedTo;
15062 series.visible = pick(series.options.visible, linkedTo.options.visible, series.visible); // #3879
15063 }
15064 }
15065 });
15066 },
15067
15068 /**
15069 * Render series for the chart
15070 */
15071 renderSeries: function() {
15072 each(this.series, function(serie) {
15073 serie.translate();
15074 serie.render();
15075 });
15076 },
15077
15078 /**
15079 * Render labels for the chart
15080 */
15081 renderLabels: function() {
15082 var chart = this,
15083 labels = chart.options.labels;
15084 if (labels.items) {
15085 each(labels.items, function(label) {
15086 var style = extend(labels.style, label.style),
15087 x = pInt(style.left) + chart.plotLeft,
15088 y = pInt(style.top) + chart.plotTop + 12;
15089
15090 // delete to prevent rewriting in IE
15091 delete style.left;
15092 delete style.top;
15093
15094 chart.renderer.text(
15095 label.html,
15096 x,
15097 y
15098 )
15099 .attr({
15100 zIndex: 2
15101 })
15102 .css(style)
15103 .add();
15104
15105 });
15106 }
15107 },
15108
15109 /**
15110 * Render all graphics for the chart
15111 */
15112 render: function() {
15113 var chart = this,
15114 axes = chart.axes,
15115 renderer = chart.renderer,
15116 options = chart.options,
15117 tempWidth,
15118 tempHeight,
15119 redoHorizontal,
15120 redoVertical;
15121
15122 // Title
15123 chart.setTitle();
15124
15125
15126 // Legend
15127 chart.legend = new Legend(chart, options.legend);
15128
15129 // Get stacks
15130 if (chart.getStacks) {
15131 chart.getStacks();
15132 }
15133
15134 // Get chart margins
15135 chart.getMargins(true);
15136 chart.setChartSize();
15137
15138 // Record preliminary dimensions for later comparison
15139 tempWidth = chart.plotWidth;
15140 tempHeight = chart.plotHeight = chart.plotHeight - 21; // 21 is the most common correction for X axis labels
15141
15142 // Get margins by pre-rendering axes
15143 each(axes, function(axis) {
15144 axis.setScale();
15145 });
15146 chart.getAxisMargins();
15147
15148 // If the plot area size has changed significantly, calculate tick positions again
15149 redoHorizontal = tempWidth / chart.plotWidth > 1.1;
15150 redoVertical = tempHeight / chart.plotHeight > 1.05; // Height is more sensitive
15151
15152 if (redoHorizontal || redoVertical) {
15153
15154 each(axes, function(axis) {
15155 if ((axis.horiz && redoHorizontal) || (!axis.horiz && redoVertical)) {
15156 axis.setTickInterval(true); // update to reflect the new margins
15157 }
15158 });
15159 chart.getMargins(); // second pass to check for new labels
15160 }
15161
15162 // Draw the borders and backgrounds
15163 chart.drawChartBox();
15164
15165
15166 // Axes
15167 if (chart.hasCartesianSeries) {
15168 each(axes, function(axis) {
15169 if (axis.visible) {
15170 axis.render();
15171 }
15172 });
15173 }
15174
15175 // The series
15176 if (!chart.seriesGroup) {
15177 chart.seriesGroup = renderer.g('series-group')
15178 .attr({
15179 zIndex: 3
15180 })
15181 .add();
15182 }
15183 chart.renderSeries();
15184
15185 // Labels
15186 chart.renderLabels();
15187
15188 // Credits
15189 chart.addCredits();
15190
15191 // Handle responsiveness
15192 if (chart.setResponsive) {
15193 chart.setResponsive();
15194 }
15195
15196 // Set flag
15197 chart.hasRendered = true;
15198
15199 },
15200
15201 /**
15202 * Show chart credits based on config options
15203 */
15204 addCredits: function(credits) {
15205 var chart = this;
15206
15207 credits = merge(true, this.options.credits, credits);
15208 if (credits.enabled && !this.credits) {
15209 this.credits = this.renderer.text(
15210 credits.text + (this.mapCredits || ''),
15211 0,
15212 0
15213 )
15214 .addClass('highcharts-credits')
15215 .on('click', function() {
15216 if (credits.href) {
15217 win.location.href = credits.href;
15218 }
15219 })
15220 .attr({
15221 align: credits.position.align,
15222 zIndex: 8
15223 })
15224
15225 .css(credits.style)
15226
15227 .add()
15228 .align(credits.position);
15229
15230 // Dynamically update
15231 this.credits.update = function(options) {
15232 chart.credits = chart.credits.destroy();
15233 chart.addCredits(options);
15234 };
15235 }
15236 },
15237
15238 /**
15239 * Clean up memory usage
15240 */
15241 destroy: function() {
15242 var chart = this,
15243 axes = chart.axes,
15244 series = chart.series,
15245 container = chart.container,
15246 i,
15247 parentNode = container && container.parentNode;
15248
15249 // fire the chart.destoy event
15250 fireEvent(chart, 'destroy');
15251
15252 // Delete the chart from charts lookup array
15253 charts[chart.index] = undefined;
15254 H.chartCount--;
15255 chart.renderTo.removeAttribute('data-highcharts-chart');
15256
15257 // remove events
15258 removeEvent(chart);
15259
15260 // ==== Destroy collections:
15261 // Destroy axes
15262 i = axes.length;
15263 while (i--) {
15264 axes[i] = axes[i].destroy();
15265 }
15266
15267 // Destroy scroller & scroller series before destroying base series
15268 if (this.scroller && this.scroller.destroy) {
15269 this.scroller.destroy();
15270 }
15271
15272 // Destroy each series
15273 i = series.length;
15274 while (i--) {
15275 series[i] = series[i].destroy();
15276 }
15277
15278 // ==== Destroy chart properties:
15279 each(['title', 'subtitle', 'chartBackground', 'plotBackground', 'plotBGImage',
15280 'plotBorder', 'seriesGroup', 'clipRect', 'credits', 'pointer',
15281 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', 'renderer'
15282 ], function(name) {
15283 var prop = chart[name];
15284
15285 if (prop && prop.destroy) {
15286 chart[name] = prop.destroy();
15287 }
15288 });
15289
15290 // remove container and all SVG
15291 if (container) { // can break in IE when destroyed before finished loading
15292 container.innerHTML = '';
15293 removeEvent(container);
15294 if (parentNode) {
15295 discardElement(container);
15296 }
15297
15298 }
15299
15300 // clean it all up
15301 for (i in chart) {
15302 delete chart[i];
15303 }
15304
15305 },
15306
15307
15308 /**
15309 * VML namespaces can't be added until after complete. Listening
15310 * for Perini's doScroll hack is not enough.
15311 */
15312 isReadyToRender: function() {
15313 var chart = this;
15314
15315 // Note: win == win.top is required
15316 if ((!svg && (win == win.top && doc.readyState !== 'complete'))) { // eslint-disable-line eqeqeq
15317 doc.attachEvent('onreadystatechange', function() {
15318 doc.detachEvent('onreadystatechange', chart.firstRender);
15319 if (doc.readyState === 'complete') {
15320 chart.firstRender();
15321 }
15322 });
15323 return false;
15324 }
15325 return true;
15326 },
15327
15328 /**
15329 * Prepare for first rendering after all data are loaded
15330 */
15331 firstRender: function() {
15332 var chart = this,
15333 options = chart.options;
15334
15335 // Check whether the chart is ready to render
15336 if (!chart.isReadyToRender()) {
15337 return;
15338 }
15339
15340 // Create the container
15341 chart.getContainer();
15342
15343 // Run an early event after the container and renderer are established
15344 fireEvent(chart, 'init');
15345
15346
15347 chart.resetMargins();
15348 chart.setChartSize();
15349
15350 // Set the common chart properties (mainly invert) from the given series
15351 chart.propFromSeries();
15352
15353 // get axes
15354 chart.getAxes();
15355
15356 // Initialize the series
15357 each(options.series || [], function(serieOptions) {
15358 chart.initSeries(serieOptions);
15359 });
15360
15361 chart.linkSeries();
15362
15363 // Run an event after axes and series are initialized, but before render. At this stage,
15364 // the series data is indexed and cached in the xData and yData arrays, so we can access
15365 // those before rendering. Used in Highstock.
15366 fireEvent(chart, 'beforeRender');
15367
15368 // depends on inverted and on margins being set
15369 if (Pointer) {
15370 chart.pointer = new Pointer(chart, options);
15371 }
15372
15373 chart.render();
15374
15375 // add canvas
15376 chart.renderer.draw();
15377
15378 // Fire the load event if there are no external images
15379 if (!chart.renderer.imgCount && chart.onload) {
15380 chart.onload();
15381 }
15382
15383 // If the chart was rendered outside the top container, put it back in (#3679)
15384 chart.cloneRenderTo(true);
15385
15386 },
15387
15388 /**
15389 * On chart load
15390 */
15391 onload: function() {
15392
15393 // Run callbacks
15394 each([this.callback].concat(this.callbacks), function(fn) {
15395 if (fn && this.index !== undefined) { // Chart destroyed in its own callback (#3600)
15396 fn.apply(this, [this]);
15397 }
15398 }, this);
15399
15400 fireEvent(this, 'load');
15401
15402 // Set up auto resize
15403 if (this.options.chart.reflow !== false) {
15404 this.initReflow();
15405 }
15406
15407 // Don't run again
15408 this.onload = null;
15409 }
15410
15411 }; // end Chart
15412
15413 }(Highcharts));
15414 (function(H) {
15415 /**
15416 * (c) 2010-2016 Torstein Honsi
15417 *
15418 * License: www.highcharts.com/license
15419 */
15420 'use strict';
15421 var Point,
15422
15423 each = H.each,
15424 extend = H.extend,
15425 erase = H.erase,
15426 fireEvent = H.fireEvent,
15427 format = H.format,
15428 isArray = H.isArray,
15429 isNumber = H.isNumber,
15430 pick = H.pick,
15431 removeEvent = H.removeEvent;
15432
15433 /**
15434 * The Point object. The point objects are generated from the series.data
15435 * configuration objects or raw numbers. They can be accessed from the
15436 * Series.points array.
15437 * @constructor Point
15438 */
15439 Point = H.Point = function() {};
15440 Point.prototype = {
15441
15442 /**
15443 * Initialize the point. Called internally based on the series.data option.
15444 * @function #init
15445 * @memberOf Point
15446 * @param {Object} series The series object containing this point.
15447 * @param {Object} options The data in either number, array or object
15448 * format.
15449 * @param {Number} x Optionally, the X value of the.
15450 * @returns {Object} The Point instance.
15451 */
15452 init: function(series, options, x) {
15453
15454 var point = this,
15455 colors,
15456 colorCount = series.chart.options.chart.colorCount,
15457 colorIndex;
15458
15459 point.series = series;
15460
15461 point.color = series.color; // #3445
15462
15463 point.applyOptions(options, x);
15464
15465 if (series.options.colorByPoint) {
15466
15467 colors = series.options.colors || series.chart.options.colors;
15468 point.color = point.color || colors[series.colorCounter];
15469 colorCount = colors.length;
15470
15471 colorIndex = series.colorCounter;
15472 series.colorCounter++;
15473 // loop back to zero
15474 if (series.colorCounter === colorCount) {
15475 series.colorCounter = 0;
15476 }
15477 } else {
15478 colorIndex = series.colorIndex;
15479 }
15480 point.colorIndex = pick(point.colorIndex, colorIndex);
15481
15482 series.chart.pointCount++;
15483 return point;
15484 },
15485 /**
15486 * Apply the options containing the x and y data and possible some extra
15487 * properties. Called on point init or from point.update.
15488 *
15489 * @function #applyOptions
15490 * @memberOf Point
15491 * @param {Object} options The point options as defined in series.data.
15492 * @param {Number} x Optionally, the X value.
15493 * @returns {Object} The Point instance.
15494 */
15495 applyOptions: function(options, x) {
15496 var point = this,
15497 series = point.series,
15498 pointValKey = series.options.pointValKey || series.pointValKey;
15499
15500 options = Point.prototype.optionsToObject.call(this, options);
15501
15502 // copy options directly to point
15503 extend(point, options);
15504 point.options = point.options ? extend(point.options, options) : options;
15505
15506 // Since options are copied into the Point instance, some accidental options must be shielded (#5681)
15507 if (options.group) {
15508 delete point.group;
15509 }
15510
15511 // For higher dimension series types. For instance, for ranges, point.y is mapped to point.low.
15512 if (pointValKey) {
15513 point.y = point[pointValKey];
15514 }
15515 point.isNull = pick(
15516 point.isValid && !point.isValid(),
15517 point.x === null || !isNumber(point.y, true)
15518 ); // #3571, check for NaN
15519
15520 // The point is initially selected by options (#5777)
15521 if (point.selected) {
15522 point.state = 'select';
15523 }
15524
15525 // If no x is set by now, get auto incremented value. All points must have an
15526 // x value, however the y value can be null to create a gap in the series
15527 if ('name' in point && x === undefined && series.xAxis && series.xAxis.hasNames) {
15528 point.x = series.xAxis.nameToX(point);
15529 }
15530 if (point.x === undefined && series) {
15531 if (x === undefined) {
15532 point.x = series.autoIncrement(point);
15533 } else {
15534 point.x = x;
15535 }
15536 }
15537
15538 return point;
15539 },
15540
15541 /**
15542 * Transform number or array configs into objects
15543 */
15544 optionsToObject: function(options) {
15545 var ret = {},
15546 series = this.series,
15547 keys = series.options.keys,
15548 pointArrayMap = keys || series.pointArrayMap || ['y'],
15549 valueCount = pointArrayMap.length,
15550 firstItemType,
15551 i = 0,
15552 j = 0;
15553
15554 if (isNumber(options) || options === null) {
15555 ret[pointArrayMap[0]] = options;
15556
15557 } else if (isArray(options)) {
15558 // with leading x value
15559 if (!keys && options.length > valueCount) {
15560 firstItemType = typeof options[0];
15561 if (firstItemType === 'string') {
15562 ret.name = options[0];
15563 } else if (firstItemType === 'number') {
15564 ret.x = options[0];
15565 }
15566 i++;
15567 }
15568 while (j < valueCount) {
15569 if (!keys || options[i] !== undefined) { // Skip undefined positions for keys
15570 ret[pointArrayMap[j]] = options[i];
15571 }
15572 i++;
15573 j++;
15574 }
15575 } else if (typeof options === 'object') {
15576 ret = options;
15577
15578 // This is the fastest way to detect if there are individual point dataLabels that need
15579 // to be considered in drawDataLabels. These can only occur in object configs.
15580 if (options.dataLabels) {
15581 series._hasPointLabels = true;
15582 }
15583
15584 // Same approach as above for markers
15585 if (options.marker) {
15586 series._hasPointMarkers = true;
15587 }
15588 }
15589 return ret;
15590 },
15591
15592 /**
15593 * Get the CSS class names for individual points
15594 * @returns {String} The class name
15595 */
15596 getClassName: function() {
15597 return 'highcharts-point' +
15598 (this.selected ? ' highcharts-point-select' : '') +
15599 (this.negative ? ' highcharts-negative' : '') +
15600 (this.isNull ? ' highcharts-null-point' : '') +
15601 (this.colorIndex !== undefined ? ' highcharts-color-' + this.colorIndex : '') +
15602 (this.options.className ? ' ' + this.options.className : '');
15603 },
15604
15605 /**
15606 * Return the zone that the point belongs to
15607 */
15608 getZone: function() {
15609 var series = this.series,
15610 zones = series.zones,
15611 zoneAxis = series.zoneAxis || 'y',
15612 i = 0,
15613 zone;
15614
15615 zone = zones[i];
15616 while (this[zoneAxis] >= zone.value) {
15617 zone = zones[++i];
15618 }
15619
15620 if (zone && zone.color && !this.options.color) {
15621 this.color = zone.color;
15622 }
15623
15624 return zone;
15625 },
15626
15627 /**
15628 * Destroy a point to clear memory. Its reference still stays in series.data.
15629 */
15630 destroy: function() {
15631 var point = this,
15632 series = point.series,
15633 chart = series.chart,
15634 hoverPoints = chart.hoverPoints,
15635 prop;
15636
15637 chart.pointCount--;
15638
15639 if (hoverPoints) {
15640 point.setState();
15641 erase(hoverPoints, point);
15642 if (!hoverPoints.length) {
15643 chart.hoverPoints = null;
15644 }
15645
15646 }
15647 if (point === chart.hoverPoint) {
15648 point.onMouseOut();
15649 }
15650
15651 // remove all events
15652 if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive
15653 removeEvent(point);
15654 point.destroyElements();
15655 }
15656
15657 if (point.legendItem) { // pies have legend items
15658 chart.legend.destroyItem(point);
15659 }
15660
15661 for (prop in point) {
15662 point[prop] = null;
15663 }
15664
15665
15666 },
15667
15668 /**
15669 * Destroy SVG elements associated with the point
15670 */
15671 destroyElements: function() {
15672 var point = this,
15673 props = ['graphic', 'dataLabel', 'dataLabelUpper', 'connector', 'shadowGroup'],
15674 prop,
15675 i = 6;
15676 while (i--) {
15677 prop = props[i];
15678 if (point[prop]) {
15679 point[prop] = point[prop].destroy();
15680 }
15681 }
15682 },
15683
15684 /**
15685 * Return the configuration hash needed for the data label and tooltip formatters
15686 */
15687 getLabelConfig: function() {
15688 return {
15689 x: this.category,
15690 y: this.y,
15691 color: this.color,
15692 key: this.name || this.category,
15693 series: this.series,
15694 point: this,
15695 percentage: this.percentage,
15696 total: this.total || this.stackTotal
15697 };
15698 },
15699
15700 /**
15701 * Extendable method for formatting each point's tooltip line
15702 *
15703 * @return {String} A string to be concatenated in to the common tooltip text
15704 */
15705 tooltipFormatter: function(pointFormat) {
15706
15707 // Insert options for valueDecimals, valuePrefix, and valueSuffix
15708 var series = this.series,
15709 seriesTooltipOptions = series.tooltipOptions,
15710 valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''),
15711 valuePrefix = seriesTooltipOptions.valuePrefix || '',
15712 valueSuffix = seriesTooltipOptions.valueSuffix || '';
15713
15714 // Loop over the point array map and replace unformatted values with sprintf formatting markup
15715 each(series.pointArrayMap || ['y'], function(key) {
15716 key = '{point.' + key; // without the closing bracket
15717 if (valuePrefix || valueSuffix) {
15718 pointFormat = pointFormat.replace(key + '}', valuePrefix + key + '}' + valueSuffix);
15719 }
15720 pointFormat = pointFormat.replace(key + '}', key + ':,.' + valueDecimals + 'f}');
15721 });
15722
15723 return format(pointFormat, {
15724 point: this,
15725 series: this.series
15726 });
15727 },
15728
15729 /**
15730 * Fire an event on the Point object.
15731 * @param {String} eventType
15732 * @param {Object} eventArgs Additional event arguments
15733 * @param {Function} defaultFunction Default event handler
15734 */
15735 firePointEvent: function(eventType, eventArgs, defaultFunction) {
15736 var point = this,
15737 series = this.series,
15738 seriesOptions = series.options;
15739
15740 // load event handlers on demand to save time on mouseover/out
15741 if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) {
15742 this.importEvents();
15743 }
15744
15745 // add default handler if in selection mode
15746 if (eventType === 'click' && seriesOptions.allowPointSelect) {
15747 defaultFunction = function(event) {
15748 // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera
15749 if (point.select) { // Could be destroyed by prior event handlers (#2911)
15750 point.select(null, event.ctrlKey || event.metaKey || event.shiftKey);
15751 }
15752 };
15753 }
15754
15755 fireEvent(this, eventType, eventArgs, defaultFunction);
15756 },
15757 visible: true
15758 };
15759
15760 }(Highcharts));
15761 (function(H) {
15762 /**
15763 * (c) 2010-2016 Torstein Honsi
15764 *
15765 * License: www.highcharts.com/license
15766 */
15767 'use strict';
15768 var addEvent = H.addEvent,
15769 animObject = H.animObject,
15770 arrayMax = H.arrayMax,
15771 arrayMin = H.arrayMin,
15772 correctFloat = H.correctFloat,
15773 Date = H.Date,
15774 defaultOptions = H.defaultOptions,
15775 defaultPlotOptions = H.defaultPlotOptions,
15776 defined = H.defined,
15777 each = H.each,
15778 erase = H.erase,
15779 error = H.error,
15780 extend = H.extend,
15781 fireEvent = H.fireEvent,
15782 grep = H.grep,
15783 isArray = H.isArray,
15784 isNumber = H.isNumber,
15785 isString = H.isString,
15786 LegendSymbolMixin = H.LegendSymbolMixin, // @todo add as a requirement
15787 merge = H.merge,
15788 pick = H.pick,
15789 Point = H.Point, // @todo add as a requirement
15790 removeEvent = H.removeEvent,
15791 splat = H.splat,
15792 stableSort = H.stableSort,
15793 SVGElement = H.SVGElement,
15794 syncTimeout = H.syncTimeout,
15795 win = H.win;
15796
15797 /**
15798 * The base function which all other series types inherit from. The data in the series is stored
15799 * in various arrays.
15800 *
15801 * - First, series.options.data contains all the original config options for
15802 * each point whether added by options or methods like series.addPoint.
15803 * - Next, series.data contains those values converted to points, but in case the series data length
15804 * exceeds the cropThreshold, or if the data is grouped, series.data doesn't contain all the points. It
15805 * only contains the points that have been created on demand.
15806 * - Then there's series.points that contains all currently visible point objects. In case of cropping,
15807 * the cropped-away points are not part of this array. The series.points array starts at series.cropStart
15808 * compared to series.data and series.options.data. If however the series data is grouped, these can't
15809 * be correlated one to one.
15810 * - series.xData and series.processedXData contain clean x values, equivalent to series.data and series.points.
15811 * - series.yData and series.processedYData contain clean y values, equivalent to series.data and series.points.
15812 *
15813 * @constructor Series
15814 * @param {Object} chart - The chart instance.
15815 * @param {Object} options - The series options.
15816 */
15817 H.Series = H.seriesType('line', null, { // base series options
15818
15819 //cursor: 'default',
15820 //dashStyle: null,
15821 //linecap: 'round',
15822 lineWidth: 2,
15823 //shadow: false,
15824
15825 allowPointSelect: false,
15826 showCheckbox: false,
15827 animation: {
15828 duration: 1000
15829 },
15830 //clip: true,
15831 //connectNulls: false,
15832 //enableMouseTracking: true,
15833 events: {},
15834 //legendIndex: 0,
15835 // stacking: null,
15836 marker: {
15837
15838 lineWidth: 0,
15839 lineColor: '#ffffff',
15840 //fillColor: null,
15841
15842 //enabled: true,
15843 //symbol: null,
15844 radius: 4,
15845 states: { // states for a single point
15846 hover: {
15847 animation: {
15848 duration: 50
15849 },
15850 enabled: true,
15851 radiusPlus: 2,
15852
15853 lineWidthPlus: 1
15854
15855 },
15856
15857 select: {
15858 fillColor: '#cccccc',
15859 lineColor: '#000000',
15860 lineWidth: 2
15861 }
15862
15863 }
15864 },
15865 point: {
15866 events: {}
15867 },
15868 dataLabels: {
15869 align: 'center',
15870 // defer: true,
15871 // enabled: false,
15872 formatter: function() {
15873 return this.y === null ? '' : H.numberFormat(this.y, -1);
15874 },
15875
15876 style: {
15877 fontSize: '11px',
15878 fontWeight: 'bold',
15879 color: 'contrast',
15880 textOutline: '1px contrast'
15881 },
15882 // backgroundColor: undefined,
15883 // borderColor: undefined,
15884 // borderWidth: undefined,
15885 // shadow: false
15886
15887 verticalAlign: 'bottom', // above singular point
15888 x: 0,
15889 y: 0,
15890 // borderRadius: undefined,
15891 padding: 5
15892 },
15893 cropThreshold: 300, // draw points outside the plot area when the number of points is less than this
15894 pointRange: 0,
15895 //pointStart: 0,
15896 //pointInterval: 1,
15897 //showInLegend: null, // auto: true for standalone series, false for linked series
15898 softThreshold: true,
15899 states: { // states for the entire series
15900 hover: {
15901 //enabled: false,
15902 lineWidthPlus: 1,
15903 marker: {
15904 // lineWidth: base + 1,
15905 // radius: base + 1
15906 },
15907 halo: {
15908 size: 10,
15909
15910 opacity: 0.25
15911
15912 }
15913 },
15914 select: {
15915 marker: {}
15916 }
15917 },
15918 stickyTracking: true,
15919 //tooltip: {
15920 //pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b>'
15921 //valueDecimals: null,
15922 //xDateFormat: '%A, %b %e, %Y',
15923 //valuePrefix: '',
15924 //ySuffix: ''
15925 //}
15926 turboThreshold: 1000
15927 // zIndex: null
15928
15929
15930 }, /** @lends Series.prototype */ {
15931 isCartesian: true,
15932 pointClass: Point,
15933 sorted: true, // requires the data to be sorted
15934 requireSorting: true,
15935 directTouch: false,
15936 axisTypes: ['xAxis', 'yAxis'],
15937 colorCounter: 0,
15938 parallelArrays: ['x', 'y'], // each point's x and y values are stored in this.xData and this.yData
15939 coll: 'series',
15940 init: function(chart, options) {
15941 var series = this,
15942 eventType,
15943 events,
15944 chartSeries = chart.series,
15945 lastSeries,
15946 sortByIndex = function(a, b) {
15947 return pick(a.options.index, a._i) - pick(b.options.index, b._i);
15948 };
15949
15950 series.chart = chart;
15951 series.options = options = series.setOptions(options); // merge with plotOptions
15952 series.linkedSeries = [];
15953
15954 // bind the axes
15955 series.bindAxes();
15956
15957 // set some variables
15958 extend(series, {
15959 name: options.name,
15960 state: '',
15961 visible: options.visible !== false, // true by default
15962 selected: options.selected === true // false by default
15963 });
15964
15965 // register event listeners
15966 events = options.events;
15967 for (eventType in events) {
15968 addEvent(series, eventType, events[eventType]);
15969 }
15970 if (
15971 (events && events.click) ||
15972 (options.point && options.point.events && options.point.events.click) ||
15973 options.allowPointSelect
15974 ) {
15975 chart.runTrackerClick = true;
15976 }
15977
15978 series.getColor();
15979 series.getSymbol();
15980
15981 // Set the data
15982 each(series.parallelArrays, function(key) {
15983 series[key + 'Data'] = [];
15984 });
15985 series.setData(options.data, false);
15986
15987 // Mark cartesian
15988 if (series.isCartesian) {
15989 chart.hasCartesianSeries = true;
15990 }
15991
15992 // Get the index and register the series in the chart. The index is one
15993 // more than the current latest series index (#5960).
15994 if (chartSeries.length) {
15995 lastSeries = chartSeries[chartSeries.length - 1];
15996 }
15997 series._i = pick(lastSeries && lastSeries._i, -1) + 1;
15998 chartSeries.push(series);
15999
16000 // Sort series according to index option (#248, #1123, #2456)
16001 stableSort(chartSeries, sortByIndex);
16002 if (this.yAxis) {
16003 stableSort(this.yAxis.series, sortByIndex);
16004 }
16005
16006 each(chartSeries, function(series, i) {
16007 series.index = i;
16008 series.name = series.name || 'Series ' + (i + 1);
16009 });
16010
16011 },
16012
16013 /**
16014 * Set the xAxis and yAxis properties of cartesian series, and register the
16015 * series in the `axis.series` array.
16016 *
16017 * @function #bindAxes
16018 * @memberOf Series
16019 * @returns {void}
16020 */
16021 bindAxes: function() {
16022 var series = this,
16023 seriesOptions = series.options,
16024 chart = series.chart,
16025 axisOptions;
16026
16027 each(series.axisTypes || [], function(AXIS) { // repeat for xAxis and yAxis
16028
16029 each(chart[AXIS], function(axis) { // loop through the chart's axis objects
16030 axisOptions = axis.options;
16031
16032 // apply if the series xAxis or yAxis option mathches the number of the
16033 // axis, or if undefined, use the first axis
16034 if ((seriesOptions[AXIS] === axisOptions.index) ||
16035 (seriesOptions[AXIS] !== undefined && seriesOptions[AXIS] === axisOptions.id) ||
16036 (seriesOptions[AXIS] === undefined && axisOptions.index === 0)) {
16037
16038 // register this series in the axis.series lookup
16039 axis.series.push(series);
16040
16041 // set this series.xAxis or series.yAxis reference
16042 series[AXIS] = axis;
16043
16044 // mark dirty for redraw
16045 axis.isDirty = true;
16046 }
16047 });
16048
16049 // The series needs an X and an Y axis
16050 if (!series[AXIS] && series.optionalAxis !== AXIS) {
16051 error(18, true);
16052 }
16053
16054 });
16055 },
16056
16057 /**
16058 * For simple series types like line and column, the data values are held in arrays like
16059 * xData and yData for quick lookup to find extremes and more. For multidimensional series
16060 * like bubble and map, this can be extended with arrays like zData and valueData by
16061 * adding to the series.parallelArrays array.
16062 */
16063 updateParallelArrays: function(point, i) {
16064 var series = point.series,
16065 args = arguments,
16066 fn = isNumber(i) ?
16067 // Insert the value in the given position
16068 function(key) {
16069 var val = key === 'y' && series.toYData ? series.toYData(point) : point[key];
16070 series[key + 'Data'][i] = val;
16071 } :
16072 // Apply the method specified in i with the following arguments as arguments
16073 function(key) {
16074 Array.prototype[i].apply(series[key + 'Data'], Array.prototype.slice.call(args, 2));
16075 };
16076
16077 each(series.parallelArrays, fn);
16078 },
16079
16080 /**
16081 * Return an auto incremented x value based on the pointStart and pointInterval options.
16082 * This is only used if an x value is not given for the point that calls autoIncrement.
16083 */
16084 autoIncrement: function() {
16085
16086 var options = this.options,
16087 xIncrement = this.xIncrement,
16088 date,
16089 pointInterval,
16090 pointIntervalUnit = options.pointIntervalUnit;
16091
16092 xIncrement = pick(xIncrement, options.pointStart, 0);
16093
16094 this.pointInterval = pointInterval = pick(this.pointInterval, options.pointInterval, 1);
16095
16096 // Added code for pointInterval strings
16097 if (pointIntervalUnit) {
16098 date = new Date(xIncrement);
16099
16100 if (pointIntervalUnit === 'day') {
16101 date = +date[Date.hcSetDate](date[Date.hcGetDate]() + pointInterval);
16102 } else if (pointIntervalUnit === 'month') {
16103 date = +date[Date.hcSetMonth](date[Date.hcGetMonth]() + pointInterval);
16104 } else if (pointIntervalUnit === 'year') {
16105 date = +date[Date.hcSetFullYear](date[Date.hcGetFullYear]() + pointInterval);
16106 }
16107 pointInterval = date - xIncrement;
16108
16109 }
16110
16111 this.xIncrement = xIncrement + pointInterval;
16112 return xIncrement;
16113 },
16114
16115 /**
16116 * Set the series options by merging from the options tree
16117 * @param {Object} itemOptions
16118 */
16119 setOptions: function(itemOptions) {
16120 var chart = this.chart,
16121 chartOptions = chart.options,
16122 plotOptions = chartOptions.plotOptions,
16123 userOptions = chart.userOptions || {},
16124 userPlotOptions = userOptions.plotOptions || {},
16125 typeOptions = plotOptions[this.type],
16126 options,
16127 zones;
16128
16129 this.userOptions = itemOptions;
16130
16131 // General series options take precedence over type options because otherwise, default
16132 // type options like column.animation would be overwritten by the general option.
16133 // But issues have been raised here (#3881), and the solution may be to distinguish
16134 // between default option and userOptions like in the tooltip below.
16135 options = merge(
16136 typeOptions,
16137 plotOptions.series,
16138 itemOptions
16139 );
16140
16141 // The tooltip options are merged between global and series specific options
16142 this.tooltipOptions = merge(
16143 defaultOptions.tooltip,
16144 defaultOptions.plotOptions[this.type].tooltip,
16145 userOptions.tooltip,
16146 userPlotOptions.series && userPlotOptions.series.tooltip,
16147 userPlotOptions[this.type] && userPlotOptions[this.type].tooltip,
16148 itemOptions.tooltip
16149 );
16150
16151 // Delete marker object if not allowed (#1125)
16152 if (typeOptions.marker === null) {
16153 delete options.marker;
16154 }
16155
16156 // Handle color zones
16157 this.zoneAxis = options.zoneAxis;
16158 zones = this.zones = (options.zones || []).slice();
16159 if ((options.negativeColor || options.negativeFillColor) && !options.zones) {
16160 zones.push({
16161 value: options[this.zoneAxis + 'Threshold'] || options.threshold || 0,
16162 className: 'highcharts-negative',
16163
16164 color: options.negativeColor,
16165 fillColor: options.negativeFillColor
16166
16167 });
16168 }
16169 if (zones.length) { // Push one extra zone for the rest
16170 if (defined(zones[zones.length - 1].value)) {
16171 zones.push({
16172
16173 color: this.color,
16174 fillColor: this.fillColor
16175
16176 });
16177 }
16178 }
16179 return options;
16180 },
16181
16182 getCyclic: function(prop, value, defaults) {
16183 var i,
16184 userOptions = this.userOptions,
16185 indexName = prop + 'Index',
16186 counterName = prop + 'Counter',
16187 len = defaults ? defaults.length : pick(this.chart.options.chart[prop + 'Count'], this.chart[prop + 'Count']),
16188 setting;
16189
16190 if (!value) {
16191 // Pick up either the colorIndex option, or the _colorIndex after Series.update()
16192 setting = pick(userOptions[indexName], userOptions['_' + indexName]);
16193 if (defined(setting)) { // after Series.update()
16194 i = setting;
16195 } else {
16196 userOptions['_' + indexName] = i = this.chart[counterName] % len;
16197 this.chart[counterName] += 1;
16198 }
16199 if (defaults) {
16200 value = defaults[i];
16201 }
16202 }
16203 // Set the colorIndex
16204 if (i !== undefined) {
16205 this[indexName] = i;
16206 }
16207 this[prop] = value;
16208 },
16209
16210 /**
16211 * Get the series' color
16212 */
16213
16214 getColor: function() {
16215 if (this.options.colorByPoint) {
16216 this.options.color = null; // #4359, selected slice got series.color even when colorByPoint was set.
16217 } else {
16218 this.getCyclic('color', this.options.color || defaultPlotOptions[this.type].color, this.chart.options.colors);
16219 }
16220 },
16221
16222 /**
16223 * Get the series' symbol
16224 */
16225 getSymbol: function() {
16226 var seriesMarkerOption = this.options.marker;
16227
16228 this.getCyclic('symbol', seriesMarkerOption.symbol, this.chart.options.symbols);
16229 },
16230
16231 drawLegendSymbol: LegendSymbolMixin.drawLineMarker,
16232
16233 /**
16234 * Replace the series data with a new set of data
16235 * @param {Object} data
16236 * @param {Object} redraw
16237 */
16238 setData: function(data, redraw, animation, updatePoints) {
16239 var series = this,
16240 oldData = series.points,
16241 oldDataLength = (oldData && oldData.length) || 0,
16242 dataLength,
16243 options = series.options,
16244 chart = series.chart,
16245 firstPoint = null,
16246 xAxis = series.xAxis,
16247 i,
16248 turboThreshold = options.turboThreshold,
16249 pt,
16250 xData = this.xData,
16251 yData = this.yData,
16252 pointArrayMap = series.pointArrayMap,
16253 valueCount = pointArrayMap && pointArrayMap.length;
16254
16255 data = data || [];
16256 dataLength = data.length;
16257 redraw = pick(redraw, true);
16258
16259 // If the point count is the same as is was, just run Point.update which is
16260 // cheaper, allows animation, and keeps references to points.
16261 if (updatePoints !== false && dataLength && oldDataLength === dataLength && !series.cropped && !series.hasGroupedData && series.visible) {
16262 each(data, function(point, i) {
16263 // .update doesn't exist on a linked, hidden series (#3709)
16264 if (oldData[i].update && point !== options.data[i]) {
16265 oldData[i].update(point, false, null, false);
16266 }
16267 });
16268
16269 } else {
16270
16271 // Reset properties
16272 series.xIncrement = null;
16273
16274 series.colorCounter = 0; // for series with colorByPoint (#1547)
16275
16276 // Update parallel arrays
16277 each(this.parallelArrays, function(key) {
16278 series[key + 'Data'].length = 0;
16279 });
16280
16281 // In turbo mode, only one- or twodimensional arrays of numbers are allowed. The
16282 // first value is tested, and we assume that all the rest are defined the same
16283 // way. Although the 'for' loops are similar, they are repeated inside each
16284 // if-else conditional for max performance.
16285 if (turboThreshold && dataLength > turboThreshold) {
16286
16287 // find the first non-null point
16288 i = 0;
16289 while (firstPoint === null && i < dataLength) {
16290 firstPoint = data[i];
16291 i++;
16292 }
16293
16294
16295 if (isNumber(firstPoint)) { // assume all points are numbers
16296 for (i = 0; i < dataLength; i++) {
16297 xData[i] = this.autoIncrement();
16298 yData[i] = data[i];
16299 }
16300 } else if (isArray(firstPoint)) { // assume all points are arrays
16301 if (valueCount) { // [x, low, high] or [x, o, h, l, c]
16302 for (i = 0; i < dataLength; i++) {
16303 pt = data[i];
16304 xData[i] = pt[0];
16305 yData[i] = pt.slice(1, valueCount + 1);
16306 }
16307 } else { // [x, y]
16308 for (i = 0; i < dataLength; i++) {
16309 pt = data[i];
16310 xData[i] = pt[0];
16311 yData[i] = pt[1];
16312 }
16313 }
16314 } else {
16315 error(12); // Highcharts expects configs to be numbers or arrays in turbo mode
16316 }
16317 } else {
16318 for (i = 0; i < dataLength; i++) {
16319 if (data[i] !== undefined) { // stray commas in oldIE
16320 pt = {
16321 series: series
16322 };
16323 series.pointClass.prototype.applyOptions.apply(pt, [data[i]]);
16324 series.updateParallelArrays(pt, i);
16325 }
16326 }
16327 }
16328
16329 // Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON
16330 if (isString(yData[0])) {
16331 error(14, true);
16332 }
16333
16334 series.data = [];
16335 series.options.data = series.userOptions.data = data;
16336
16337 // destroy old points
16338 i = oldDataLength;
16339 while (i--) {
16340 if (oldData[i] && oldData[i].destroy) {
16341 oldData[i].destroy();
16342 }
16343 }
16344
16345 // reset minRange (#878)
16346 if (xAxis) {
16347 xAxis.minRange = xAxis.userMinRange;
16348 }
16349
16350 // redraw
16351 series.isDirty = chart.isDirtyBox = true;
16352 series.isDirtyData = !!oldData;
16353 animation = false;
16354 }
16355
16356 // Typically for pie series, points need to be processed and generated
16357 // prior to rendering the legend
16358 if (options.legendType === 'point') {
16359 this.processData();
16360 this.generatePoints();
16361 }
16362
16363 if (redraw) {
16364 chart.redraw(animation);
16365 }
16366 },
16367
16368 /**
16369 * Process the data by cropping away unused data points if the series is longer
16370 * than the crop threshold. This saves computing time for lage series.
16371 */
16372 processData: function(force) {
16373 var series = this,
16374 processedXData = series.xData, // copied during slice operation below
16375 processedYData = series.yData,
16376 dataLength = processedXData.length,
16377 croppedData,
16378 cropStart = 0,
16379 cropped,
16380 distance,
16381 closestPointRange,
16382 xAxis = series.xAxis,
16383 i, // loop variable
16384 options = series.options,
16385 cropThreshold = options.cropThreshold,
16386 getExtremesFromAll = series.getExtremesFromAll || options.getExtremesFromAll, // #4599
16387 isCartesian = series.isCartesian,
16388 xExtremes,
16389 val2lin = xAxis && xAxis.val2lin,
16390 isLog = xAxis && xAxis.isLog,
16391 min,
16392 max;
16393
16394 // If the series data or axes haven't changed, don't go through this. Return false to pass
16395 // the message on to override methods like in data grouping.
16396 if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) {
16397 return false;
16398 }
16399
16400 if (xAxis) {
16401 xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053)
16402 min = xExtremes.min;
16403 max = xExtremes.max;
16404 }
16405
16406 // optionally filter out points outside the plot area
16407 if (isCartesian && series.sorted && !getExtremesFromAll && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) {
16408
16409 // it's outside current extremes
16410 if (processedXData[dataLength - 1] < min || processedXData[0] > max) {
16411 processedXData = [];
16412 processedYData = [];
16413
16414 // only crop if it's actually spilling out
16415 } else if (processedXData[0] < min || processedXData[dataLength - 1] > max) {
16416 croppedData = this.cropData(series.xData, series.yData, min, max);
16417 processedXData = croppedData.xData;
16418 processedYData = croppedData.yData;
16419 cropStart = croppedData.start;
16420 cropped = true;
16421 }
16422 }
16423
16424
16425 // Find the closest distance between processed points
16426 i = processedXData.length || 1;
16427 while (--i) {
16428 distance = isLog ?
16429 val2lin(processedXData[i]) - val2lin(processedXData[i - 1]) :
16430 processedXData[i] - processedXData[i - 1];
16431
16432 if (distance > 0 && (closestPointRange === undefined || distance < closestPointRange)) {
16433 closestPointRange = distance;
16434
16435 // Unsorted data is not supported by the line tooltip, as well as data grouping and
16436 // navigation in Stock charts (#725) and width calculation of columns (#1900)
16437 } else if (distance < 0 && series.requireSorting) {
16438 error(15);
16439 }
16440 }
16441
16442 // Record the properties
16443 series.cropped = cropped; // undefined or true
16444 series.cropStart = cropStart;
16445 series.processedXData = processedXData;
16446 series.processedYData = processedYData;
16447
16448 series.closestPointRange = closestPointRange;
16449
16450 },
16451
16452 /**
16453 * Iterate over xData and crop values between min and max. Returns object containing crop start/end
16454 * cropped xData with corresponding part of yData, dataMin and dataMax within the cropped range
16455 */
16456 cropData: function(xData, yData, min, max) {
16457 var dataLength = xData.length,
16458 cropStart = 0,
16459 cropEnd = dataLength,
16460 cropShoulder = pick(this.cropShoulder, 1), // line-type series need one point outside
16461 i,
16462 j;
16463
16464 // iterate up to find slice start
16465 for (i = 0; i < dataLength; i++) {
16466 if (xData[i] >= min) {
16467 cropStart = Math.max(0, i - cropShoulder);
16468 break;
16469 }
16470 }
16471
16472 // proceed to find slice end
16473 for (j = i; j < dataLength; j++) {
16474 if (xData[j] > max) {
16475 cropEnd = j + cropShoulder;
16476 break;
16477 }
16478 }
16479
16480 return {
16481 xData: xData.slice(cropStart, cropEnd),
16482 yData: yData.slice(cropStart, cropEnd),
16483 start: cropStart,
16484 end: cropEnd
16485 };
16486 },
16487
16488
16489 /**
16490 * Generate the data point after the data has been processed by cropping away
16491 * unused points and optionally grouped in Highcharts Stock.
16492 */
16493 generatePoints: function() {
16494 var series = this,
16495 options = series.options,
16496 dataOptions = options.data,
16497 data = series.data,
16498 dataLength,
16499 processedXData = series.processedXData,
16500 processedYData = series.processedYData,
16501 PointClass = series.pointClass,
16502 processedDataLength = processedXData.length,
16503 cropStart = series.cropStart || 0,
16504 cursor,
16505 hasGroupedData = series.hasGroupedData,
16506 point,
16507 points = [],
16508 i;
16509
16510 if (!data && !hasGroupedData) {
16511 var arr = [];
16512 arr.length = dataOptions.length;
16513 data = series.data = arr;
16514 }
16515
16516 for (i = 0; i < processedDataLength; i++) {
16517 cursor = cropStart + i;
16518 if (!hasGroupedData) {
16519 point = data[cursor];
16520 if (!point && dataOptions[cursor] !== undefined) { // #970
16521 data[cursor] = point = (new PointClass()).init(series, dataOptions[cursor], processedXData[i]);
16522 }
16523 } else {
16524 // splat the y data in case of ohlc data array
16525 point = (new PointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i])));
16526 point.dataGroup = series.groupMap[i];
16527 }
16528 point.index = cursor; // For faster access in Point.update
16529 points[i] = point;
16530 }
16531
16532 // Hide cropped-away points - this only runs when the number of points is above cropThreshold, or when
16533 // swithching view from non-grouped data to grouped data (#637)
16534 if (data && (processedDataLength !== (dataLength = data.length) || hasGroupedData)) {
16535 for (i = 0; i < dataLength; i++) {
16536 if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points
16537 i += processedDataLength;
16538 }
16539 if (data[i]) {
16540 data[i].destroyElements();
16541 data[i].plotX = undefined; // #1003
16542 }
16543 }
16544 }
16545
16546 series.data = data;
16547 series.points = points;
16548 },
16549
16550 /**
16551 * Calculate Y extremes for visible data
16552 */
16553 getExtremes: function(yData) {
16554 var xAxis = this.xAxis,
16555 yAxis = this.yAxis,
16556 xData = this.processedXData,
16557 yDataLength,
16558 activeYData = [],
16559 activeCounter = 0,
16560 xExtremes = xAxis.getExtremes(), // #2117, need to compensate for log X axis
16561 xMin = xExtremes.min,
16562 xMax = xExtremes.max,
16563 validValue,
16564 withinRange,
16565 x,
16566 y,
16567 i,
16568 j;
16569
16570 yData = yData || this.stackedYData || this.processedYData || [];
16571 yDataLength = yData.length;
16572
16573 for (i = 0; i < yDataLength; i++) {
16574
16575 x = xData[i];
16576 y = yData[i];
16577
16578 // For points within the visible range, including the first point outside the
16579 // visible range, consider y extremes
16580 validValue = (isNumber(y, true) || isArray(y)) && (!yAxis.isLog || (y.length || y > 0));
16581 withinRange = this.getExtremesFromAll || this.options.getExtremesFromAll || this.cropped ||
16582 ((xData[i + 1] || x) >= xMin && (xData[i - 1] || x) <= xMax);
16583
16584 if (validValue && withinRange) {
16585
16586 j = y.length;
16587 if (j) { // array, like ohlc or range data
16588 while (j--) {
16589 if (y[j] !== null) {
16590 activeYData[activeCounter++] = y[j];
16591 }
16592 }
16593 } else {
16594 activeYData[activeCounter++] = y;
16595 }
16596 }
16597 }
16598 this.dataMin = arrayMin(activeYData);
16599 this.dataMax = arrayMax(activeYData);
16600 },
16601
16602 /**
16603 * Translate data points from raw data values to chart specific positioning
16604 * data needed later in drawPoints, drawGraph and drawTracker.
16605 *
16606 * @function #translate
16607 * @memberOf Series
16608 * @returns {void}
16609 */
16610 translate: function() {
16611 if (!this.processedXData) { // hidden series
16612 this.processData();
16613 }
16614 this.generatePoints();
16615 var series = this,
16616 options = series.options,
16617 stacking = options.stacking,
16618 xAxis = series.xAxis,
16619 categories = xAxis.categories,
16620 yAxis = series.yAxis,
16621 points = series.points,
16622 dataLength = points.length,
16623 hasModifyValue = !!series.modifyValue,
16624 i,
16625 pointPlacement = options.pointPlacement,
16626 dynamicallyPlaced = pointPlacement === 'between' || isNumber(pointPlacement),
16627 threshold = options.threshold,
16628 stackThreshold = options.startFromThreshold ? threshold : 0,
16629 plotX,
16630 plotY,
16631 lastPlotX,
16632 stackIndicator,
16633 closestPointRangePx = Number.MAX_VALUE;
16634
16635 // Point placement is relative to each series pointRange (#5889)
16636 if (pointPlacement === 'between') {
16637 pointPlacement = 0.5;
16638 }
16639 if (isNumber(pointPlacement)) {
16640 pointPlacement *= pick(options.pointRange || xAxis.pointRange);
16641 }
16642
16643 // Translate each point
16644 for (i = 0; i < dataLength; i++) {
16645 var point = points[i],
16646 xValue = point.x,
16647 yValue = point.y,
16648 yBottom = point.low,
16649 stack = stacking && yAxis.stacks[(series.negStacks && yValue < (stackThreshold ? 0 : threshold) ? '-' : '') + series.stackKey],
16650 pointStack,
16651 stackValues;
16652
16653 // Discard disallowed y values for log axes (#3434)
16654 if (yAxis.isLog && yValue !== null && yValue <= 0) {
16655 point.isNull = true;
16656 }
16657
16658 // Get the plotX translation
16659 point.plotX = plotX = correctFloat( // #5236
16660 Math.min(Math.max(-1e5, xAxis.translate(
16661 xValue,
16662 0,
16663 0,
16664 0,
16665 1,
16666 pointPlacement,
16667 this.type === 'flags'
16668 )), 1e5) // #3923
16669 );
16670
16671 // Calculate the bottom y value for stacked series
16672 if (stacking && series.visible && !point.isNull && stack && stack[xValue]) {
16673 stackIndicator = series.getStackIndicator(stackIndicator, xValue, series.index);
16674 pointStack = stack[xValue];
16675 stackValues = pointStack.points[stackIndicator.key];
16676 yBottom = stackValues[0];
16677 yValue = stackValues[1];
16678
16679 if (yBottom === stackThreshold && stackIndicator.key === stack[xValue].base) {
16680 yBottom = pick(threshold, yAxis.min);
16681 }
16682 if (yAxis.isLog && yBottom <= 0) { // #1200, #1232
16683 yBottom = null;
16684 }
16685
16686 point.total = point.stackTotal = pointStack.total;
16687 point.percentage = pointStack.total && (point.y / pointStack.total * 100);
16688 point.stackY = yValue;
16689
16690 // Place the stack label
16691 pointStack.setOffset(series.pointXOffset || 0, series.barW || 0);
16692
16693 }
16694
16695 // Set translated yBottom or remove it
16696 point.yBottom = defined(yBottom) ?
16697 yAxis.translate(yBottom, 0, 1, 0, 1) :
16698 null;
16699
16700 // general hook, used for Highstock compare mode
16701 if (hasModifyValue) {
16702 yValue = series.modifyValue(yValue, point);
16703 }
16704
16705 // Set the the plotY value, reset it for redraws
16706 point.plotY = plotY = (typeof yValue === 'number' && yValue !== Infinity) ?
16707 Math.min(Math.max(-1e5, yAxis.translate(yValue, 0, 1, 0, 1)), 1e5) : // #3201
16708 undefined;
16709
16710 point.isInside = plotY !== undefined && plotY >= 0 && plotY <= yAxis.len && // #3519
16711 plotX >= 0 && plotX <= xAxis.len;
16712
16713
16714 // Set client related positions for mouse tracking
16715 point.clientX = dynamicallyPlaced ? correctFloat(xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement)) : plotX; // #1514, #5383, #5518
16716
16717 point.negative = point.y < (threshold || 0);
16718
16719 // some API data
16720 point.category = categories && categories[point.x] !== undefined ?
16721 categories[point.x] : point.x;
16722
16723 // Determine auto enabling of markers (#3635, #5099)
16724 if (!point.isNull) {
16725 if (lastPlotX !== undefined) {
16726 closestPointRangePx = Math.min(closestPointRangePx, Math.abs(plotX - lastPlotX));
16727 }
16728 lastPlotX = plotX;
16729 }
16730
16731 }
16732 series.closestPointRangePx = closestPointRangePx;
16733 },
16734
16735 /**
16736 * Return the series points with null points filtered out
16737 */
16738 getValidPoints: function(points, insideOnly) {
16739 var chart = this.chart;
16740 return grep(points || this.points || [], function isValidPoint(point) { // #3916, #5029
16741 if (insideOnly && !chart.isInsidePlot(point.plotX, point.plotY, chart.inverted)) { // #5085
16742 return false;
16743 }
16744 return !point.isNull;
16745 });
16746 },
16747
16748 /**
16749 * Set the clipping for the series. For animated series it is called twice, first to initiate
16750 * animating the clip then the second time without the animation to set the final clip.
16751 */
16752 setClip: function(animation) {
16753 var chart = this.chart,
16754 options = this.options,
16755 renderer = chart.renderer,
16756 inverted = chart.inverted,
16757 seriesClipBox = this.clipBox,
16758 clipBox = seriesClipBox || chart.clipBox,
16759 sharedClipKey = this.sharedClipKey || ['_sharedClip', animation && animation.duration, animation && animation.easing, clipBox.height, options.xAxis, options.yAxis].join(','), // #4526
16760 clipRect = chart[sharedClipKey],
16761 markerClipRect = chart[sharedClipKey + 'm'];
16762
16763 // If a clipping rectangle with the same properties is currently present in the chart, use that.
16764 if (!clipRect) {
16765
16766 // When animation is set, prepare the initial positions
16767 if (animation) {
16768 clipBox.width = 0;
16769
16770 chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect(-99, // include the width of the first marker
16771 inverted ? -chart.plotLeft : -chart.plotTop,
16772 99,
16773 inverted ? chart.chartWidth : chart.chartHeight
16774 );
16775 }
16776 chart[sharedClipKey] = clipRect = renderer.clipRect(clipBox);
16777 // Create hashmap for series indexes
16778 clipRect.count = {
16779 length: 0
16780 };
16781
16782 }
16783 if (animation) {
16784 if (!clipRect.count[this.index]) {
16785 clipRect.count[this.index] = true;
16786 clipRect.count.length += 1;
16787 }
16788 }
16789
16790 if (options.clip !== false) {
16791 this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect);
16792 this.markerGroup.clip(markerClipRect);
16793 this.sharedClipKey = sharedClipKey;
16794 }
16795
16796 // Remove the shared clipping rectangle when all series are shown
16797 if (!animation) {
16798 if (clipRect.count[this.index]) {
16799 delete clipRect.count[this.index];
16800 clipRect.count.length -= 1;
16801 }
16802
16803 if (clipRect.count.length === 0 && sharedClipKey && chart[sharedClipKey]) {
16804 if (!seriesClipBox) {
16805 chart[sharedClipKey] = chart[sharedClipKey].destroy();
16806 }
16807 if (chart[sharedClipKey + 'm']) {
16808 chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy();
16809 }
16810 }
16811 }
16812 },
16813
16814 /**
16815 * Animate in the series
16816 */
16817 animate: function(init) {
16818 var series = this,
16819 chart = series.chart,
16820 clipRect,
16821 animation = animObject(series.options.animation),
16822 sharedClipKey;
16823
16824 // Initialize the animation. Set up the clipping rectangle.
16825 if (init) {
16826
16827 series.setClip(animation);
16828
16829 // Run the animation
16830 } else {
16831 sharedClipKey = this.sharedClipKey;
16832 clipRect = chart[sharedClipKey];
16833 if (clipRect) {
16834 clipRect.animate({
16835 width: chart.plotSizeX
16836 }, animation);
16837 }
16838 if (chart[sharedClipKey + 'm']) {
16839 chart[sharedClipKey + 'm'].animate({
16840 width: chart.plotSizeX + 99
16841 }, animation);
16842 }
16843
16844 // Delete this function to allow it only once
16845 series.animate = null;
16846
16847 }
16848 },
16849
16850 /**
16851 * This runs after animation to land on the final plot clipping
16852 */
16853 afterAnimate: function() {
16854 this.setClip();
16855 fireEvent(this, 'afterAnimate');
16856 },
16857
16858 /**
16859 * Draw the markers.
16860 *
16861 * @function #drawPoints
16862 * @memberOf Series
16863 * @returns {void}
16864 */
16865 drawPoints: function() {
16866 var series = this,
16867 points = series.points,
16868 chart = series.chart,
16869 plotY,
16870 i,
16871 point,
16872 symbol,
16873 graphic,
16874 options = series.options,
16875 seriesMarkerOptions = options.marker,
16876 pointMarkerOptions,
16877 hasPointMarker,
16878 enabled,
16879 isInside,
16880 markerGroup = series.markerGroup,
16881 xAxis = series.xAxis,
16882 markerAttribs,
16883 globallyEnabled = pick(
16884 seriesMarkerOptions.enabled,
16885 xAxis.isRadial ? true : null,
16886 series.closestPointRangePx > 2 * seriesMarkerOptions.radius
16887 );
16888
16889 if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) {
16890
16891 i = points.length;
16892 while (i--) {
16893 point = points[i];
16894 plotY = point.plotY;
16895 graphic = point.graphic;
16896 pointMarkerOptions = point.marker || {};
16897 hasPointMarker = !!point.marker;
16898 enabled = (globallyEnabled && pointMarkerOptions.enabled === undefined) || pointMarkerOptions.enabled;
16899 isInside = point.isInside;
16900
16901 // only draw the point if y is defined
16902 if (enabled && isNumber(plotY) && point.y !== null) {
16903
16904 // Shortcuts
16905 symbol = pick(pointMarkerOptions.symbol, series.symbol);
16906 point.hasImage = symbol.indexOf('url') === 0;
16907
16908 markerAttribs = series.markerAttribs(
16909 point,
16910 point.selected && 'select'
16911 );
16912
16913 if (graphic) { // update
16914 graphic[isInside ? 'show' : 'hide'](true) // Since the marker group isn't clipped, each individual marker must be toggled
16915 .animate(markerAttribs);
16916 } else if (isInside && (markerAttribs.width > 0 || point.hasImage)) {
16917 point.graphic = graphic = chart.renderer.symbol(
16918 symbol,
16919 markerAttribs.x,
16920 markerAttribs.y,
16921 markerAttribs.width,
16922 markerAttribs.height,
16923 hasPointMarker ? pointMarkerOptions : seriesMarkerOptions
16924 )
16925 .add(markerGroup);
16926 }
16927
16928
16929 // Presentational attributes
16930 if (graphic) {
16931 graphic.attr(series.pointAttribs(point, point.selected && 'select'));
16932 }
16933
16934
16935 if (graphic) {
16936 graphic.addClass(point.getClassName(), true);
16937 }
16938
16939 } else if (graphic) {
16940 point.graphic = graphic.destroy(); // #1269
16941 }
16942 }
16943 }
16944
16945 },
16946
16947 /**
16948 * Get non-presentational attributes for the point.
16949 */
16950 markerAttribs: function(point, state) {
16951 var seriesMarkerOptions = this.options.marker,
16952 seriesStateOptions,
16953 pointOptions = point && point.options,
16954 pointMarkerOptions = (pointOptions && pointOptions.marker) || {},
16955 pointStateOptions,
16956 radius = pick(
16957 pointMarkerOptions.radius,
16958 seriesMarkerOptions.radius
16959 ),
16960 attribs;
16961
16962 // Handle hover and select states
16963 if (state) {
16964 seriesStateOptions = seriesMarkerOptions.states[state];
16965 pointStateOptions = pointMarkerOptions.states &&
16966 pointMarkerOptions.states[state];
16967
16968 radius = pick(
16969 pointStateOptions && pointStateOptions.radius,
16970 seriesStateOptions && seriesStateOptions.radius,
16971 radius + (seriesStateOptions && seriesStateOptions.radiusPlus || 0)
16972 );
16973 }
16974
16975 if (point.hasImage) {
16976 radius = 0; // and subsequently width and height is not set
16977 }
16978
16979 attribs = {
16980 x: Math.floor(point.plotX) - radius, // Math.floor for #1843
16981 y: point.plotY - radius
16982 };
16983
16984 if (radius) {
16985 attribs.width = attribs.height = 2 * radius;
16986 }
16987
16988 return attribs;
16989
16990 },
16991
16992
16993 /**
16994 * Get presentational attributes for marker-based series (line, spline, scatter, bubble, mappoint...)
16995 */
16996 pointAttribs: function(point, state) {
16997 var seriesMarkerOptions = this.options.marker,
16998 seriesStateOptions,
16999 pointOptions = point && point.options,
17000 pointMarkerOptions = (pointOptions && pointOptions.marker) || {},
17001 pointStateOptions,
17002 color = this.color,
17003 pointColorOption = pointOptions && pointOptions.color,
17004 pointColor = point && point.color,
17005 strokeWidth = pick(
17006 pointMarkerOptions.lineWidth,
17007 seriesMarkerOptions.lineWidth
17008 ),
17009 zoneColor,
17010 fill,
17011 stroke,
17012 zone;
17013
17014 if (point && this.zones.length) {
17015 zone = point.getZone();
17016 if (zone && zone.color) {
17017 zoneColor = zone.color;
17018 }
17019 }
17020
17021 color = pointColorOption || zoneColor || pointColor || color;
17022 fill = pointMarkerOptions.fillColor || seriesMarkerOptions.fillColor || color;
17023 stroke = pointMarkerOptions.lineColor || seriesMarkerOptions.lineColor || color;
17024
17025 // Handle hover and select states
17026 if (state) {
17027 seriesStateOptions = seriesMarkerOptions.states[state];
17028 pointStateOptions = (pointMarkerOptions.states && pointMarkerOptions.states[state]) || {};
17029 strokeWidth = pick(
17030 pointStateOptions.lineWidth,
17031 seriesStateOptions.lineWidth,
17032 strokeWidth + pick(
17033 pointStateOptions.lineWidthPlus,
17034 seriesStateOptions.lineWidthPlus,
17035 0
17036 )
17037 );
17038 fill = pointStateOptions.fillColor || seriesStateOptions.fillColor || fill;
17039 stroke = pointStateOptions.lineColor || seriesStateOptions.lineColor || stroke;
17040 }
17041
17042 return {
17043 'stroke': stroke,
17044 'stroke-width': strokeWidth,
17045 'fill': fill
17046 };
17047 },
17048
17049 /**
17050 * Clear DOM objects and free up memory
17051 */
17052 destroy: function() {
17053 var series = this,
17054 chart = series.chart,
17055 issue134 = /AppleWebKit\/533/.test(win.navigator.userAgent),
17056 destroy,
17057 i,
17058 data = series.data || [],
17059 point,
17060 prop,
17061 axis;
17062
17063 // add event hook
17064 fireEvent(series, 'destroy');
17065
17066 // remove all events
17067 removeEvent(series);
17068
17069 // erase from axes
17070 each(series.axisTypes || [], function(AXIS) {
17071 axis = series[AXIS];
17072 if (axis && axis.series) {
17073 erase(axis.series, series);
17074 axis.isDirty = axis.forceRedraw = true;
17075 }
17076 });
17077
17078 // remove legend items
17079 if (series.legendItem) {
17080 series.chart.legend.destroyItem(series);
17081 }
17082
17083 // destroy all points with their elements
17084 i = data.length;
17085 while (i--) {
17086 point = data[i];
17087 if (point && point.destroy) {
17088 point.destroy();
17089 }
17090 }
17091 series.points = null;
17092
17093 // Clear the animation timeout if we are destroying the series during initial animation
17094 clearTimeout(series.animationTimeout);
17095
17096 // Destroy all SVGElements associated to the series
17097 for (prop in series) {
17098 if (series[prop] instanceof SVGElement && !series[prop].survive) { // Survive provides a hook for not destroying
17099
17100 // issue 134 workaround
17101 destroy = issue134 && prop === 'group' ?
17102 'hide' :
17103 'destroy';
17104
17105 series[prop][destroy]();
17106 }
17107 }
17108
17109 // remove from hoverSeries
17110 if (chart.hoverSeries === series) {
17111 chart.hoverSeries = null;
17112 }
17113 erase(chart.series, series);
17114
17115 // clear all members
17116 for (prop in series) {
17117 delete series[prop];
17118 }
17119 },
17120
17121 /**
17122 * Get the graph path
17123 */
17124 getGraphPath: function(points, nullsAsZeroes, connectCliffs) {
17125 var series = this,
17126 options = series.options,
17127 step = options.step,
17128 reversed,
17129 graphPath = [],
17130 xMap = [],
17131 gap;
17132
17133 points = points || series.points;
17134
17135 // Bottom of a stack is reversed
17136 reversed = points.reversed;
17137 if (reversed) {
17138 points.reverse();
17139 }
17140 // Reverse the steps (#5004)
17141 step = {
17142 right: 1,
17143 center: 2
17144 }[step] || (step && 3);
17145 if (step && reversed) {
17146 step = 4 - step;
17147 }
17148
17149 // Remove invalid points, especially in spline (#5015)
17150 if (options.connectNulls && !nullsAsZeroes && !connectCliffs) {
17151 points = this.getValidPoints(points);
17152 }
17153
17154 // Build the line
17155 each(points, function(point, i) {
17156
17157 var plotX = point.plotX,
17158 plotY = point.plotY,
17159 lastPoint = points[i - 1],
17160 pathToPoint; // the path to this point from the previous
17161
17162 if ((point.leftCliff || (lastPoint && lastPoint.rightCliff)) && !connectCliffs) {
17163 gap = true; // ... and continue
17164 }
17165
17166 // Line series, nullsAsZeroes is not handled
17167 if (point.isNull && !defined(nullsAsZeroes) && i > 0) {
17168 gap = !options.connectNulls;
17169
17170 // Area series, nullsAsZeroes is set
17171 } else if (point.isNull && !nullsAsZeroes) {
17172 gap = true;
17173
17174 } else {
17175
17176 if (i === 0 || gap) {
17177 pathToPoint = ['M', point.plotX, point.plotY];
17178
17179 } else if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object
17180
17181 pathToPoint = series.getPointSpline(points, point, i);
17182
17183 } else if (step) {
17184
17185 if (step === 1) { // right
17186 pathToPoint = [
17187 'L',
17188 lastPoint.plotX,
17189 plotY
17190 ];
17191
17192 } else if (step === 2) { // center
17193 pathToPoint = [
17194 'L',
17195 (lastPoint.plotX + plotX) / 2,
17196 lastPoint.plotY,
17197 'L',
17198 (lastPoint.plotX + plotX) / 2,
17199 plotY
17200 ];
17201
17202 } else {
17203 pathToPoint = [
17204 'L',
17205 plotX,
17206 lastPoint.plotY
17207 ];
17208 }
17209 pathToPoint.push('L', plotX, plotY);
17210
17211 } else {
17212 // normal line to next point
17213 pathToPoint = [
17214 'L',
17215 plotX,
17216 plotY
17217 ];
17218 }
17219
17220 // Prepare for animation. When step is enabled, there are two path nodes for each x value.
17221 xMap.push(point.x);
17222 if (step) {
17223 xMap.push(point.x);
17224 }
17225
17226 graphPath.push.apply(graphPath, pathToPoint);
17227 gap = false;
17228 }
17229 });
17230
17231 graphPath.xMap = xMap;
17232 series.graphPath = graphPath;
17233
17234 return graphPath;
17235
17236 },
17237
17238 /**
17239 * Draw the actual graph
17240 */
17241 drawGraph: function() {
17242 var series = this,
17243 options = this.options,
17244 graphPath = (this.gappedPath || this.getGraphPath).call(this),
17245 props = [
17246 [
17247 'graph',
17248 'highcharts-graph',
17249
17250 options.lineColor || this.color,
17251 options.dashStyle
17252
17253 ]
17254 ];
17255
17256 // Add the zone properties if any
17257 each(this.zones, function(zone, i) {
17258 props.push([
17259 'zone-graph-' + i,
17260 'highcharts-graph highcharts-zone-graph-' + i + ' ' + (zone.className || ''),
17261
17262 zone.color || series.color,
17263 zone.dashStyle || options.dashStyle
17264
17265 ]);
17266 });
17267
17268 // Draw the graph
17269 each(props, function(prop, i) {
17270 var graphKey = prop[0],
17271 graph = series[graphKey],
17272 attribs;
17273
17274 if (graph) {
17275 graph.endX = graphPath.xMap;
17276 graph.animate({
17277 d: graphPath
17278 });
17279
17280 } else if (graphPath.length) { // #1487
17281
17282 series[graphKey] = series.chart.renderer.path(graphPath)
17283 .addClass(prop[1])
17284 .attr({
17285 zIndex: 1
17286 }) // #1069
17287 .add(series.group);
17288
17289
17290 attribs = {
17291 'stroke': prop[2],
17292 'stroke-width': options.lineWidth,
17293 'fill': (series.fillGraph && series.color) || 'none' // Polygon series use filled graph
17294 };
17295
17296 if (prop[3]) {
17297 attribs.dashstyle = prop[3];
17298 } else if (options.linecap !== 'square') {
17299 attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round';
17300 }
17301
17302 graph = series[graphKey]
17303 .attr(attribs)
17304 .shadow((i < 2) && options.shadow); // add shadow to normal series (0) or to first zone (1) #3932
17305
17306 }
17307
17308 // Helpers for animation
17309 if (graph) {
17310 graph.startX = graphPath.xMap;
17311 //graph.shiftUnit = options.step ? 2 : 1;
17312 graph.isArea = graphPath.isArea; // For arearange animation
17313 }
17314 });
17315 },
17316
17317 /**
17318 * Clip the graphs into the positive and negative coloured graphs
17319 */
17320 applyZones: function() {
17321 var series = this,
17322 chart = this.chart,
17323 renderer = chart.renderer,
17324 zones = this.zones,
17325 translatedFrom,
17326 translatedTo,
17327 clips = this.clips || [],
17328 clipAttr,
17329 graph = this.graph,
17330 area = this.area,
17331 chartSizeMax = Math.max(chart.chartWidth, chart.chartHeight),
17332 axis = this[(this.zoneAxis || 'y') + 'Axis'],
17333 extremes,
17334 reversed,
17335 inverted = chart.inverted,
17336 horiz,
17337 pxRange,
17338 pxPosMin,
17339 pxPosMax,
17340 ignoreZones = false;
17341
17342 if (zones.length && (graph || area) && axis && axis.min !== undefined) {
17343 reversed = axis.reversed;
17344 horiz = axis.horiz;
17345 // The use of the Color Threshold assumes there are no gaps
17346 // so it is safe to hide the original graph and area
17347 if (graph) {
17348 graph.hide();
17349 }
17350 if (area) {
17351 area.hide();
17352 }
17353
17354 // Create the clips
17355 extremes = axis.getExtremes();
17356 each(zones, function(threshold, i) {
17357
17358 translatedFrom = reversed ?
17359 (horiz ? chart.plotWidth : 0) :
17360 (horiz ? 0 : axis.toPixels(extremes.min));
17361 translatedFrom = Math.min(Math.max(pick(translatedTo, translatedFrom), 0), chartSizeMax);
17362 translatedTo = Math.min(Math.max(Math.round(axis.toPixels(pick(threshold.value, extremes.max), true)), 0), chartSizeMax);
17363
17364 if (ignoreZones) {
17365 translatedFrom = translatedTo = axis.toPixels(extremes.max);
17366 }
17367
17368 pxRange = Math.abs(translatedFrom - translatedTo);
17369 pxPosMin = Math.min(translatedFrom, translatedTo);
17370 pxPosMax = Math.max(translatedFrom, translatedTo);
17371 if (axis.isXAxis) {
17372 clipAttr = {
17373 x: inverted ? pxPosMax : pxPosMin,
17374 y: 0,
17375 width: pxRange,
17376 height: chartSizeMax
17377 };
17378 if (!horiz) {
17379 clipAttr.x = chart.plotHeight - clipAttr.x;
17380 }
17381 } else {
17382 clipAttr = {
17383 x: 0,
17384 y: inverted ? pxPosMax : pxPosMin,
17385 width: chartSizeMax,
17386 height: pxRange
17387 };
17388 if (horiz) {
17389 clipAttr.y = chart.plotWidth - clipAttr.y;
17390 }
17391 }
17392
17393
17394 /// VML SUPPPORT
17395 if (inverted && renderer.isVML) {
17396 if (axis.isXAxis) {
17397 clipAttr = {
17398 x: 0,
17399 y: reversed ? pxPosMin : pxPosMax,
17400 height: clipAttr.width,
17401 width: chart.chartWidth
17402 };
17403 } else {
17404 clipAttr = {
17405 x: clipAttr.y - chart.plotLeft - chart.spacingBox.x,
17406 y: 0,
17407 width: clipAttr.height,
17408 height: chart.chartHeight
17409 };
17410 }
17411 }
17412 /// END OF VML SUPPORT
17413
17414
17415 if (clips[i]) {
17416 clips[i].animate(clipAttr);
17417 } else {
17418 clips[i] = renderer.clipRect(clipAttr);
17419
17420 if (graph) {
17421 series['zone-graph-' + i].clip(clips[i]);
17422 }
17423
17424 if (area) {
17425 series['zone-area-' + i].clip(clips[i]);
17426 }
17427 }
17428 // if this zone extends out of the axis, ignore the others
17429 ignoreZones = threshold.value > extremes.max;
17430 });
17431 this.clips = clips;
17432 }
17433 },
17434
17435 /**
17436 * Initialize and perform group inversion on series.group and series.markerGroup
17437 */
17438 invertGroups: function(inverted) {
17439 var series = this,
17440 chart = series.chart,
17441 remover;
17442
17443 function setInvert() {
17444 var size = {
17445 width: series.yAxis.len,
17446 height: series.xAxis.len
17447 };
17448
17449 each(['group', 'markerGroup'], function(groupName) {
17450 if (series[groupName]) {
17451 series[groupName].attr(size).invert(inverted);
17452 }
17453 });
17454 }
17455
17456 // Pie, go away (#1736)
17457 if (!series.xAxis) {
17458 return;
17459 }
17460
17461 // A fixed size is needed for inversion to work
17462 remover = addEvent(chart, 'resize', setInvert);
17463 addEvent(series, 'destroy', remover);
17464
17465 // Do it now
17466 setInvert(inverted); // do it now
17467
17468 // On subsequent render and redraw, just do setInvert without setting up events again
17469 series.invertGroups = setInvert;
17470 },
17471
17472 /**
17473 * General abstraction for creating plot groups like series.group, series.dataLabelsGroup and
17474 * series.markerGroup. On subsequent calls, the group will only be adjusted to the updated plot size.
17475 */
17476 plotGroup: function(prop, name, visibility, zIndex, parent) {
17477 var group = this[prop],
17478 isNew = !group;
17479
17480 // Generate it on first call
17481 if (isNew) {
17482 this[prop] = group = this.chart.renderer.g(name)
17483 .attr({
17484 zIndex: zIndex || 0.1 // IE8 and pointer logic use this
17485 })
17486 .add(parent);
17487
17488 group.addClass('highcharts-series-' + this.index + ' highcharts-' + this.type + '-series highcharts-color-' + this.colorIndex +
17489 ' ' + (this.options.className || ''));
17490 }
17491
17492 // Place it on first and subsequent (redraw) calls
17493 group.attr({
17494 visibility: visibility
17495 })[isNew ? 'attr' : 'animate'](this.getPlotBox());
17496 return group;
17497 },
17498
17499 /**
17500 * Get the translation and scale for the plot area of this series
17501 */
17502 getPlotBox: function() {
17503 var chart = this.chart,
17504 xAxis = this.xAxis,
17505 yAxis = this.yAxis;
17506
17507 // Swap axes for inverted (#2339)
17508 if (chart.inverted) {
17509 xAxis = yAxis;
17510 yAxis = this.xAxis;
17511 }
17512 return {
17513 translateX: xAxis ? xAxis.left : chart.plotLeft,
17514 translateY: yAxis ? yAxis.top : chart.plotTop,
17515 scaleX: 1, // #1623
17516 scaleY: 1
17517 };
17518 },
17519
17520 /**
17521 * Render the graph and markers
17522 */
17523 render: function() {
17524 var series = this,
17525 chart = series.chart,
17526 group,
17527 options = series.options,
17528 // Animation doesn't work in IE8 quirks when the group div is hidden,
17529 // and looks bad in other oldIE
17530 animDuration = !!series.animate && chart.renderer.isSVG && animObject(options.animation).duration,
17531 visibility = series.visible ? 'inherit' : 'hidden', // #2597
17532 zIndex = options.zIndex,
17533 hasRendered = series.hasRendered,
17534 chartSeriesGroup = chart.seriesGroup,
17535 inverted = chart.inverted;
17536
17537 // the group
17538 group = series.plotGroup(
17539 'group',
17540 'series',
17541 visibility,
17542 zIndex,
17543 chartSeriesGroup
17544 );
17545
17546 series.markerGroup = series.plotGroup(
17547 'markerGroup',
17548 'markers',
17549 visibility,
17550 zIndex,
17551 chartSeriesGroup
17552 );
17553
17554 // initiate the animation
17555 if (animDuration) {
17556 series.animate(true);
17557 }
17558
17559 // SVGRenderer needs to know this before drawing elements (#1089, #1795)
17560 group.inverted = series.isCartesian ? inverted : false;
17561
17562 // draw the graph if any
17563 if (series.drawGraph) {
17564 series.drawGraph();
17565 series.applyZones();
17566 }
17567
17568 /* each(series.points, function (point) {
17569 if (point.redraw) {
17570 point.redraw();
17571 }
17572 });*/
17573
17574 // draw the data labels (inn pies they go before the points)
17575 if (series.drawDataLabels) {
17576 series.drawDataLabels();
17577 }
17578
17579 // draw the points
17580 if (series.visible) {
17581 series.drawPoints();
17582 }
17583
17584
17585 // draw the mouse tracking area
17586 if (series.drawTracker && series.options.enableMouseTracking !== false) {
17587 series.drawTracker();
17588 }
17589
17590 // Handle inverted series and tracker groups
17591 series.invertGroups(inverted);
17592
17593 // Initial clipping, must be defined after inverting groups for VML. Applies to columns etc. (#3839).
17594 if (options.clip !== false && !series.sharedClipKey && !hasRendered) {
17595 group.clip(chart.clipRect);
17596 }
17597
17598 // Run the animation
17599 if (animDuration) {
17600 series.animate();
17601 }
17602
17603 // Call the afterAnimate function on animation complete (but don't overwrite the animation.complete option
17604 // which should be available to the user).
17605 if (!hasRendered) {
17606 series.animationTimeout = syncTimeout(function() {
17607 series.afterAnimate();
17608 }, animDuration);
17609 }
17610
17611 series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
17612 // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
17613 series.hasRendered = true;
17614 },
17615
17616 /**
17617 * Redraw the series after an update in the axes.
17618 */
17619 redraw: function() {
17620 var series = this,
17621 chart = series.chart,
17622 wasDirty = series.isDirty || series.isDirtyData, // cache it here as it is set to false in render, but used after
17623 group = series.group,
17624 xAxis = series.xAxis,
17625 yAxis = series.yAxis;
17626
17627 // reposition on resize
17628 if (group) {
17629 if (chart.inverted) {
17630 group.attr({
17631 width: chart.plotWidth,
17632 height: chart.plotHeight
17633 });
17634 }
17635
17636 group.animate({
17637 translateX: pick(xAxis && xAxis.left, chart.plotLeft),
17638 translateY: pick(yAxis && yAxis.top, chart.plotTop)
17639 });
17640 }
17641
17642 series.translate();
17643 series.render();
17644 if (wasDirty) { // #3868, #3945
17645 delete this.kdTree;
17646 }
17647 },
17648
17649 /**
17650 * KD Tree && PointSearching Implementation
17651 */
17652
17653 kdDimensions: 1,
17654 kdAxisArray: ['clientX', 'plotY'],
17655
17656 searchPoint: function(e, compareX) {
17657 var series = this,
17658 xAxis = series.xAxis,
17659 yAxis = series.yAxis,
17660 inverted = series.chart.inverted;
17661
17662 return this.searchKDTree({
17663 clientX: inverted ? xAxis.len - e.chartY + xAxis.pos : e.chartX - xAxis.pos,
17664 plotY: inverted ? yAxis.len - e.chartX + yAxis.pos : e.chartY - yAxis.pos
17665 }, compareX);
17666 },
17667
17668 buildKDTree: function() {
17669 var series = this,
17670 dimensions = series.kdDimensions;
17671
17672 // Internal function
17673 function _kdtree(points, depth, dimensions) {
17674 var axis,
17675 median,
17676 length = points && points.length;
17677
17678 if (length) {
17679
17680 // alternate between the axis
17681 axis = series.kdAxisArray[depth % dimensions];
17682
17683 // sort point array
17684 points.sort(function(a, b) {
17685 return a[axis] - b[axis];
17686 });
17687
17688 median = Math.floor(length / 2);
17689
17690 // build and return nod
17691 return {
17692 point: points[median],
17693 left: _kdtree(points.slice(0, median), depth + 1, dimensions),
17694 right: _kdtree(points.slice(median + 1), depth + 1, dimensions)
17695 };
17696
17697 }
17698 }
17699
17700 // Start the recursive build process with a clone of the points array and null points filtered out (#3873)
17701 function startRecursive() {
17702 series.kdTree = _kdtree(
17703 series.getValidPoints(
17704 null, !series.directTouch // For line-type series restrict to plot area, but column-type series not (#3916, #4511)
17705 ),
17706 dimensions,
17707 dimensions
17708 );
17709 }
17710 delete series.kdTree;
17711
17712 // For testing tooltips, don't build async
17713 syncTimeout(startRecursive, series.options.kdNow ? 0 : 1);
17714 },
17715
17716 searchKDTree: function(point, compareX) {
17717 var series = this,
17718 kdX = this.kdAxisArray[0],
17719 kdY = this.kdAxisArray[1],
17720 kdComparer = compareX ? 'distX' : 'dist';
17721
17722 // Set the one and two dimensional distance on the point object
17723 function setDistance(p1, p2) {
17724 var x = (defined(p1[kdX]) && defined(p2[kdX])) ? Math.pow(p1[kdX] - p2[kdX], 2) : null,
17725 y = (defined(p1[kdY]) && defined(p2[kdY])) ? Math.pow(p1[kdY] - p2[kdY], 2) : null,
17726 r = (x || 0) + (y || 0);
17727
17728 p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE;
17729 p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE;
17730 }
17731
17732 function _search(search, tree, depth, dimensions) {
17733 var point = tree.point,
17734 axis = series.kdAxisArray[depth % dimensions],
17735 tdist,
17736 sideA,
17737 sideB,
17738 ret = point,
17739 nPoint1,
17740 nPoint2;
17741
17742 setDistance(search, point);
17743
17744 // Pick side based on distance to splitting point
17745 tdist = search[axis] - point[axis];
17746 sideA = tdist < 0 ? 'left' : 'right';
17747 sideB = tdist < 0 ? 'right' : 'left';
17748
17749 // End of tree
17750 if (tree[sideA]) {
17751 nPoint1 = _search(search, tree[sideA], depth + 1, dimensions);
17752
17753 ret = (nPoint1[kdComparer] < ret[kdComparer] ? nPoint1 : point);
17754 }
17755 if (tree[sideB]) {
17756 // compare distance to current best to splitting point to decide wether to check side B or not
17757 if (Math.sqrt(tdist * tdist) < ret[kdComparer]) {
17758 nPoint2 = _search(search, tree[sideB], depth + 1, dimensions);
17759 ret = (nPoint2[kdComparer] < ret[kdComparer] ? nPoint2 : ret);
17760 }
17761 }
17762
17763 return ret;
17764 }
17765
17766 if (!this.kdTree) {
17767 this.buildKDTree();
17768 }
17769
17770 if (this.kdTree) {
17771 return _search(point,
17772 this.kdTree, this.kdDimensions, this.kdDimensions);
17773 }
17774 }
17775
17776 }); // end Series prototype
17777
17778 }(Highcharts));
17779 (function(H) {
17780 /**
17781 * (c) 2010-2016 Torstein Honsi
17782 *
17783 * License: www.highcharts.com/license
17784 */
17785 'use strict';
17786 var addEvent = H.addEvent,
17787 animate = H.animate,
17788 Axis = H.Axis,
17789 Chart = H.Chart,
17790 createElement = H.createElement,
17791 css = H.css,
17792 defined = H.defined,
17793 each = H.each,
17794 erase = H.erase,
17795 extend = H.extend,
17796 fireEvent = H.fireEvent,
17797 inArray = H.inArray,
17798 isNumber = H.isNumber,
17799 isObject = H.isObject,
17800 merge = H.merge,
17801 pick = H.pick,
17802 Point = H.Point,
17803 Series = H.Series,
17804 seriesTypes = H.seriesTypes,
17805 setAnimation = H.setAnimation,
17806 splat = H.splat;
17807
17808 // Extend the Chart prototype for dynamic methods
17809 extend(Chart.prototype, /** @lends Highcharts.Chart.prototype */ {
17810
17811 /**
17812 * Add a series dynamically after time
17813 *
17814 * @param {Object} options The config options
17815 * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true.
17816 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
17817 * configuration
17818 *
17819 * @return {Object} series The newly created series object
17820 */
17821 addSeries: function(options, redraw, animation) {
17822 var series,
17823 chart = this;
17824
17825 if (options) {
17826 redraw = pick(redraw, true); // defaults to true
17827
17828 fireEvent(chart, 'addSeries', {
17829 options: options
17830 }, function() {
17831 series = chart.initSeries(options);
17832
17833 chart.isDirtyLegend = true; // the series array is out of sync with the display
17834 chart.linkSeries();
17835 if (redraw) {
17836 chart.redraw(animation);
17837 }
17838 });
17839 }
17840
17841 return series;
17842 },
17843
17844 /**
17845 * Add an axis to the chart
17846 * @param {Object} options The axis option
17847 * @param {Boolean} isX Whether it is an X axis or a value axis
17848 */
17849 addAxis: function(options, isX, redraw, animation) {
17850 var key = isX ? 'xAxis' : 'yAxis',
17851 chartOptions = this.options,
17852 userOptions = merge(options, {
17853 index: this[key].length,
17854 isX: isX
17855 });
17856
17857 new Axis(this, userOptions); // eslint-disable-line no-new
17858
17859 // Push the new axis options to the chart options
17860 chartOptions[key] = splat(chartOptions[key] || {});
17861 chartOptions[key].push(userOptions);
17862
17863 if (pick(redraw, true)) {
17864 this.redraw(animation);
17865 }
17866 },
17867
17868 /**
17869 * Dim the chart and show a loading text or symbol
17870 * @param {String} str An optional text to show in the loading label instead of the default one
17871 */
17872 showLoading: function(str) {
17873 var chart = this,
17874 options = chart.options,
17875 loadingDiv = chart.loadingDiv,
17876 loadingOptions = options.loading,
17877 setLoadingSize = function() {
17878 if (loadingDiv) {
17879 css(loadingDiv, {
17880 left: chart.plotLeft + 'px',
17881 top: chart.plotTop + 'px',
17882 width: chart.plotWidth + 'px',
17883 height: chart.plotHeight + 'px'
17884 });
17885 }
17886 };
17887
17888 // create the layer at the first call
17889 if (!loadingDiv) {
17890 chart.loadingDiv = loadingDiv = createElement('div', {
17891 className: 'highcharts-loading highcharts-loading-hidden'
17892 }, null, chart.container);
17893
17894 chart.loadingSpan = createElement(
17895 'span', {
17896 className: 'highcharts-loading-inner'
17897 },
17898 null,
17899 loadingDiv
17900 );
17901 addEvent(chart, 'redraw', setLoadingSize); // #1080
17902 }
17903
17904 loadingDiv.className = 'highcharts-loading';
17905
17906 // Update text
17907 chart.loadingSpan.innerHTML = str || options.lang.loading;
17908
17909
17910 // Update visuals
17911 css(loadingDiv, extend(loadingOptions.style, {
17912 zIndex: 10
17913 }));
17914 css(chart.loadingSpan, loadingOptions.labelStyle);
17915
17916 // Show it
17917 if (!chart.loadingShown) {
17918 css(loadingDiv, {
17919 opacity: 0,
17920 display: ''
17921 });
17922 animate(loadingDiv, {
17923 opacity: loadingOptions.style.opacity || 0.5
17924 }, {
17925 duration: loadingOptions.showDuration || 0
17926 });
17927 }
17928
17929
17930 chart.loadingShown = true;
17931 setLoadingSize();
17932 },
17933
17934 /**
17935 * Hide the loading layer
17936 */
17937 hideLoading: function() {
17938 var options = this.options,
17939 loadingDiv = this.loadingDiv;
17940
17941 if (loadingDiv) {
17942 loadingDiv.className = 'highcharts-loading highcharts-loading-hidden';
17943
17944 animate(loadingDiv, {
17945 opacity: 0
17946 }, {
17947 duration: options.loading.hideDuration || 100,
17948 complete: function() {
17949 css(loadingDiv, {
17950 display: 'none'
17951 });
17952 }
17953 });
17954
17955 }
17956 this.loadingShown = false;
17957 },
17958
17959 /**
17960 * These properties cause isDirtyBox to be set to true when updating. Can be extended from plugins.
17961 */
17962 propsRequireDirtyBox: ['backgroundColor', 'borderColor', 'borderWidth', 'margin', 'marginTop', 'marginRight',
17963 'marginBottom', 'marginLeft', 'spacing', 'spacingTop', 'spacingRight', 'spacingBottom', 'spacingLeft',
17964 'borderRadius', 'plotBackgroundColor', 'plotBackgroundImage', 'plotBorderColor', 'plotBorderWidth',
17965 'plotShadow', 'shadow'
17966 ],
17967
17968 /**
17969 * These properties cause all series to be updated when updating. Can be
17970 * extended from plugins.
17971 */
17972 propsRequireUpdateSeries: ['chart.inverted', 'chart.polar',
17973 'chart.ignoreHiddenSeries', 'chart.type', 'colors', 'plotOptions'
17974 ],
17975
17976 /**
17977 * Chart.update function that takes the whole options stucture.
17978 */
17979 update: function(options, redraw) {
17980 var key,
17981 adders = {
17982 credits: 'addCredits',
17983 title: 'setTitle',
17984 subtitle: 'setSubtitle'
17985 },
17986 optionsChart = options.chart,
17987 updateAllAxes,
17988 updateAllSeries,
17989 newWidth,
17990 newHeight;
17991
17992 // If the top-level chart option is present, some special updates are required
17993 if (optionsChart) {
17994 merge(true, this.options.chart, optionsChart);
17995
17996 // Setter function
17997 if ('className' in optionsChart) {
17998 this.setClassName(optionsChart.className);
17999 }
18000
18001 if ('inverted' in optionsChart || 'polar' in optionsChart) {
18002 this.propFromSeries(); // Parses options.chart.inverted and options.chart.polar together with the available series
18003 updateAllAxes = true;
18004 }
18005
18006 for (key in optionsChart) {
18007 if (optionsChart.hasOwnProperty(key)) {
18008 if (inArray('chart.' + key, this.propsRequireUpdateSeries) !== -1) {
18009 updateAllSeries = true;
18010 }
18011 // Only dirty box
18012 if (inArray(key, this.propsRequireDirtyBox) !== -1) {
18013 this.isDirtyBox = true;
18014 }
18015
18016 }
18017 }
18018
18019
18020 if ('style' in optionsChart) {
18021 this.renderer.setStyle(optionsChart.style);
18022 }
18023
18024 }
18025
18026 // Some option stuctures correspond one-to-one to chart objects that have
18027 // update methods, for example
18028 // options.credits => chart.credits
18029 // options.legend => chart.legend
18030 // options.title => chart.title
18031 // options.tooltip => chart.tooltip
18032 // options.subtitle => chart.subtitle
18033 // options.navigator => chart.navigator
18034 // options.scrollbar => chart.scrollbar
18035 for (key in options) {
18036 if (this[key] && typeof this[key].update === 'function') {
18037 this[key].update(options[key], false);
18038
18039 // If a one-to-one object does not exist, look for an adder function
18040 } else if (typeof this[adders[key]] === 'function') {
18041 this[adders[key]](options[key]);
18042 }
18043
18044 if (key !== 'chart' && inArray(key, this.propsRequireUpdateSeries) !== -1) {
18045 updateAllSeries = true;
18046 }
18047 }
18048
18049
18050 if (options.colors) {
18051 this.options.colors = options.colors;
18052 }
18053
18054
18055 if (options.plotOptions) {
18056 merge(true, this.options.plotOptions, options.plotOptions);
18057 }
18058
18059 // Setters for collections. For axes and series, each item is referred by an id. If the
18060 // id is not found, it defaults to the first item in the collection, so setting series
18061 // without an id, will update the first series in the chart.
18062 each(['xAxis', 'yAxis', 'series'], function(coll) {
18063 if (options[coll]) {
18064 each(splat(options[coll]), function(newOptions) {
18065 var item = (defined(newOptions.id) && this.get(newOptions.id)) || this[coll][0];
18066 if (item && item.coll === coll) {
18067 item.update(newOptions, false);
18068 }
18069 }, this);
18070 }
18071 }, this);
18072
18073 if (updateAllAxes) {
18074 each(this.axes, function(axis) {
18075 axis.update({}, false);
18076 });
18077 }
18078
18079 // Certain options require the whole series structure to be thrown away
18080 // and rebuilt
18081 if (updateAllSeries) {
18082 each(this.series, function(series) {
18083 series.update({}, false);
18084 });
18085 }
18086
18087 // For loading, just update the options, do not redraw
18088 if (options.loading) {
18089 merge(true, this.options.loading, options.loading);
18090 }
18091
18092 // Update size. Redraw is forced.
18093 newWidth = optionsChart && optionsChart.width;
18094 newHeight = optionsChart && optionsChart.height;
18095 if ((isNumber(newWidth) && newWidth !== this.chartWidth) ||
18096 (isNumber(newHeight) && newHeight !== this.chartHeight)) {
18097 this.setSize(newWidth, newHeight);
18098 } else if (pick(redraw, true)) {
18099 this.redraw();
18100 }
18101 },
18102
18103 /**
18104 * Setter function to allow use from chart.update
18105 */
18106 setSubtitle: function(options) {
18107 this.setTitle(undefined, options);
18108 }
18109
18110
18111 });
18112
18113 // extend the Point prototype for dynamic methods
18114 extend(Point.prototype, /** @lends Point.prototype */ {
18115 /**
18116 * Point.update with new options (typically x/y data) and optionally redraw the series.
18117 *
18118 * @param {Object} options Point options as defined in the series.data array
18119 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
18120 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
18121 * configuration
18122 */
18123 update: function(options, redraw, animation, runEvent) {
18124 var point = this,
18125 series = point.series,
18126 graphic = point.graphic,
18127 i,
18128 chart = series.chart,
18129 seriesOptions = series.options;
18130
18131 redraw = pick(redraw, true);
18132
18133 function update() {
18134
18135 point.applyOptions(options);
18136
18137 // Update visuals
18138 if (point.y === null && graphic) { // #4146
18139 point.graphic = graphic.destroy();
18140 }
18141 if (isObject(options, true)) {
18142 // Destroy so we can get new elements
18143 if (graphic && graphic.element) {
18144 if (options && options.marker && options.marker.symbol) {
18145 point.graphic = graphic.destroy();
18146 }
18147 }
18148 if (options && options.dataLabels && point.dataLabel) { // #2468
18149 point.dataLabel = point.dataLabel.destroy();
18150 }
18151 }
18152
18153 // record changes in the parallel arrays
18154 i = point.index;
18155 series.updateParallelArrays(point, i);
18156
18157 // Record the options to options.data. If there is an object from before,
18158 // use point options, otherwise use raw options. (#4701)
18159 seriesOptions.data[i] = isObject(seriesOptions.data[i], true) ? point.options : options;
18160
18161 // redraw
18162 series.isDirty = series.isDirtyData = true;
18163 if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320
18164 chart.isDirtyBox = true;
18165 }
18166
18167 if (seriesOptions.legendType === 'point') { // #1831, #1885
18168 chart.isDirtyLegend = true;
18169 }
18170 if (redraw) {
18171 chart.redraw(animation);
18172 }
18173 }
18174
18175 // Fire the event with a default handler of doing the update
18176 if (runEvent === false) { // When called from setData
18177 update();
18178 } else {
18179 point.firePointEvent('update', {
18180 options: options
18181 }, update);
18182 }
18183 },
18184
18185 /**
18186 * Remove a point and optionally redraw the series and if necessary the axes
18187 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
18188 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
18189 * configuration
18190 */
18191 remove: function(redraw, animation) {
18192 this.series.removePoint(inArray(this, this.series.data), redraw, animation);
18193 }
18194 });
18195
18196 // Extend the series prototype for dynamic methods
18197 extend(Series.prototype, /** @lends Series.prototype */ {
18198 /**
18199 * Add a point dynamically after chart load time
18200 * @param {Object} options Point options as given in series.data
18201 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
18202 * @param {Boolean} shift If shift is true, a point is shifted off the start
18203 * of the series as one is appended to the end.
18204 * @param {Boolean|AnimationOptions} animation Whether to apply animation, and optionally animation
18205 * configuration
18206 */
18207 addPoint: function(options, redraw, shift, animation) {
18208 var series = this,
18209 seriesOptions = series.options,
18210 data = series.data,
18211 chart = series.chart,
18212 names = series.xAxis && series.xAxis.names,
18213 dataOptions = seriesOptions.data,
18214 point,
18215 isInTheMiddle,
18216 xData = series.xData,
18217 i,
18218 x;
18219
18220 // Optional redraw, defaults to true
18221 redraw = pick(redraw, true);
18222
18223 // Get options and push the point to xData, yData and series.options. In series.generatePoints
18224 // the Point instance will be created on demand and pushed to the series.data array.
18225 point = {
18226 series: series
18227 };
18228 series.pointClass.prototype.applyOptions.apply(point, [options]);
18229 x = point.x;
18230
18231 // Get the insertion point
18232 i = xData.length;
18233 if (series.requireSorting && x < xData[i - 1]) {
18234 isInTheMiddle = true;
18235 while (i && xData[i - 1] > x) {
18236 i--;
18237 }
18238 }
18239
18240 series.updateParallelArrays(point, 'splice', i, 0, 0); // insert undefined item
18241 series.updateParallelArrays(point, i); // update it
18242
18243 if (names && point.name) {
18244 names[x] = point.name;
18245 }
18246 dataOptions.splice(i, 0, options);
18247
18248 if (isInTheMiddle) {
18249 series.data.splice(i, 0, null);
18250 series.processData();
18251 }
18252
18253 // Generate points to be added to the legend (#1329)
18254 if (seriesOptions.legendType === 'point') {
18255 series.generatePoints();
18256 }
18257
18258 // Shift the first point off the parallel arrays
18259 if (shift) {
18260 if (data[0] && data[0].remove) {
18261 data[0].remove(false);
18262 } else {
18263 data.shift();
18264 series.updateParallelArrays(point, 'shift');
18265
18266 dataOptions.shift();
18267 }
18268 }
18269
18270 // redraw
18271 series.isDirty = true;
18272 series.isDirtyData = true;
18273
18274 if (redraw) {
18275
18276 chart.redraw(animation); // Animation is set anyway on redraw, #5665
18277 }
18278 },
18279
18280 /**
18281 * Remove a point (rendered or not), by index
18282 */
18283 removePoint: function(i, redraw, animation) {
18284
18285 var series = this,
18286 data = series.data,
18287 point = data[i],
18288 points = series.points,
18289 chart = series.chart,
18290 remove = function() {
18291
18292 if (points && points.length === data.length) { // #4935
18293 points.splice(i, 1);
18294 }
18295 data.splice(i, 1);
18296 series.options.data.splice(i, 1);
18297 series.updateParallelArrays(point || {
18298 series: series
18299 }, 'splice', i, 1);
18300
18301 if (point) {
18302 point.destroy();
18303 }
18304
18305 // redraw
18306 series.isDirty = true;
18307 series.isDirtyData = true;
18308 if (redraw) {
18309 chart.redraw();
18310 }
18311 };
18312
18313 setAnimation(animation, chart);
18314 redraw = pick(redraw, true);
18315
18316 // Fire the event with a default handler of removing the point
18317 if (point) {
18318 point.firePointEvent('remove', null, remove);
18319 } else {
18320 remove();
18321 }
18322 },
18323
18324 /**
18325 * Remove a series and optionally redraw the chart
18326 *
18327 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
18328 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
18329 * configuration
18330 */
18331 remove: function(redraw, animation, withEvent) {
18332 var series = this,
18333 chart = series.chart;
18334
18335 function remove() {
18336
18337 // Destroy elements
18338 series.destroy();
18339
18340 // Redraw
18341 chart.isDirtyLegend = chart.isDirtyBox = true;
18342 chart.linkSeries();
18343
18344 if (pick(redraw, true)) {
18345 chart.redraw(animation);
18346 }
18347 }
18348
18349 // Fire the event with a default handler of removing the point
18350 if (withEvent !== false) {
18351 fireEvent(series, 'remove', null, remove);
18352 } else {
18353 remove();
18354 }
18355 },
18356
18357 /**
18358 * Series.update with a new set of options
18359 */
18360 update: function(newOptions, redraw) {
18361 var series = this,
18362 chart = this.chart,
18363 // must use user options when changing type because this.options is merged
18364 // in with type specific plotOptions
18365 oldOptions = this.userOptions,
18366 oldType = this.type,
18367 newType = newOptions.type || oldOptions.type || chart.options.chart.type,
18368 proto = seriesTypes[oldType].prototype,
18369 preserve = ['group', 'markerGroup', 'dataLabelsGroup'],
18370 n;
18371
18372 // If we're changing type or zIndex, create new groups (#3380, #3404)
18373 if ((newType && newType !== oldType) || newOptions.zIndex !== undefined) {
18374 preserve.length = 0;
18375 }
18376
18377 // Make sure groups are not destroyed (#3094)
18378 each(preserve, function(prop) {
18379 preserve[prop] = series[prop];
18380 delete series[prop];
18381 });
18382
18383 // Do the merge, with some forced options
18384 newOptions = merge(oldOptions, {
18385 animation: false,
18386 index: this.index,
18387 pointStart: this.xData[0] // when updating after addPoint
18388 }, {
18389 data: this.options.data
18390 }, newOptions);
18391
18392 // Destroy the series and delete all properties. Reinsert all methods
18393 // and properties from the new type prototype (#2270, #3719)
18394 this.remove(false, null, false);
18395 for (n in proto) {
18396 this[n] = undefined;
18397 }
18398 extend(this, seriesTypes[newType || oldType].prototype);
18399
18400 // Re-register groups (#3094)
18401 each(preserve, function(prop) {
18402 series[prop] = preserve[prop];
18403 });
18404
18405 this.init(chart, newOptions);
18406 chart.linkSeries(); // Links are lost in this.remove (#3028)
18407 if (pick(redraw, true)) {
18408 chart.redraw(false);
18409 }
18410 }
18411 });
18412
18413 // Extend the Axis.prototype for dynamic methods
18414 extend(Axis.prototype, /** @lends Axis.prototype */ {
18415
18416 /**
18417 * Axis.update with a new options structure
18418 */
18419 update: function(newOptions, redraw) {
18420 var chart = this.chart;
18421
18422 newOptions = chart.options[this.coll][this.options.index] = merge(this.userOptions, newOptions);
18423
18424 this.destroy(true);
18425
18426 this.init(chart, extend(newOptions, {
18427 events: undefined
18428 }));
18429
18430 chart.isDirtyBox = true;
18431 if (pick(redraw, true)) {
18432 chart.redraw();
18433 }
18434 },
18435
18436 /**
18437 * Remove the axis from the chart
18438 */
18439 remove: function(redraw) {
18440 var chart = this.chart,
18441 key = this.coll, // xAxis or yAxis
18442 axisSeries = this.series,
18443 i = axisSeries.length;
18444
18445 // Remove associated series (#2687)
18446 while (i--) {
18447 if (axisSeries[i]) {
18448 axisSeries[i].remove(false);
18449 }
18450 }
18451
18452 // Remove the axis
18453 erase(chart.axes, this);
18454 erase(chart[key], this);
18455 chart.options[key].splice(this.options.index, 1);
18456 each(chart[key], function(axis, i) { // Re-index, #1706
18457 axis.options.index = i;
18458 });
18459 this.destroy();
18460 chart.isDirtyBox = true;
18461
18462 if (pick(redraw, true)) {
18463 chart.redraw();
18464 }
18465 },
18466
18467 /**
18468 * Update the axis title by options
18469 */
18470 setTitle: function(newTitleOptions, redraw) {
18471 this.update({
18472 title: newTitleOptions
18473 }, redraw);
18474 },
18475
18476 /**
18477 * Set new axis categories and optionally redraw
18478 * @param {Array} categories
18479 * @param {Boolean} redraw
18480 */
18481 setCategories: function(categories, redraw) {
18482 this.update({
18483 categories: categories
18484 }, redraw);
18485 }
18486
18487 });
18488
18489 }(Highcharts));
18490 (function(H) {
18491 /**
18492 * (c) 2010-2016 Torstein Honsi
18493 *
18494 * License: www.highcharts.com/license
18495 */
18496 'use strict';
18497 var animObject = H.animObject,
18498 color = H.color,
18499 each = H.each,
18500 extend = H.extend,
18501 isNumber = H.isNumber,
18502 LegendSymbolMixin = H.LegendSymbolMixin,
18503 merge = H.merge,
18504 noop = H.noop,
18505 pick = H.pick,
18506 Series = H.Series,
18507 seriesType = H.seriesType,
18508 svg = H.svg;
18509 /**
18510 * The column series type.
18511 *
18512 * @constructor seriesTypes.column
18513 * @augments Series
18514 */
18515 seriesType('column', 'line', {
18516 borderRadius: 0,
18517 //colorByPoint: undefined,
18518 groupPadding: 0.2,
18519 //grouping: true,
18520 marker: null, // point options are specified in the base options
18521 pointPadding: 0.1,
18522 //pointWidth: null,
18523 minPointLength: 0,
18524 cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes
18525 pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories
18526 states: {
18527 hover: {
18528 halo: false,
18529
18530 brightness: 0.1,
18531 shadow: false
18532
18533 },
18534
18535 select: {
18536 color: '#cccccc',
18537 borderColor: '#000000',
18538 shadow: false
18539 }
18540
18541 },
18542 dataLabels: {
18543 align: null, // auto
18544 verticalAlign: null, // auto
18545 y: null
18546 },
18547 softThreshold: false,
18548 startFromThreshold: true, // false doesn't work well: http://jsfiddle.net/highcharts/hz8fopan/14/
18549 stickyTracking: false,
18550 tooltip: {
18551 distance: 6
18552 },
18553 threshold: 0,
18554
18555 borderColor: '#ffffff'
18556 // borderWidth: 1
18557
18558
18559 }, /** @lends seriesTypes.column.prototype */ {
18560 cropShoulder: 0,
18561 directTouch: true, // When tooltip is not shared, this series (and derivatives) requires direct touch/hover. KD-tree does not apply.
18562 trackerGroups: ['group', 'dataLabelsGroup'],
18563 negStacks: true, // use separate negative stacks, unlike area stacks where a negative
18564 // point is substracted from previous (#1910)
18565
18566 /**
18567 * Initialize the series. Extends the basic Series.init method by
18568 * marking other series of the same type as dirty.
18569 *
18570 * @function #init
18571 * @memberOf seriesTypes.column
18572 * @returns {void}
18573 */
18574 init: function() {
18575 Series.prototype.init.apply(this, arguments);
18576
18577 var series = this,
18578 chart = series.chart;
18579
18580 // if the series is added dynamically, force redraw of other
18581 // series affected by a new column
18582 if (chart.hasRendered) {
18583 each(chart.series, function(otherSeries) {
18584 if (otherSeries.type === series.type) {
18585 otherSeries.isDirty = true;
18586 }
18587 });
18588 }
18589 },
18590
18591 /**
18592 * Return the width and x offset of the columns adjusted for grouping, groupPadding, pointPadding,
18593 * pointWidth etc.
18594 */
18595 getColumnMetrics: function() {
18596
18597 var series = this,
18598 options = series.options,
18599 xAxis = series.xAxis,
18600 yAxis = series.yAxis,
18601 reversedXAxis = xAxis.reversed,
18602 stackKey,
18603 stackGroups = {},
18604 columnCount = 0;
18605
18606 // Get the total number of column type series.
18607 // This is called on every series. Consider moving this logic to a
18608 // chart.orderStacks() function and call it on init, addSeries and removeSeries
18609 if (options.grouping === false) {
18610 columnCount = 1;
18611 } else {
18612 each(series.chart.series, function(otherSeries) {
18613 var otherOptions = otherSeries.options,
18614 otherYAxis = otherSeries.yAxis,
18615 columnIndex;
18616 if (otherSeries.type === series.type && otherSeries.visible &&
18617 yAxis.len === otherYAxis.len && yAxis.pos === otherYAxis.pos) { // #642, #2086
18618 if (otherOptions.stacking) {
18619 stackKey = otherSeries.stackKey;
18620 if (stackGroups[stackKey] === undefined) {
18621 stackGroups[stackKey] = columnCount++;
18622 }
18623 columnIndex = stackGroups[stackKey];
18624 } else if (otherOptions.grouping !== false) { // #1162
18625 columnIndex = columnCount++;
18626 }
18627 otherSeries.columnIndex = columnIndex;
18628 }
18629 });
18630 }
18631
18632 var categoryWidth = Math.min(
18633 Math.abs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || xAxis.tickInterval || 1), // #2610
18634 xAxis.len // #1535
18635 ),
18636 groupPadding = categoryWidth * options.groupPadding,
18637 groupWidth = categoryWidth - 2 * groupPadding,
18638 pointOffsetWidth = groupWidth / columnCount,
18639 pointWidth = Math.min(
18640 options.maxPointWidth || xAxis.len,
18641 pick(options.pointWidth, pointOffsetWidth * (1 - 2 * options.pointPadding))
18642 ),
18643 pointPadding = (pointOffsetWidth - pointWidth) / 2,
18644 colIndex = (series.columnIndex || 0) + (reversedXAxis ? 1 : 0), // #1251, #3737
18645 pointXOffset = pointPadding + (groupPadding + colIndex *
18646 pointOffsetWidth - (categoryWidth / 2)) *
18647 (reversedXAxis ? -1 : 1);
18648
18649 // Save it for reading in linked series (Error bars particularly)
18650 series.columnMetrics = {
18651 width: pointWidth,
18652 offset: pointXOffset
18653 };
18654 return series.columnMetrics;
18655
18656 },
18657
18658 /**
18659 * Make the columns crisp. The edges are rounded to the nearest full pixel.
18660 */
18661 crispCol: function(x, y, w, h) {
18662 var chart = this.chart,
18663 borderWidth = this.borderWidth,
18664 xCrisp = -(borderWidth % 2 ? 0.5 : 0),
18665 yCrisp = borderWidth % 2 ? 0.5 : 1,
18666 right,
18667 bottom,
18668 fromTop;
18669
18670 if (chart.inverted && chart.renderer.isVML) {
18671 yCrisp += 1;
18672 }
18673
18674 // Horizontal. We need to first compute the exact right edge, then round it
18675 // and compute the width from there.
18676 right = Math.round(x + w) + xCrisp;
18677 x = Math.round(x) + xCrisp;
18678 w = right - x;
18679
18680 // Vertical
18681 bottom = Math.round(y + h) + yCrisp;
18682 fromTop = Math.abs(y) <= 0.5 && bottom > 0.5; // #4504, #4656
18683 y = Math.round(y) + yCrisp;
18684 h = bottom - y;
18685
18686 // Top edges are exceptions
18687 if (fromTop && h) { // #5146
18688 y -= 1;
18689 h += 1;
18690 }
18691
18692 return {
18693 x: x,
18694 y: y,
18695 width: w,
18696 height: h
18697 };
18698 },
18699
18700 /**
18701 * Translate each point to the plot area coordinate system and find shape positions
18702 */
18703 translate: function() {
18704 var series = this,
18705 chart = series.chart,
18706 options = series.options,
18707 dense = series.dense = series.closestPointRange * series.xAxis.transA < 2,
18708 borderWidth = series.borderWidth = pick(
18709 options.borderWidth,
18710 dense ? 0 : 1 // #3635
18711 ),
18712 yAxis = series.yAxis,
18713 threshold = options.threshold,
18714 translatedThreshold = series.translatedThreshold = yAxis.getThreshold(threshold),
18715 minPointLength = pick(options.minPointLength, 5),
18716 metrics = series.getColumnMetrics(),
18717 pointWidth = metrics.width,
18718 seriesBarW = series.barW = Math.max(pointWidth, 1 + 2 * borderWidth), // postprocessed for border width
18719 pointXOffset = series.pointXOffset = metrics.offset;
18720
18721 if (chart.inverted) {
18722 translatedThreshold -= 0.5; // #3355
18723 }
18724
18725 // When the pointPadding is 0, we want the columns to be packed tightly, so we allow individual
18726 // columns to have individual sizes. When pointPadding is greater, we strive for equal-width
18727 // columns (#2694).
18728 if (options.pointPadding) {
18729 seriesBarW = Math.ceil(seriesBarW);
18730 }
18731
18732 Series.prototype.translate.apply(series);
18733
18734 // Record the new values
18735 each(series.points, function(point) {
18736 var yBottom = pick(point.yBottom, translatedThreshold),
18737 safeDistance = 999 + Math.abs(yBottom),
18738 plotY = Math.min(Math.max(-safeDistance, point.plotY), yAxis.len + safeDistance), // Don't draw too far outside plot area (#1303, #2241, #4264)
18739 barX = point.plotX + pointXOffset,
18740 barW = seriesBarW,
18741 barY = Math.min(plotY, yBottom),
18742 up,
18743 barH = Math.max(plotY, yBottom) - barY;
18744
18745 // Handle options.minPointLength
18746 if (Math.abs(barH) < minPointLength) {
18747 if (minPointLength) {
18748 barH = minPointLength;
18749 up = (!yAxis.reversed && !point.negative) || (yAxis.reversed && point.negative);
18750 barY = Math.abs(barY - translatedThreshold) > minPointLength ? // stacked
18751 yBottom - minPointLength : // keep position
18752 translatedThreshold - (up ? minPointLength : 0); // #1485, #4051
18753 }
18754 }
18755
18756 // Cache for access in polar
18757 point.barX = barX;
18758 point.pointWidth = pointWidth;
18759
18760 // Fix the tooltip on center of grouped columns (#1216, #424, #3648)
18761 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];
18762
18763 // Register shape type and arguments to be used in drawPoints
18764 point.shapeType = 'rect';
18765 point.shapeArgs = series.crispCol.apply(
18766 series,
18767 point.isNull ? [point.plotX, yAxis.len / 2, 0, 0] : // #3169, drilldown from null must have a position to work from
18768 [barX, barY, barW, barH]
18769 );
18770 });
18771
18772 },
18773
18774 getSymbol: noop,
18775
18776 /**
18777 * Use a solid rectangle like the area series types
18778 */
18779 drawLegendSymbol: LegendSymbolMixin.drawRectangle,
18780
18781
18782 /**
18783 * Columns have no graph
18784 */
18785 drawGraph: function() {
18786 this.group[this.dense ? 'addClass' : 'removeClass']('highcharts-dense-data');
18787 },
18788
18789
18790 /**
18791 * Get presentational attributes
18792 */
18793 pointAttribs: function(point, state) {
18794 var options = this.options,
18795 stateOptions,
18796 ret,
18797 p2o = this.pointAttrToOptions || {},
18798 strokeOption = p2o.stroke || 'borderColor',
18799 strokeWidthOption = p2o['stroke-width'] || 'borderWidth',
18800 fill = (point && point.color) || this.color,
18801 stroke = point[strokeOption] || options[strokeOption] ||
18802 this.color || fill, // set to fill when borderColor null
18803 dashstyle = options.dashStyle,
18804 zone,
18805 brightness;
18806
18807 // Handle zone colors
18808 if (point && this.zones.length) {
18809 zone = point.getZone();
18810 fill = (zone && zone.color) || point.options.color || this.color; // When zones are present, don't use point.color (#4267)
18811 }
18812
18813 // Select or hover states
18814 if (state) {
18815 stateOptions = options.states[state];
18816 brightness = stateOptions.brightness;
18817 fill = stateOptions.color ||
18818 (brightness !== undefined && color(fill).brighten(stateOptions.brightness).get()) ||
18819 fill;
18820 stroke = stateOptions[strokeOption] || stroke;
18821 dashstyle = stateOptions.dashStyle || dashstyle;
18822 }
18823
18824 ret = {
18825 'fill': fill,
18826 'stroke': stroke,
18827 'stroke-width': point[strokeWidthOption] || options[strokeWidthOption] || this[strokeWidthOption] || 0
18828 };
18829 if (options.borderRadius) {
18830 ret.r = options.borderRadius;
18831 }
18832
18833 if (dashstyle) {
18834 ret.dashstyle = dashstyle;
18835 }
18836
18837 return ret;
18838 },
18839
18840
18841 /**
18842 * Draw the columns. For bars, the series.group is rotated, so the same coordinates
18843 * apply for columns and bars. This method is inherited by scatter series.
18844 *
18845 */
18846 drawPoints: function() {
18847 var series = this,
18848 chart = this.chart,
18849 options = series.options,
18850 renderer = chart.renderer,
18851 animationLimit = options.animationLimit || 250,
18852 shapeArgs;
18853
18854 // draw the columns
18855 each(series.points, function(point) {
18856 var plotY = point.plotY,
18857 graphic = point.graphic;
18858
18859 if (isNumber(plotY) && point.y !== null) {
18860 shapeArgs = point.shapeArgs;
18861
18862 if (graphic) { // update
18863 graphic[chart.pointCount < animationLimit ? 'animate' : 'attr'](
18864 merge(shapeArgs)
18865 );
18866
18867 } else {
18868 point.graphic = graphic = renderer[point.shapeType](shapeArgs)
18869 .attr({
18870 'class': point.getClassName()
18871 })
18872 .add(point.group || series.group);
18873 }
18874
18875
18876 // Presentational
18877 graphic
18878 .attr(series.pointAttribs(point, point.selected && 'select'))
18879 .shadow(options.shadow, null, options.stacking && !options.borderRadius);
18880
18881
18882 } else if (graphic) {
18883 point.graphic = graphic.destroy(); // #1269
18884 }
18885 });
18886 },
18887
18888 /**
18889 * Animate the column heights one by one from zero
18890 * @param {Boolean} init Whether to initialize the animation or run it
18891 */
18892 animate: function(init) {
18893 var series = this,
18894 yAxis = this.yAxis,
18895 options = series.options,
18896 inverted = this.chart.inverted,
18897 attr = {},
18898 translatedThreshold;
18899
18900 if (svg) { // VML is too slow anyway
18901 if (init) {
18902 attr.scaleY = 0.001;
18903 translatedThreshold = Math.min(yAxis.pos + yAxis.len, Math.max(yAxis.pos, yAxis.toPixels(options.threshold)));
18904 if (inverted) {
18905 attr.translateX = translatedThreshold - yAxis.len;
18906 } else {
18907 attr.translateY = translatedThreshold;
18908 }
18909 series.group.attr(attr);
18910
18911 } else { // run the animation
18912
18913 attr[inverted ? 'translateX' : 'translateY'] = yAxis.pos;
18914 series.group.animate(attr, extend(animObject(series.options.animation), {
18915 // Do the scale synchronously to ensure smooth updating (#5030)
18916 step: function(val, fx) {
18917 series.group.attr({
18918 scaleY: Math.max(0.001, fx.pos) // #5250
18919 });
18920 }
18921 }));
18922
18923 // delete this function to allow it only once
18924 series.animate = null;
18925 }
18926 }
18927 },
18928
18929 /**
18930 * Remove this series from the chart
18931 */
18932 remove: function() {
18933 var series = this,
18934 chart = series.chart;
18935
18936 // column and bar series affects other series of the same type
18937 // as they are either stacked or grouped
18938 if (chart.hasRendered) {
18939 each(chart.series, function(otherSeries) {
18940 if (otherSeries.type === series.type) {
18941 otherSeries.isDirty = true;
18942 }
18943 });
18944 }
18945
18946 Series.prototype.remove.apply(series, arguments);
18947 }
18948 });
18949
18950 }(Highcharts));
18951 (function(H) {
18952 /**
18953 * (c) 2010-2016 Torstein Honsi
18954 *
18955 * License: www.highcharts.com/license
18956 */
18957 'use strict';
18958 var Series = H.Series,
18959 seriesType = H.seriesType;
18960 /**
18961 * The scatter series type
18962 */
18963 seriesType('scatter', 'line', {
18964 lineWidth: 0,
18965 marker: {
18966 enabled: true // Overrides auto-enabling in line series (#3647)
18967 },
18968 tooltip: {
18969 headerFormat: '<span style="color:{point.color}">\u25CF</span> <span style="font-size: 0.85em"> {series.name}</span><br/>',
18970 pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>'
18971 }
18972
18973 // Prototype members
18974 }, {
18975 sorted: false,
18976 requireSorting: false,
18977 noSharedTooltip: true,
18978 trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
18979 takeOrdinalPosition: false, // #2342
18980 kdDimensions: 2,
18981 drawGraph: function() {
18982 if (this.options.lineWidth) {
18983 Series.prototype.drawGraph.call(this);
18984 }
18985 }
18986 });
18987
18988 }(Highcharts));
18989 (function(H) {
18990 /**
18991 * (c) 2010-2016 Torstein Honsi
18992 *
18993 * License: www.highcharts.com/license
18994 */
18995 'use strict';
18996 var addEvent = H.addEvent,
18997 arrayMax = H.arrayMax,
18998 defined = H.defined,
18999 each = H.each,
19000 extend = H.extend,
19001 format = H.format,
19002 map = H.map,
19003 merge = H.merge,
19004 noop = H.noop,
19005 pick = H.pick,
19006 relativeLength = H.relativeLength,
19007 Series = H.Series,
19008 seriesTypes = H.seriesTypes,
19009 stableSort = H.stableSort;
19010
19011
19012 /**
19013 * Generatl distribution algorithm for distributing labels of differing size along a
19014 * confined length in two dimensions. The algorithm takes an array of objects containing
19015 * a size, a target and a rank. It will place the labels as close as possible to their
19016 * targets, skipping the lowest ranked labels if necessary.
19017 */
19018 H.distribute = function(boxes, len) {
19019
19020 var i,
19021 overlapping = true,
19022 origBoxes = boxes, // Original array will be altered with added .pos
19023 restBoxes = [], // The outranked overshoot
19024 box,
19025 target,
19026 total = 0;
19027
19028 function sortByTarget(a, b) {
19029 return a.target - b.target;
19030 }
19031
19032 // If the total size exceeds the len, remove those boxes with the lowest rank
19033 i = boxes.length;
19034 while (i--) {
19035 total += boxes[i].size;
19036 }
19037
19038 // Sort by rank, then slice away overshoot
19039 if (total > len) {
19040 stableSort(boxes, function(a, b) {
19041 return (b.rank || 0) - (a.rank || 0);
19042 });
19043 i = 0;
19044 total = 0;
19045 while (total <= len) {
19046 total += boxes[i].size;
19047 i++;
19048 }
19049 restBoxes = boxes.splice(i - 1, boxes.length);
19050 }
19051
19052 // Order by target
19053 stableSort(boxes, sortByTarget);
19054
19055
19056 // So far we have been mutating the original array. Now
19057 // create a copy with target arrays
19058 boxes = map(boxes, function(box) {
19059 return {
19060 size: box.size,
19061 targets: [box.target]
19062 };
19063 });
19064
19065 while (overlapping) {
19066 // Initial positions: target centered in box
19067 i = boxes.length;
19068 while (i--) {
19069 box = boxes[i];
19070 // Composite box, average of targets
19071 target = (Math.min.apply(0, box.targets) + Math.max.apply(0, box.targets)) / 2;
19072 box.pos = Math.min(Math.max(0, target - box.size / 2), len - box.size);
19073 }
19074
19075 // Detect overlap and join boxes
19076 i = boxes.length;
19077 overlapping = false;
19078 while (i--) {
19079 if (i > 0 && boxes[i - 1].pos + boxes[i - 1].size > boxes[i].pos) { // Overlap
19080 boxes[i - 1].size += boxes[i].size; // Add this size to the previous box
19081 boxes[i - 1].targets = boxes[i - 1].targets.concat(boxes[i].targets);
19082
19083 // Overlapping right, push left
19084 if (boxes[i - 1].pos + boxes[i - 1].size > len) {
19085 boxes[i - 1].pos = len - boxes[i - 1].size;
19086 }
19087 boxes.splice(i, 1); // Remove this item
19088 overlapping = true;
19089 }
19090 }
19091 }
19092
19093 // Now the composite boxes are placed, we need to put the original boxes within them
19094 i = 0;
19095 each(boxes, function(box) {
19096 var posInCompositeBox = 0;
19097 each(box.targets, function() {
19098 origBoxes[i].pos = box.pos + posInCompositeBox;
19099 posInCompositeBox += origBoxes[i].size;
19100 i++;
19101 });
19102 });
19103
19104 // Add the rest (hidden) boxes and sort by target
19105 origBoxes.push.apply(origBoxes, restBoxes);
19106 stableSort(origBoxes, sortByTarget);
19107 };
19108
19109
19110 /**
19111 * Draw the data labels
19112 */
19113 Series.prototype.drawDataLabels = function() {
19114
19115 var series = this,
19116 seriesOptions = series.options,
19117 options = seriesOptions.dataLabels,
19118 points = series.points,
19119 pointOptions,
19120 generalOptions,
19121 hasRendered = series.hasRendered || 0,
19122 str,
19123 dataLabelsGroup,
19124 defer = pick(options.defer, true),
19125 renderer = series.chart.renderer;
19126
19127 if (options.enabled || series._hasPointLabels) {
19128
19129 // Process default alignment of data labels for columns
19130 if (series.dlProcessOptions) {
19131 series.dlProcessOptions(options);
19132 }
19133
19134 // Create a separate group for the data labels to avoid rotation
19135 dataLabelsGroup = series.plotGroup(
19136 'dataLabelsGroup',
19137 'data-labels',
19138 defer && !hasRendered ? 'hidden' : 'visible', // #5133
19139 options.zIndex || 6
19140 );
19141
19142 if (defer) {
19143 dataLabelsGroup.attr({
19144 opacity: +hasRendered
19145 }); // #3300
19146 if (!hasRendered) {
19147 addEvent(series, 'afterAnimate', function() {
19148 if (series.visible) { // #2597, #3023, #3024
19149 dataLabelsGroup.show(true);
19150 }
19151 dataLabelsGroup[seriesOptions.animation ? 'animate' : 'attr']({
19152 opacity: 1
19153 }, {
19154 duration: 200
19155 });
19156 });
19157 }
19158 }
19159
19160 // Make the labels for each point
19161 generalOptions = options;
19162 each(points, function(point) {
19163
19164 var enabled,
19165 dataLabel = point.dataLabel,
19166 labelConfig,
19167 attr,
19168 name,
19169 rotation,
19170 connector = point.connector,
19171 isNew = true,
19172 style,
19173 moreStyle = {};
19174
19175 // Determine if each data label is enabled
19176 pointOptions = point.dlOptions || (point.options && point.options.dataLabels); // dlOptions is used in treemaps
19177 enabled = pick(pointOptions && pointOptions.enabled, generalOptions.enabled) && point.y !== null; // #2282, #4641
19178
19179
19180 // If the point is outside the plot area, destroy it. #678, #820
19181 if (dataLabel && !enabled) {
19182 point.dataLabel = dataLabel.destroy();
19183
19184 // Individual labels are disabled if the are explicitly disabled
19185 // in the point options, or if they fall outside the plot area.
19186 } else if (enabled) {
19187
19188 // Create individual options structure that can be extended without
19189 // affecting others
19190 options = merge(generalOptions, pointOptions);
19191 style = options.style;
19192
19193 rotation = options.rotation;
19194
19195 // Get the string
19196 labelConfig = point.getLabelConfig();
19197 str = options.format ?
19198 format(options.format, labelConfig) :
19199 options.formatter.call(labelConfig, options);
19200
19201
19202 // Determine the color
19203 style.color = pick(options.color, style.color, series.color, '#000000');
19204
19205
19206 // update existing label
19207 if (dataLabel) {
19208
19209 if (defined(str)) {
19210 dataLabel
19211 .attr({
19212 text: str
19213 });
19214 isNew = false;
19215
19216 } else { // #1437 - the label is shown conditionally
19217 point.dataLabel = dataLabel = dataLabel.destroy();
19218 if (connector) {
19219 point.connector = connector.destroy();
19220 }
19221 }
19222
19223 // create new label
19224 } else if (defined(str)) {
19225 attr = {
19226 //align: align,
19227
19228 fill: options.backgroundColor,
19229 stroke: options.borderColor,
19230 'stroke-width': options.borderWidth,
19231
19232 r: options.borderRadius || 0,
19233 rotation: rotation,
19234 padding: options.padding,
19235 zIndex: 1
19236 };
19237
19238
19239 // Get automated contrast color
19240 if (style.color === 'contrast') {
19241 moreStyle.color = options.inside || options.distance < 0 || !!seriesOptions.stacking ?
19242 renderer.getContrast(point.color || series.color) :
19243 '#000000';
19244 }
19245
19246 if (seriesOptions.cursor) {
19247 moreStyle.cursor = seriesOptions.cursor;
19248 }
19249
19250
19251
19252 // Remove unused attributes (#947)
19253 for (name in attr) {
19254 if (attr[name] === undefined) {
19255 delete attr[name];
19256 }
19257 }
19258
19259 dataLabel = point.dataLabel = renderer[rotation ? 'text' : 'label']( // labels don't support rotation
19260 str,
19261 0, -9999,
19262 options.shape,
19263 null,
19264 null,
19265 options.useHTML,
19266 null,
19267 'data-label'
19268 )
19269 .attr(attr);
19270
19271 dataLabel.addClass(
19272 'highcharts-data-label-color-' + point.colorIndex +
19273 ' ' + (options.className || '') +
19274 (options.useHTML ? 'highcharts-tracker' : '') // #3398
19275 );
19276
19277
19278 // Styles must be applied before add in order to read text bounding box
19279 dataLabel.css(extend(style, moreStyle));
19280
19281
19282 dataLabel.add(dataLabelsGroup);
19283
19284
19285 dataLabel.shadow(options.shadow);
19286
19287
19288
19289 }
19290
19291 if (dataLabel) {
19292 // Now the data label is created and placed at 0,0, so we need to align it
19293 series.alignDataLabel(point, dataLabel, options, null, isNew);
19294 }
19295 }
19296 });
19297 }
19298 };
19299
19300 /**
19301 * Align each individual data label
19302 */
19303 Series.prototype.alignDataLabel = function(point, dataLabel, options, alignTo, isNew) {
19304 var chart = this.chart,
19305 inverted = chart.inverted,
19306 plotX = pick(point.plotX, -9999),
19307 plotY = pick(point.plotY, -9999),
19308 bBox = dataLabel.getBBox(),
19309 fontSize,
19310 baseline,
19311 rotation = options.rotation,
19312 normRotation,
19313 negRotation,
19314 align = options.align,
19315 rotCorr, // rotation correction
19316 // Math.round for rounding errors (#2683), alignTo to allow column labels (#2700)
19317 visible = this.visible && (point.series.forceDL || chart.isInsidePlot(plotX, Math.round(plotY), inverted) ||
19318 (alignTo && chart.isInsidePlot(plotX, inverted ? alignTo.x + 1 : alignTo.y + alignTo.height - 1, inverted))),
19319 alignAttr, // the final position;
19320 justify = pick(options.overflow, 'justify') === 'justify';
19321
19322 if (visible) {
19323
19324
19325 fontSize = options.style.fontSize;
19326
19327
19328 baseline = chart.renderer.fontMetrics(fontSize, dataLabel).b;
19329
19330 // The alignment box is a singular point
19331 alignTo = extend({
19332 x: inverted ? chart.plotWidth - plotY : plotX,
19333 y: Math.round(inverted ? chart.plotHeight - plotX : plotY),
19334 width: 0,
19335 height: 0
19336 }, alignTo);
19337
19338 // Add the text size for alignment calculation
19339 extend(options, {
19340 width: bBox.width,
19341 height: bBox.height
19342 });
19343
19344 // Allow a hook for changing alignment in the last moment, then do the alignment
19345 if (rotation) {
19346 justify = false; // Not supported for rotated text
19347 rotCorr = chart.renderer.rotCorr(baseline, rotation); // #3723
19348 alignAttr = {
19349 x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x,
19350 y: alignTo.y + options.y + {
19351 top: 0,
19352 middle: 0.5,
19353 bottom: 1
19354 }[options.verticalAlign] * alignTo.height
19355 };
19356 dataLabel[isNew ? 'attr' : 'animate'](alignAttr)
19357 .attr({ // #3003
19358 align: align
19359 });
19360
19361 // Compensate for the rotated label sticking out on the sides
19362 normRotation = (rotation + 720) % 360;
19363 negRotation = normRotation > 180 && normRotation < 360;
19364
19365 if (align === 'left') {
19366 alignAttr.y -= negRotation ? bBox.height : 0;
19367 } else if (align === 'center') {
19368 alignAttr.x -= bBox.width / 2;
19369 alignAttr.y -= bBox.height / 2;
19370 } else if (align === 'right') {
19371 alignAttr.x -= bBox.width;
19372 alignAttr.y -= negRotation ? 0 : bBox.height;
19373 }
19374
19375
19376 } else {
19377 dataLabel.align(options, null, alignTo);
19378 alignAttr = dataLabel.alignAttr;
19379 }
19380
19381 // Handle justify or crop
19382 if (justify) {
19383 this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew);
19384
19385 // Now check that the data label is within the plot area
19386 } else if (pick(options.crop, true)) {
19387 visible = chart.isInsidePlot(alignAttr.x, alignAttr.y) && chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height);
19388 }
19389
19390 // When we're using a shape, make it possible with a connector or an arrow pointing to thie point
19391 if (options.shape && !rotation) {
19392 dataLabel.attr({
19393 anchorX: point.plotX,
19394 anchorY: point.plotY
19395 });
19396 }
19397 }
19398
19399 // Show or hide based on the final aligned position
19400 if (!visible) {
19401 dataLabel.attr({
19402 y: -9999
19403 });
19404 dataLabel.placed = false; // don't animate back in
19405 }
19406
19407 };
19408
19409 /**
19410 * If data labels fall partly outside the plot area, align them back in, in a way that
19411 * doesn't hide the point.
19412 */
19413 Series.prototype.justifyDataLabel = function(dataLabel, options, alignAttr, bBox, alignTo, isNew) {
19414 var chart = this.chart,
19415 align = options.align,
19416 verticalAlign = options.verticalAlign,
19417 off,
19418 justified,
19419 padding = dataLabel.box ? 0 : (dataLabel.padding || 0);
19420
19421 // Off left
19422 off = alignAttr.x + padding;
19423 if (off < 0) {
19424 if (align === 'right') {
19425 options.align = 'left';
19426 } else {
19427 options.x = -off;
19428 }
19429 justified = true;
19430 }
19431
19432 // Off right
19433 off = alignAttr.x + bBox.width - padding;
19434 if (off > chart.plotWidth) {
19435 if (align === 'left') {
19436 options.align = 'right';
19437 } else {
19438 options.x = chart.plotWidth - off;
19439 }
19440 justified = true;
19441 }
19442
19443 // Off top
19444 off = alignAttr.y + padding;
19445 if (off < 0) {
19446 if (verticalAlign === 'bottom') {
19447 options.verticalAlign = 'top';
19448 } else {
19449 options.y = -off;
19450 }
19451 justified = true;
19452 }
19453
19454 // Off bottom
19455 off = alignAttr.y + bBox.height - padding;
19456 if (off > chart.plotHeight) {
19457 if (verticalAlign === 'top') {
19458 options.verticalAlign = 'bottom';
19459 } else {
19460 options.y = chart.plotHeight - off;
19461 }
19462 justified = true;
19463 }
19464
19465 if (justified) {
19466 dataLabel.placed = !isNew;
19467 dataLabel.align(options, null, alignTo);
19468 }
19469 };
19470
19471 /**
19472 * Override the base drawDataLabels method by pie specific functionality
19473 */
19474 if (seriesTypes.pie) {
19475 seriesTypes.pie.prototype.drawDataLabels = function() {
19476 var series = this,
19477 data = series.data,
19478 point,
19479 chart = series.chart,
19480 options = series.options.dataLabels,
19481 connectorPadding = pick(options.connectorPadding, 10),
19482 connectorWidth = pick(options.connectorWidth, 1),
19483 plotWidth = chart.plotWidth,
19484 plotHeight = chart.plotHeight,
19485 connector,
19486 distanceOption = options.distance,
19487 seriesCenter = series.center,
19488 radius = seriesCenter[2] / 2,
19489 centerY = seriesCenter[1],
19490 outside = distanceOption > 0,
19491 dataLabel,
19492 dataLabelWidth,
19493 labelPos,
19494 labelHeight,
19495 halves = [ // divide the points into right and left halves for anti collision
19496 [], // right
19497 [] // left
19498 ],
19499 x,
19500 y,
19501 visibility,
19502 j,
19503 overflow = [0, 0, 0, 0]; // top, right, bottom, left
19504
19505 // get out if not enabled
19506 if (!series.visible || (!options.enabled && !series._hasPointLabels)) {
19507 return;
19508 }
19509
19510 // run parent method
19511 Series.prototype.drawDataLabels.apply(series);
19512
19513 each(data, function(point) {
19514 if (point.dataLabel && point.visible) { // #407, #2510
19515
19516 // Arrange points for detection collision
19517 halves[point.half].push(point);
19518
19519 // Reset positions (#4905)
19520 point.dataLabel._pos = null;
19521 }
19522 });
19523
19524 /* Loop over the points in each half, starting from the top and bottom
19525 * of the pie to detect overlapping labels.
19526 */
19527 each(halves, function(points, i) {
19528
19529 var top,
19530 bottom,
19531 length = points.length,
19532 positions,
19533 naturalY,
19534 size;
19535
19536 if (!length) {
19537 return;
19538 }
19539
19540 // Sort by angle
19541 series.sortByAngle(points, i - 0.5);
19542
19543 // Only do anti-collision when we are outside the pie and have connectors (#856)
19544 if (distanceOption > 0) {
19545 top = Math.max(0, centerY - radius - distanceOption);
19546 bottom = Math.min(centerY + radius + distanceOption, chart.plotHeight);
19547 positions = map(points, function(point) {
19548 if (point.dataLabel) {
19549 size = point.dataLabel.getBBox().height || 21;
19550 return {
19551 target: point.labelPos[1] - top + size / 2,
19552 size: size,
19553 rank: point.y
19554 };
19555 }
19556 });
19557 H.distribute(positions, bottom + size - top);
19558 }
19559
19560 // now the used slots are sorted, fill them up sequentially
19561 for (j = 0; j < length; j++) {
19562
19563 point = points[j];
19564 labelPos = point.labelPos;
19565 dataLabel = point.dataLabel;
19566 visibility = point.visible === false ? 'hidden' : 'inherit';
19567 naturalY = labelPos[1];
19568
19569 if (positions) {
19570 if (positions[j].pos === undefined) {
19571 visibility = 'hidden';
19572 } else {
19573 labelHeight = positions[j].size;
19574 y = top + positions[j].pos;
19575 }
19576
19577 } else {
19578 y = naturalY;
19579 }
19580
19581 // get the x - use the natural x position for labels near the top and bottom, to prevent the top
19582 // and botton slice connectors from touching each other on either side
19583 if (options.justify) {
19584 x = seriesCenter[0] + (i ? -1 : 1) * (radius + distanceOption);
19585 } else {
19586 x = series.getX(y < top + 2 || y > bottom - 2 ? naturalY : y, i);
19587 }
19588
19589
19590 // Record the placement and visibility
19591 dataLabel._attr = {
19592 visibility: visibility,
19593 align: labelPos[6]
19594 };
19595 dataLabel._pos = {
19596 x: x + options.x +
19597 ({
19598 left: connectorPadding,
19599 right: -connectorPadding
19600 }[labelPos[6]] || 0),
19601 y: y + options.y - 10 // 10 is for the baseline (label vs text)
19602 };
19603 labelPos.x = x;
19604 labelPos.y = y;
19605
19606
19607 // Detect overflowing data labels
19608 if (series.options.size === null) {
19609 dataLabelWidth = dataLabel.width;
19610 // Overflow left
19611 if (x - dataLabelWidth < connectorPadding) {
19612 overflow[3] = Math.max(Math.round(dataLabelWidth - x + connectorPadding), overflow[3]);
19613
19614 // Overflow right
19615 } else if (x + dataLabelWidth > plotWidth - connectorPadding) {
19616 overflow[1] = Math.max(Math.round(x + dataLabelWidth - plotWidth + connectorPadding), overflow[1]);
19617 }
19618
19619 // Overflow top
19620 if (y - labelHeight / 2 < 0) {
19621 overflow[0] = Math.max(Math.round(-y + labelHeight / 2), overflow[0]);
19622
19623 // Overflow left
19624 } else if (y + labelHeight / 2 > plotHeight) {
19625 overflow[2] = Math.max(Math.round(y + labelHeight / 2 - plotHeight), overflow[2]);
19626 }
19627 }
19628 } // for each point
19629 }); // for each half
19630
19631 // Do not apply the final placement and draw the connectors until we have verified
19632 // that labels are not spilling over.
19633 if (arrayMax(overflow) === 0 || this.verifyDataLabelOverflow(overflow)) {
19634
19635 // Place the labels in the final position
19636 this.placeDataLabels();
19637
19638 // Draw the connectors
19639 if (outside && connectorWidth) {
19640 each(this.points, function(point) {
19641 var isNew;
19642
19643 connector = point.connector;
19644 dataLabel = point.dataLabel;
19645
19646 if (dataLabel && dataLabel._pos && point.visible) {
19647 visibility = dataLabel._attr.visibility;
19648
19649 isNew = !connector;
19650
19651 if (isNew) {
19652 point.connector = connector = chart.renderer.path()
19653 .addClass('highcharts-data-label-connector highcharts-color-' + point.colorIndex)
19654 .add(series.dataLabelsGroup);
19655
19656
19657 connector.attr({
19658 'stroke-width': connectorWidth,
19659 'stroke': options.connectorColor || point.color || '#666666'
19660 });
19661
19662 }
19663 connector[isNew ? 'attr' : 'animate']({
19664 d: series.connectorPath(point.labelPos)
19665 });
19666 connector.attr('visibility', visibility);
19667
19668 } else if (connector) {
19669 point.connector = connector.destroy();
19670 }
19671 });
19672 }
19673 }
19674 };
19675
19676 /**
19677 * Extendable method for getting the path of the connector between the data label
19678 * and the pie slice.
19679 */
19680 seriesTypes.pie.prototype.connectorPath = function(labelPos) {
19681 var x = labelPos.x,
19682 y = labelPos.y;
19683 return pick(this.options.dataLabels.softConnector, true) ? [
19684 'M',
19685 x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
19686 'C',
19687 x, y, // first break, next to the label
19688 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5],
19689 labelPos[2], labelPos[3], // second break
19690 'L',
19691 labelPos[4], labelPos[5] // base
19692 ] : [
19693 'M',
19694 x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
19695 'L',
19696 labelPos[2], labelPos[3], // second break
19697 'L',
19698 labelPos[4], labelPos[5] // base
19699 ];
19700 };
19701
19702 /**
19703 * Perform the final placement of the data labels after we have verified that they
19704 * fall within the plot area.
19705 */
19706 seriesTypes.pie.prototype.placeDataLabels = function() {
19707 each(this.points, function(point) {
19708 var dataLabel = point.dataLabel,
19709 _pos;
19710
19711 if (dataLabel && point.visible) {
19712 _pos = dataLabel._pos;
19713 if (_pos) {
19714 dataLabel.attr(dataLabel._attr);
19715 dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos);
19716 dataLabel.moved = true;
19717 } else if (dataLabel) {
19718 dataLabel.attr({
19719 y: -9999
19720 });
19721 }
19722 }
19723 });
19724 };
19725
19726 seriesTypes.pie.prototype.alignDataLabel = noop;
19727
19728 /**
19729 * Verify whether the data labels are allowed to draw, or we should run more translation and data
19730 * label positioning to keep them inside the plot area. Returns true when data labels are ready
19731 * to draw.
19732 */
19733 seriesTypes.pie.prototype.verifyDataLabelOverflow = function(overflow) {
19734
19735 var center = this.center,
19736 options = this.options,
19737 centerOption = options.center,
19738 minSize = options.minSize || 80,
19739 newSize = minSize,
19740 ret;
19741
19742 // Handle horizontal size and center
19743 if (centerOption[0] !== null) { // Fixed center
19744 newSize = Math.max(center[2] - Math.max(overflow[1], overflow[3]), minSize);
19745
19746 } else { // Auto center
19747 newSize = Math.max(
19748 center[2] - overflow[1] - overflow[3], // horizontal overflow
19749 minSize
19750 );
19751 center[0] += (overflow[3] - overflow[1]) / 2; // horizontal center
19752 }
19753
19754 // Handle vertical size and center
19755 if (centerOption[1] !== null) { // Fixed center
19756 newSize = Math.max(Math.min(newSize, center[2] - Math.max(overflow[0], overflow[2])), minSize);
19757
19758 } else { // Auto center
19759 newSize = Math.max(
19760 Math.min(
19761 newSize,
19762 center[2] - overflow[0] - overflow[2] // vertical overflow
19763 ),
19764 minSize
19765 );
19766 center[1] += (overflow[0] - overflow[2]) / 2; // vertical center
19767 }
19768
19769 // If the size must be decreased, we need to run translate and drawDataLabels again
19770 if (newSize < center[2]) {
19771 center[2] = newSize;
19772 center[3] = Math.min(relativeLength(options.innerSize || 0, newSize), newSize); // #3632
19773 this.translate(center);
19774
19775 if (this.drawDataLabels) {
19776 this.drawDataLabels();
19777 }
19778 // Else, return true to indicate that the pie and its labels is within the plot area
19779 } else {
19780 ret = true;
19781 }
19782 return ret;
19783 };
19784 }
19785
19786 if (seriesTypes.column) {
19787
19788 /**
19789 * Override the basic data label alignment by adjusting for the position of the column
19790 */
19791 seriesTypes.column.prototype.alignDataLabel = function(point, dataLabel, options, alignTo, isNew) {
19792 var inverted = this.chart.inverted,
19793 series = point.series,
19794 dlBox = point.dlBox || point.shapeArgs, // data label box for alignment
19795 below = pick(point.below, point.plotY > pick(this.translatedThreshold, series.yAxis.len)), // point.below is used in range series
19796 inside = pick(options.inside, !!this.options.stacking), // draw it inside the box?
19797 overshoot;
19798
19799 // Align to the column itself, or the top of it
19800 if (dlBox) { // Area range uses this method but not alignTo
19801 alignTo = merge(dlBox);
19802
19803 if (alignTo.y < 0) {
19804 alignTo.height += alignTo.y;
19805 alignTo.y = 0;
19806 }
19807 overshoot = alignTo.y + alignTo.height - series.yAxis.len;
19808 if (overshoot > 0) {
19809 alignTo.height -= overshoot;
19810 }
19811
19812 if (inverted) {
19813 alignTo = {
19814 x: series.yAxis.len - alignTo.y - alignTo.height,
19815 y: series.xAxis.len - alignTo.x - alignTo.width,
19816 width: alignTo.height,
19817 height: alignTo.width
19818 };
19819 }
19820
19821 // Compute the alignment box
19822 if (!inside) {
19823 if (inverted) {
19824 alignTo.x += below ? 0 : alignTo.width;
19825 alignTo.width = 0;
19826 } else {
19827 alignTo.y += below ? alignTo.height : 0;
19828 alignTo.height = 0;
19829 }
19830 }
19831 }
19832
19833
19834 // When alignment is undefined (typically columns and bars), display the individual
19835 // point below or above the point depending on the threshold
19836 options.align = pick(
19837 options.align, !inverted || inside ? 'center' : below ? 'right' : 'left'
19838 );
19839 options.verticalAlign = pick(
19840 options.verticalAlign,
19841 inverted || inside ? 'middle' : below ? 'top' : 'bottom'
19842 );
19843
19844 // Call the parent method
19845 Series.prototype.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew);
19846 };
19847 }
19848
19849 }(Highcharts));
19850 (function(H) {
19851 /**
19852 * (c) 2009-2016 Torstein Honsi
19853 *
19854 * License: www.highcharts.com/license
19855 */
19856 'use strict';
19857 /**
19858 * Highcharts module to hide overlapping data labels. This module is included in Highcharts.
19859 */
19860 var Chart = H.Chart,
19861 each = H.each,
19862 pick = H.pick,
19863 addEvent = H.addEvent;
19864
19865 // Collect potensial overlapping data labels. Stack labels probably don't need to be
19866 // considered because they are usually accompanied by data labels that lie inside the columns.
19867 Chart.prototype.callbacks.push(function(chart) {
19868 function collectAndHide() {
19869 var labels = [];
19870
19871 each(chart.series, function(series) {
19872 var dlOptions = series.options.dataLabels,
19873 collections = series.dataLabelCollections || ['dataLabel']; // Range series have two collections
19874 if ((dlOptions.enabled || series._hasPointLabels) && !dlOptions.allowOverlap && series.visible) { // #3866
19875 each(collections, function(coll) {
19876 each(series.points, function(point) {
19877 if (point[coll]) {
19878 point[coll].labelrank = pick(point.labelrank, point.shapeArgs && point.shapeArgs.height); // #4118
19879 labels.push(point[coll]);
19880 }
19881 });
19882 });
19883 }
19884 });
19885 chart.hideOverlappingLabels(labels);
19886 }
19887
19888 // Do it now ...
19889 collectAndHide();
19890
19891 // ... and after each chart redraw
19892 addEvent(chart, 'redraw', collectAndHide);
19893
19894 });
19895
19896 /**
19897 * Hide overlapping labels. Labels are moved and faded in and out on zoom to provide a smooth
19898 * visual imression.
19899 */
19900 Chart.prototype.hideOverlappingLabels = function(labels) {
19901
19902 var len = labels.length,
19903 label,
19904 i,
19905 j,
19906 label1,
19907 label2,
19908 isIntersecting,
19909 pos1,
19910 pos2,
19911 parent1,
19912 parent2,
19913 padding,
19914 intersectRect = function(x1, y1, w1, h1, x2, y2, w2, h2) {
19915 return !(
19916 x2 > x1 + w1 ||
19917 x2 + w2 < x1 ||
19918 y2 > y1 + h1 ||
19919 y2 + h2 < y1
19920 );
19921 };
19922
19923 // Mark with initial opacity
19924 for (i = 0; i < len; i++) {
19925 label = labels[i];
19926 if (label) {
19927 label.oldOpacity = label.opacity;
19928 label.newOpacity = 1;
19929 }
19930 }
19931
19932 // Prevent a situation in a gradually rising slope, that each label
19933 // will hide the previous one because the previous one always has
19934 // lower rank.
19935 labels.sort(function(a, b) {
19936 return (b.labelrank || 0) - (a.labelrank || 0);
19937 });
19938
19939 // Detect overlapping labels
19940 for (i = 0; i < len; i++) {
19941 label1 = labels[i];
19942
19943 for (j = i + 1; j < len; ++j) {
19944 label2 = labels[j];
19945 if (label1 && label2 && label1.placed && label2.placed && label1.newOpacity !== 0 && label2.newOpacity !== 0) {
19946 pos1 = label1.alignAttr;
19947 pos2 = label2.alignAttr;
19948 parent1 = label1.parentGroup; // Different panes have different positions
19949 parent2 = label2.parentGroup;
19950 padding = 2 * (label1.box ? 0 : label1.padding); // Substract the padding if no background or border (#4333)
19951 isIntersecting = intersectRect(
19952 pos1.x + parent1.translateX,
19953 pos1.y + parent1.translateY,
19954 label1.width - padding,
19955 label1.height - padding,
19956 pos2.x + parent2.translateX,
19957 pos2.y + parent2.translateY,
19958 label2.width - padding,
19959 label2.height - padding
19960 );
19961
19962 if (isIntersecting) {
19963 (label1.labelrank < label2.labelrank ? label1 : label2).newOpacity = 0;
19964 }
19965 }
19966 }
19967 }
19968
19969 // Hide or show
19970 each(labels, function(label) {
19971 var complete,
19972 newOpacity;
19973
19974 if (label) {
19975 newOpacity = label.newOpacity;
19976
19977 if (label.oldOpacity !== newOpacity && label.placed) {
19978
19979 // Make sure the label is completely hidden to avoid catching clicks (#4362)
19980 if (newOpacity) {
19981 label.show(true);
19982 } else {
19983 complete = function() {
19984 label.hide();
19985 };
19986 }
19987
19988 // Animate or set the opacity
19989 label.alignAttr.opacity = newOpacity;
19990 label[label.isOld ? 'animate' : 'attr'](label.alignAttr, null, complete);
19991
19992 }
19993 label.isOld = true;
19994 }
19995 });
19996 };
19997
19998 }(Highcharts));
19999 (function(H) {
20000 /**
20001 * (c) 2010-2016 Torstein Honsi
20002 *
20003 * License: www.highcharts.com/license
20004 */
20005 'use strict';
20006 var Axis = H.Axis,
20007 each = H.each,
20008 pick = H.pick,
20009 wrap = H.wrap;
20010 /**
20011 * Override to use the extreme coordinates from the SVG shape, not the
20012 * data values
20013 */
20014 wrap(Axis.prototype, 'getSeriesExtremes', function(proceed) {
20015 var isXAxis = this.isXAxis,
20016 dataMin,
20017 dataMax,
20018 xData = [],
20019 useMapGeometry;
20020
20021 // Remove the xData array and cache it locally so that the proceed method doesn't use it
20022 if (isXAxis) {
20023 each(this.series, function(series, i) {
20024 if (series.useMapGeometry) {
20025 xData[i] = series.xData;
20026 series.xData = [];
20027 }
20028 });
20029 }
20030
20031 // Call base to reach normal cartesian series (like mappoint)
20032 proceed.call(this);
20033
20034 // Run extremes logic for map and mapline
20035 if (isXAxis) {
20036 dataMin = pick(this.dataMin, Number.MAX_VALUE);
20037 dataMax = pick(this.dataMax, -Number.MAX_VALUE);
20038 each(this.series, function(series, i) {
20039 if (series.useMapGeometry) {
20040 dataMin = Math.min(dataMin, pick(series.minX, dataMin));
20041 dataMax = Math.max(dataMax, pick(series.maxX, dataMin));
20042 series.xData = xData[i]; // Reset xData array
20043 useMapGeometry = true;
20044 }
20045 });
20046 if (useMapGeometry) {
20047 this.dataMin = dataMin;
20048 this.dataMax = dataMax;
20049 }
20050 }
20051 });
20052
20053 /**
20054 * Override axis translation to make sure the aspect ratio is always kept
20055 */
20056 wrap(Axis.prototype, 'setAxisTranslation', function(proceed) {
20057 var chart = this.chart,
20058 mapRatio,
20059 plotRatio = chart.plotWidth / chart.plotHeight,
20060 adjustedAxisLength,
20061 xAxis = chart.xAxis[0],
20062 padAxis,
20063 fixTo,
20064 fixDiff,
20065 preserveAspectRatio;
20066
20067
20068 // Run the parent method
20069 proceed.call(this);
20070
20071 // Check for map-like series
20072 if (this.coll === 'yAxis' && xAxis.transA !== undefined) {
20073 each(this.series, function(series) {
20074 if (series.preserveAspectRatio) {
20075 preserveAspectRatio = true;
20076 }
20077 });
20078 }
20079
20080 // On Y axis, handle both
20081 if (preserveAspectRatio) {
20082
20083 // Use the same translation for both axes
20084 this.transA = xAxis.transA = Math.min(this.transA, xAxis.transA);
20085
20086 mapRatio = plotRatio / ((xAxis.max - xAxis.min) / (this.max - this.min));
20087
20088 // What axis to pad to put the map in the middle
20089 padAxis = mapRatio < 1 ? this : xAxis;
20090
20091 // Pad it
20092 adjustedAxisLength = (padAxis.max - padAxis.min) * padAxis.transA;
20093 padAxis.pixelPadding = padAxis.len - adjustedAxisLength;
20094 padAxis.minPixelPadding = padAxis.pixelPadding / 2;
20095
20096 fixTo = padAxis.fixTo;
20097 if (fixTo) {
20098 fixDiff = fixTo[1] - padAxis.toValue(fixTo[0], true);
20099 fixDiff *= padAxis.transA;
20100 if (Math.abs(fixDiff) > padAxis.minPixelPadding || (padAxis.min === padAxis.dataMin && padAxis.max === padAxis.dataMax)) { // zooming out again, keep within restricted area
20101 fixDiff = 0;
20102 }
20103 padAxis.minPixelPadding -= fixDiff;
20104 }
20105 }
20106 });
20107
20108 /**
20109 * Override Axis.render in order to delete the fixTo prop
20110 */
20111 wrap(Axis.prototype, 'render', function(proceed) {
20112 proceed.call(this);
20113 this.fixTo = null;
20114 });
20115
20116 }(Highcharts));
20117 (function(H) {
20118 /**
20119 * (c) 2010-2016 Torstein Honsi
20120 *
20121 * License: www.highcharts.com/license
20122 */
20123 'use strict';
20124 var Axis = H.Axis,
20125 Chart = H.Chart,
20126 color = H.color,
20127 ColorAxis,
20128 each = H.each,
20129 extend = H.extend,
20130 isNumber = H.isNumber,
20131 Legend = H.Legend,
20132 LegendSymbolMixin = H.LegendSymbolMixin,
20133 noop = H.noop,
20134 merge = H.merge,
20135 pick = H.pick,
20136 wrap = H.wrap;
20137
20138 /**
20139 * The ColorAxis object for inclusion in gradient legends
20140 */
20141 ColorAxis = H.ColorAxis = function() {
20142 this.init.apply(this, arguments);
20143 };
20144 extend(ColorAxis.prototype, Axis.prototype);
20145 extend(ColorAxis.prototype, {
20146 defaultColorAxisOptions: {
20147 lineWidth: 0,
20148 minPadding: 0,
20149 maxPadding: 0,
20150 gridLineWidth: 1,
20151 tickPixelInterval: 72,
20152 startOnTick: true,
20153 endOnTick: true,
20154 offset: 0,
20155 marker: {
20156 animation: {
20157 duration: 50
20158 },
20159 width: 0.01,
20160
20161 color: '#999999'
20162
20163 },
20164 labels: {
20165 overflow: 'justify'
20166 },
20167 minColor: '#e6ebf5',
20168 maxColor: '#003399',
20169 tickLength: 5,
20170 showInLegend: true
20171 },
20172
20173 // Properties to preserve after destroy, for Axis.update (#5881)
20174 keepProps: ['legendGroup', 'legendItem', 'legendSymbol']
20175 .concat(Axis.prototype.keepProps),
20176
20177 /**
20178 * Initialize the color axis
20179 */
20180 init: function(chart, userOptions) {
20181 var horiz = chart.options.legend.layout !== 'vertical',
20182 options;
20183
20184 this.coll = 'colorAxis';
20185
20186 // Build the options
20187 options = merge(this.defaultColorAxisOptions, {
20188 side: horiz ? 2 : 1,
20189 reversed: !horiz
20190 }, userOptions, {
20191 opposite: !horiz,
20192 showEmpty: false,
20193 title: null
20194 });
20195
20196 Axis.prototype.init.call(this, chart, options);
20197
20198 // Base init() pushes it to the xAxis array, now pop it again
20199 //chart[this.isXAxis ? 'xAxis' : 'yAxis'].pop();
20200
20201 // Prepare data classes
20202 if (userOptions.dataClasses) {
20203 this.initDataClasses(userOptions);
20204 }
20205 this.initStops(userOptions);
20206
20207 // Override original axis properties
20208 this.horiz = horiz;
20209 this.zoomEnabled = false;
20210
20211 // Add default values
20212 this.defaultLegendLength = 200;
20213 },
20214
20215 /*
20216 * Return an intermediate color between two colors, according to pos where 0
20217 * is the from color and 1 is the to color.
20218 * NOTE: Changes here should be copied
20219 * to the same function in drilldown.src.js and solid-gauge-src.js.
20220 */
20221 tweenColors: function(from, to, pos) {
20222 // Check for has alpha, because rgba colors perform worse due to lack of
20223 // support in WebKit.
20224 var hasAlpha,
20225 ret;
20226
20227 // Unsupported color, return to-color (#3920)
20228 if (!to.rgba.length || !from.rgba.length) {
20229 ret = to.input || 'none';
20230
20231 // Interpolate
20232 } else {
20233 from = from.rgba;
20234 to = to.rgba;
20235 hasAlpha = (to[3] !== 1 || from[3] !== 1);
20236 ret = (hasAlpha ? 'rgba(' : 'rgb(') +
20237 Math.round(to[0] + (from[0] - to[0]) * (1 - pos)) + ',' +
20238 Math.round(to[1] + (from[1] - to[1]) * (1 - pos)) + ',' +
20239 Math.round(to[2] + (from[2] - to[2]) * (1 - pos)) +
20240 (hasAlpha ? (',' + (to[3] + (from[3] - to[3]) * (1 - pos))) : '') + ')';
20241 }
20242 return ret;
20243 },
20244
20245 initDataClasses: function(userOptions) {
20246 var axis = this,
20247 chart = this.chart,
20248 dataClasses,
20249 colorCounter = 0,
20250 colorCount = chart.options.chart.colorCount,
20251 options = this.options,
20252 len = userOptions.dataClasses.length;
20253 this.dataClasses = dataClasses = [];
20254 this.legendItems = [];
20255
20256 each(userOptions.dataClasses, function(dataClass, i) {
20257 var colors;
20258
20259 dataClass = merge(dataClass);
20260 dataClasses.push(dataClass);
20261 if (!dataClass.color) {
20262 if (options.dataClassColor === 'category') {
20263
20264 colors = chart.options.colors;
20265 colorCount = colors.length;
20266 dataClass.color = colors[colorCounter];
20267
20268 dataClass.colorIndex = colorCounter;
20269
20270 // increase and loop back to zero
20271 colorCounter++;
20272 if (colorCounter === colorCount) {
20273 colorCounter = 0;
20274 }
20275 } else {
20276 dataClass.color = axis.tweenColors(
20277 color(options.minColor),
20278 color(options.maxColor),
20279 len < 2 ? 0.5 : i / (len - 1) // #3219
20280 );
20281 }
20282 }
20283 });
20284 },
20285
20286 initStops: function(userOptions) {
20287 this.stops = userOptions.stops || [
20288 [0, this.options.minColor],
20289 [1, this.options.maxColor]
20290 ];
20291 each(this.stops, function(stop) {
20292 stop.color = color(stop[1]);
20293 });
20294 },
20295
20296 /**
20297 * Extend the setOptions method to process extreme colors and color
20298 * stops.
20299 */
20300 setOptions: function(userOptions) {
20301 Axis.prototype.setOptions.call(this, userOptions);
20302
20303 this.options.crosshair = this.options.marker;
20304 },
20305
20306 setAxisSize: function() {
20307 var symbol = this.legendSymbol,
20308 chart = this.chart,
20309 legendOptions = chart.options.legend || {},
20310 x,
20311 y,
20312 width,
20313 height;
20314
20315 if (symbol) {
20316 this.left = x = symbol.attr('x');
20317 this.top = y = symbol.attr('y');
20318 this.width = width = symbol.attr('width');
20319 this.height = height = symbol.attr('height');
20320 this.right = chart.chartWidth - x - width;
20321 this.bottom = chart.chartHeight - y - height;
20322
20323 this.len = this.horiz ? width : height;
20324 this.pos = this.horiz ? x : y;
20325 } else {
20326 // Fake length for disabled legend to avoid tick issues and such (#5205)
20327 this.len = (this.horiz ? legendOptions.symbolWidth : legendOptions.symbolHeight) || this.defaultLegendLength;
20328 }
20329 },
20330
20331 /**
20332 * Translate from a value to a color
20333 */
20334 toColor: function(value, point) {
20335 var pos,
20336 stops = this.stops,
20337 from,
20338 to,
20339 color,
20340 dataClasses = this.dataClasses,
20341 dataClass,
20342 i;
20343
20344 if (dataClasses) {
20345 i = dataClasses.length;
20346 while (i--) {
20347 dataClass = dataClasses[i];
20348 from = dataClass.from;
20349 to = dataClass.to;
20350 if ((from === undefined || value >= from) && (to === undefined || value <= to)) {
20351 color = dataClass.color;
20352 if (point) {
20353 point.dataClass = i;
20354 point.colorIndex = dataClass.colorIndex;
20355 }
20356 break;
20357 }
20358 }
20359
20360 } else {
20361
20362 if (this.isLog) {
20363 value = this.val2lin(value);
20364 }
20365 pos = 1 - ((this.max - value) / ((this.max - this.min) || 1));
20366 i = stops.length;
20367 while (i--) {
20368 if (pos > stops[i][0]) {
20369 break;
20370 }
20371 }
20372 from = stops[i] || stops[i + 1];
20373 to = stops[i + 1] || from;
20374
20375 // The position within the gradient
20376 pos = 1 - (to[0] - pos) / ((to[0] - from[0]) || 1);
20377
20378 color = this.tweenColors(
20379 from.color,
20380 to.color,
20381 pos
20382 );
20383 }
20384 return color;
20385 },
20386
20387 /**
20388 * Override the getOffset method to add the whole axis groups inside the legend.
20389 */
20390 getOffset: function() {
20391 var group = this.legendGroup,
20392 sideOffset = this.chart.axisOffset[this.side];
20393
20394 if (group) {
20395
20396 // Hook for the getOffset method to add groups to this parent group
20397 this.axisParent = group;
20398
20399 // Call the base
20400 Axis.prototype.getOffset.call(this);
20401
20402 // First time only
20403 if (!this.added) {
20404
20405 this.added = true;
20406
20407 this.labelLeft = 0;
20408 this.labelRight = this.width;
20409 }
20410 // Reset it to avoid color axis reserving space
20411 this.chart.axisOffset[this.side] = sideOffset;
20412 }
20413 },
20414
20415 /**
20416 * Create the color gradient
20417 */
20418 setLegendColor: function() {
20419 var grad,
20420 horiz = this.horiz,
20421 options = this.options,
20422 reversed = this.reversed,
20423 one = reversed ? 1 : 0,
20424 zero = reversed ? 0 : 1;
20425
20426 grad = horiz ? [one, 0, zero, 0] : [0, zero, 0, one]; // #3190
20427 this.legendColor = {
20428 linearGradient: {
20429 x1: grad[0],
20430 y1: grad[1],
20431 x2: grad[2],
20432 y2: grad[3]
20433 },
20434 stops: options.stops || [
20435 [0, options.minColor],
20436 [1, options.maxColor]
20437 ]
20438 };
20439 },
20440
20441 /**
20442 * The color axis appears inside the legend and has its own legend symbol
20443 */
20444 drawLegendSymbol: function(legend, item) {
20445 var padding = legend.padding,
20446 legendOptions = legend.options,
20447 horiz = this.horiz,
20448 width = pick(legendOptions.symbolWidth, horiz ? this.defaultLegendLength : 12),
20449 height = pick(legendOptions.symbolHeight, horiz ? 12 : this.defaultLegendLength),
20450 labelPadding = pick(legendOptions.labelPadding, horiz ? 16 : 30),
20451 itemDistance = pick(legendOptions.itemDistance, 10);
20452
20453 this.setLegendColor();
20454
20455 // Create the gradient
20456 item.legendSymbol = this.chart.renderer.rect(
20457 0,
20458 legend.baseline - 11,
20459 width,
20460 height
20461 ).attr({
20462 zIndex: 1
20463 }).add(item.legendGroup);
20464
20465 // Set how much space this legend item takes up
20466 this.legendItemWidth = width + padding + (horiz ? itemDistance : labelPadding);
20467 this.legendItemHeight = height + padding + (horiz ? labelPadding : 0);
20468 },
20469 /**
20470 * Fool the legend
20471 */
20472 setState: noop,
20473 visible: true,
20474 setVisible: noop,
20475 getSeriesExtremes: function() {
20476 var series;
20477 if (this.series.length) {
20478 series = this.series[0];
20479 this.dataMin = series.valueMin;
20480 this.dataMax = series.valueMax;
20481 }
20482 },
20483 drawCrosshair: function(e, point) {
20484 var plotX = point && point.plotX,
20485 plotY = point && point.plotY,
20486 crossPos,
20487 axisPos = this.pos,
20488 axisLen = this.len;
20489
20490 if (point) {
20491 crossPos = this.toPixels(point[point.series.colorKey]);
20492 if (crossPos < axisPos) {
20493 crossPos = axisPos - 2;
20494 } else if (crossPos > axisPos + axisLen) {
20495 crossPos = axisPos + axisLen + 2;
20496 }
20497
20498 point.plotX = crossPos;
20499 point.plotY = this.len - crossPos;
20500 Axis.prototype.drawCrosshair.call(this, e, point);
20501 point.plotX = plotX;
20502 point.plotY = plotY;
20503
20504 if (this.cross) {
20505 this.cross
20506 .addClass('highcharts-coloraxis-marker')
20507 .add(this.legendGroup);
20508
20509
20510 this.cross.attr({
20511 fill: this.crosshair.color
20512 });
20513
20514
20515 }
20516 }
20517 },
20518 getPlotLinePath: function(a, b, c, d, pos) {
20519 return isNumber(pos) ? // crosshairs only // #3969 pos can be 0 !!
20520 (this.horiz ? ['M', pos - 4, this.top - 6, 'L', pos + 4, this.top - 6, pos, this.top, 'Z'] : ['M', this.left, pos, 'L', this.left - 6, pos + 6, this.left - 6, pos - 6, 'Z']) :
20521 Axis.prototype.getPlotLinePath.call(this, a, b, c, d);
20522 },
20523
20524 update: function(newOptions, redraw) {
20525 var chart = this.chart,
20526 legend = chart.legend;
20527
20528 each(this.series, function(series) {
20529 series.isDirtyData = true; // Needed for Axis.update when choropleth colors change
20530 });
20531
20532 // When updating data classes, destroy old items and make sure new ones are created (#3207)
20533 if (newOptions.dataClasses && legend.allItems) {
20534 each(legend.allItems, function(item) {
20535 if (item.isDataClass) {
20536 item.legendGroup.destroy();
20537 }
20538 });
20539 chart.isDirtyLegend = true;
20540 }
20541
20542 // Keep the options structure updated for export. Unlike xAxis and yAxis, the colorAxis is
20543 // not an array. (#3207)
20544 chart.options[this.coll] = merge(this.userOptions, newOptions);
20545
20546 Axis.prototype.update.call(this, newOptions, redraw);
20547 if (this.legendItem) {
20548 this.setLegendColor();
20549 legend.colorizeItem(this, true);
20550 }
20551 },
20552
20553 /**
20554 * Get the legend item symbols for data classes
20555 */
20556 getDataClassLegendSymbols: function() {
20557 var axis = this,
20558 chart = this.chart,
20559 legendItems = this.legendItems,
20560 legendOptions = chart.options.legend,
20561 valueDecimals = legendOptions.valueDecimals,
20562 valueSuffix = legendOptions.valueSuffix || '',
20563 name;
20564
20565 if (!legendItems.length) {
20566 each(this.dataClasses, function(dataClass, i) {
20567 var vis = true,
20568 from = dataClass.from,
20569 to = dataClass.to;
20570
20571 // Assemble the default name. This can be overridden by legend.options.labelFormatter
20572 name = '';
20573 if (from === undefined) {
20574 name = '< ';
20575 } else if (to === undefined) {
20576 name = '> ';
20577 }
20578 if (from !== undefined) {
20579 name += H.numberFormat(from, valueDecimals) + valueSuffix;
20580 }
20581 if (from !== undefined && to !== undefined) {
20582 name += ' - ';
20583 }
20584 if (to !== undefined) {
20585 name += H.numberFormat(to, valueDecimals) + valueSuffix;
20586 }
20587 // Add a mock object to the legend items
20588 legendItems.push(extend({
20589 chart: chart,
20590 name: name,
20591 options: {},
20592 drawLegendSymbol: LegendSymbolMixin.drawRectangle,
20593 visible: true,
20594 setState: noop,
20595 isDataClass: true,
20596 setVisible: function() {
20597 vis = this.visible = !vis;
20598 each(axis.series, function(series) {
20599 each(series.points, function(point) {
20600 if (point.dataClass === i) {
20601 point.setVisible(vis);
20602 }
20603 });
20604 });
20605
20606 chart.legend.colorizeItem(this, vis);
20607 }
20608 }, dataClass));
20609 });
20610 }
20611 return legendItems;
20612 },
20613 name: '' // Prevents 'undefined' in legend in IE8
20614 });
20615
20616 /**
20617 * Handle animation of the color attributes directly
20618 */
20619 each(['fill', 'stroke'], function(prop) {
20620 H.Fx.prototype[prop + 'Setter'] = function() {
20621 this.elem.attr(
20622 prop,
20623 ColorAxis.prototype.tweenColors(
20624 color(this.start),
20625 color(this.end),
20626 this.pos
20627 ),
20628 null,
20629 true
20630 );
20631 };
20632 });
20633
20634 /**
20635 * Extend the chart getAxes method to also get the color axis
20636 */
20637 wrap(Chart.prototype, 'getAxes', function(proceed) {
20638
20639 var options = this.options,
20640 colorAxisOptions = options.colorAxis;
20641
20642 proceed.call(this);
20643
20644 this.colorAxis = [];
20645 if (colorAxisOptions) {
20646 new ColorAxis(this, colorAxisOptions); // eslint-disable-line no-new
20647 }
20648 });
20649
20650
20651 /**
20652 * Wrap the legend getAllItems method to add the color axis. This also removes the
20653 * axis' own series to prevent them from showing up individually.
20654 */
20655 wrap(Legend.prototype, 'getAllItems', function(proceed) {
20656 var allItems = [],
20657 colorAxis = this.chart.colorAxis[0];
20658
20659 if (colorAxis && colorAxis.options) {
20660 if (colorAxis.options.showInLegend) {
20661 // Data classes
20662 if (colorAxis.options.dataClasses) {
20663 allItems = allItems.concat(colorAxis.getDataClassLegendSymbols());
20664 // Gradient legend
20665 } else {
20666 // Add this axis on top
20667 allItems.push(colorAxis);
20668 }
20669 }
20670
20671 // Don't add the color axis' series
20672 each(colorAxis.series, function(series) {
20673 series.options.showInLegend = false;
20674 });
20675 }
20676
20677 return allItems.concat(proceed.call(this));
20678 });
20679
20680 wrap(Legend.prototype, 'colorizeItem', function(proceed, item, visible) {
20681 proceed.call(this, item, visible);
20682 if (visible && item.legendColor) {
20683 item.legendSymbol.attr({
20684 fill: item.legendColor
20685 });
20686 }
20687 });
20688
20689 }(Highcharts));
20690 (function(H) {
20691 /**
20692 * (c) 2010-2016 Torstein Honsi
20693 *
20694 * License: www.highcharts.com/license
20695 */
20696 'use strict';
20697 var defined = H.defined,
20698 each = H.each,
20699 noop = H.noop,
20700 seriesTypes = H.seriesTypes;
20701
20702 /**
20703 * Mixin for maps and heatmaps
20704 */
20705 H.colorPointMixin = {
20706 /**
20707 * Color points have a value option that determines whether or not it is a null point
20708 */
20709 isValid: function() {
20710 return this.value !== null;
20711 },
20712
20713 /**
20714 * Set the visibility of a single point
20715 */
20716 setVisible: function(vis) {
20717 var point = this,
20718 method = vis ? 'show' : 'hide';
20719
20720 // Show and hide associated elements
20721 each(['graphic', 'dataLabel'], function(key) {
20722 if (point[key]) {
20723 point[key][method]();
20724 }
20725 });
20726 }
20727 };
20728
20729 H.colorSeriesMixin = {
20730 pointArrayMap: ['value'],
20731 axisTypes: ['xAxis', 'yAxis', 'colorAxis'],
20732 optionalAxis: 'colorAxis',
20733 trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
20734 getSymbol: noop,
20735 parallelArrays: ['x', 'y', 'value'],
20736 colorKey: 'value',
20737
20738
20739 pointAttribs: seriesTypes.column.prototype.pointAttribs,
20740
20741
20742 /**
20743 * In choropleth maps, the color is a result of the value, so this needs translation too
20744 */
20745 translateColors: function() {
20746 var series = this,
20747 nullColor = this.options.nullColor,
20748 colorAxis = this.colorAxis,
20749 colorKey = this.colorKey;
20750
20751 each(this.data, function(point) {
20752 var value = point[colorKey],
20753 color;
20754
20755 color = point.options.color ||
20756 (point.isNull ? nullColor : (colorAxis && value !== undefined) ? colorAxis.toColor(value, point) : point.color || series.color);
20757
20758 if (color) {
20759 point.color = color;
20760 }
20761 });
20762 },
20763
20764 /**
20765 * Get the color attibutes to apply on the graphic
20766 */
20767 colorAttribs: function(point) {
20768 var ret = {};
20769 if (defined(point.color)) {
20770 ret[this.colorProp || 'fill'] = point.color;
20771 }
20772 return ret;
20773 }
20774 };
20775
20776 }(Highcharts));
20777 (function(H) {
20778 /**
20779 * (c) 2010-2016 Torstein Honsi
20780 *
20781 * License: www.highcharts.com/license
20782 */
20783 'use strict';
20784 var color = H.color,
20785 ColorAxis = H.ColorAxis,
20786 colorPointMixin = H.colorPointMixin,
20787 colorSeriesMixin = H.colorSeriesMixin,
20788 doc = H.doc,
20789 each = H.each,
20790 extend = H.extend,
20791 isNumber = H.isNumber,
20792 LegendSymbolMixin = H.LegendSymbolMixin,
20793 map = H.map,
20794 merge = H.merge,
20795 noop = H.noop,
20796 pick = H.pick,
20797 isArray = H.isArray,
20798 Point = H.Point,
20799 Series = H.Series,
20800 seriesType = H.seriesType,
20801 seriesTypes = H.seriesTypes,
20802 splat = H.splat;
20803
20804 // The vector-effect attribute is not supported in IE <= 11 (at least), so we need
20805 // diffent logic (#3218)
20806 var supportsVectorEffect = doc.documentElement.style.vectorEffect !== undefined;
20807
20808
20809 /**
20810 * The MapAreaPoint object
20811 */
20812 /**
20813 * Add the map series type
20814 */
20815 seriesType('map', 'scatter', {
20816 allAreas: true,
20817
20818 animation: false, // makes the complex shapes slow
20819 nullColor: '#f7f7f7',
20820 borderColor: '#cccccc',
20821 borderWidth: 1,
20822 marker: null,
20823 stickyTracking: false,
20824 joinBy: 'hc-key',
20825 dataLabels: {
20826 formatter: function() { // #2945
20827 return this.point.value;
20828 },
20829 inside: true, // for the color
20830 verticalAlign: 'middle',
20831 crop: false,
20832 overflow: false,
20833 padding: 0
20834 },
20835 turboThreshold: 0,
20836 tooltip: {
20837 followPointer: true,
20838 pointFormat: '{point.name}: {point.value}<br/>'
20839 },
20840 states: {
20841 normal: {
20842 animation: true
20843 },
20844 hover: {
20845 brightness: 0.2,
20846 halo: null
20847 },
20848 select: {
20849 color: '#cccccc'
20850 }
20851 }
20852
20853 // Prototype members
20854 }, merge(colorSeriesMixin, {
20855 type: 'map',
20856 supportsDrilldown: true,
20857 getExtremesFromAll: true,
20858 useMapGeometry: true, // get axis extremes from paths, not values
20859 forceDL: true,
20860 searchPoint: noop,
20861 directTouch: true, // When tooltip is not shared, this series (and derivatives) requires direct touch/hover. KD-tree does not apply.
20862 preserveAspectRatio: true, // X axis and Y axis must have same translation slope
20863 pointArrayMap: ['value'],
20864 /**
20865 * Get the bounding box of all paths in the map combined.
20866 */
20867 getBox: function(paths) {
20868 var MAX_VALUE = Number.MAX_VALUE,
20869 maxX = -MAX_VALUE,
20870 minX = MAX_VALUE,
20871 maxY = -MAX_VALUE,
20872 minY = MAX_VALUE,
20873 minRange = MAX_VALUE,
20874 xAxis = this.xAxis,
20875 yAxis = this.yAxis,
20876 hasBox;
20877
20878 // Find the bounding box
20879 each(paths || [], function(point) {
20880
20881 if (point.path) {
20882 if (typeof point.path === 'string') {
20883 point.path = H.splitPath(point.path);
20884 }
20885
20886 var path = point.path || [],
20887 i = path.length,
20888 even = false, // while loop reads from the end
20889 pointMaxX = -MAX_VALUE,
20890 pointMinX = MAX_VALUE,
20891 pointMaxY = -MAX_VALUE,
20892 pointMinY = MAX_VALUE,
20893 properties = point.properties;
20894
20895 // The first time a map point is used, analyze its box
20896 if (!point._foundBox) {
20897 while (i--) {
20898 if (isNumber(path[i])) {
20899 if (even) { // even = x
20900 pointMaxX = Math.max(pointMaxX, path[i]);
20901 pointMinX = Math.min(pointMinX, path[i]);
20902 } else { // odd = Y
20903 pointMaxY = Math.max(pointMaxY, path[i]);
20904 pointMinY = Math.min(pointMinY, path[i]);
20905 }
20906 even = !even;
20907 }
20908 }
20909 // Cache point bounding box for use to position data labels, bubbles etc
20910 point._midX = pointMinX + (pointMaxX - pointMinX) *
20911 (point.middleX || (properties && properties['hc-middle-x']) || 0.5); // pick is slower and very marginally needed
20912 point._midY = pointMinY + (pointMaxY - pointMinY) *
20913 (point.middleY || (properties && properties['hc-middle-y']) || 0.5);
20914 point._maxX = pointMaxX;
20915 point._minX = pointMinX;
20916 point._maxY = pointMaxY;
20917 point._minY = pointMinY;
20918 point.labelrank = pick(point.labelrank, (pointMaxX - pointMinX) * (pointMaxY - pointMinY));
20919 point._foundBox = true;
20920 }
20921
20922 maxX = Math.max(maxX, point._maxX);
20923 minX = Math.min(minX, point._minX);
20924 maxY = Math.max(maxY, point._maxY);
20925 minY = Math.min(minY, point._minY);
20926 minRange = Math.min(point._maxX - point._minX, point._maxY - point._minY, minRange);
20927 hasBox = true;
20928 }
20929 });
20930
20931 // Set the box for the whole series
20932 if (hasBox) {
20933 this.minY = Math.min(minY, pick(this.minY, MAX_VALUE));
20934 this.maxY = Math.max(maxY, pick(this.maxY, -MAX_VALUE));
20935 this.minX = Math.min(minX, pick(this.minX, MAX_VALUE));
20936 this.maxX = Math.max(maxX, pick(this.maxX, -MAX_VALUE));
20937
20938 // If no minRange option is set, set the default minimum zooming range to 5 times the
20939 // size of the smallest element
20940 if (xAxis && xAxis.options.minRange === undefined) {
20941 xAxis.minRange = Math.min(5 * minRange, (this.maxX - this.minX) / 5, xAxis.minRange || MAX_VALUE);
20942 }
20943 if (yAxis && yAxis.options.minRange === undefined) {
20944 yAxis.minRange = Math.min(5 * minRange, (this.maxY - this.minY) / 5, yAxis.minRange || MAX_VALUE);
20945 }
20946 }
20947 },
20948
20949 getExtremes: function() {
20950 // Get the actual value extremes for colors
20951 Series.prototype.getExtremes.call(this, this.valueData);
20952
20953 // Recalculate box on updated data
20954 if (this.chart.hasRendered && this.isDirtyData) {
20955 this.getBox(this.options.data);
20956 }
20957
20958 this.valueMin = this.dataMin;
20959 this.valueMax = this.dataMax;
20960
20961 // Extremes for the mock Y axis
20962 this.dataMin = this.minY;
20963 this.dataMax = this.maxY;
20964 },
20965
20966 /**
20967 * Translate the path so that it automatically fits into the plot area box
20968 * @param {Object} path
20969 */
20970 translatePath: function(path) {
20971
20972 var series = this,
20973 even = false, // while loop reads from the end
20974 xAxis = series.xAxis,
20975 yAxis = series.yAxis,
20976 xMin = xAxis.min,
20977 xTransA = xAxis.transA,
20978 xMinPixelPadding = xAxis.minPixelPadding,
20979 yMin = yAxis.min,
20980 yTransA = yAxis.transA,
20981 yMinPixelPadding = yAxis.minPixelPadding,
20982 i,
20983 ret = []; // Preserve the original
20984
20985 // Do the translation
20986 if (path) {
20987 i = path.length;
20988 while (i--) {
20989 if (isNumber(path[i])) {
20990 ret[i] = even ?
20991 (path[i] - xMin) * xTransA + xMinPixelPadding :
20992 (path[i] - yMin) * yTransA + yMinPixelPadding;
20993 even = !even;
20994 } else {
20995 ret[i] = path[i];
20996 }
20997 }
20998 }
20999
21000 return ret;
21001 },
21002
21003 /**
21004 * Extend setData to join in mapData. If the allAreas option is true, all areas
21005 * from the mapData are used, and those that don't correspond to a data value
21006 * are given null values.
21007 */
21008 setData: function(data, redraw, animation, updatePoints) {
21009 var options = this.options,
21010 chartOptions = this.chart.options.chart,
21011 globalMapData = chartOptions && chartOptions.map,
21012 mapData = options.mapData,
21013 joinBy = options.joinBy,
21014 joinByNull = joinBy === null,
21015 pointArrayMap = options.keys || this.pointArrayMap,
21016 dataUsed = [],
21017 mapMap = {},
21018 mapPoint,
21019 transform,
21020 mapTransforms = this.chart.mapTransforms,
21021 props,
21022 i;
21023
21024 // Collect mapData from chart options if not defined on series
21025 if (!mapData && globalMapData) {
21026 mapData = typeof globalMapData === 'string' ? H.maps[globalMapData] : globalMapData;
21027 }
21028
21029 if (joinByNull) {
21030 joinBy = '_i';
21031 }
21032 joinBy = this.joinBy = splat(joinBy);
21033 if (!joinBy[1]) {
21034 joinBy[1] = joinBy[0];
21035 }
21036
21037 // Pick up numeric values, add index
21038 // Convert Array point definitions to objects using pointArrayMap
21039 if (data) {
21040 each(data, function(val, i) {
21041 var ix = 0;
21042 if (isNumber(val)) {
21043 data[i] = {
21044 value: val
21045 };
21046 } else if (isArray(val)) {
21047 data[i] = {};
21048 // Automatically copy first item to hc-key if there is an extra leading string
21049 if (!options.keys && val.length > pointArrayMap.length && typeof val[0] === 'string') {
21050 data[i]['hc-key'] = val[0];
21051 ++ix;
21052 }
21053 // Run through pointArrayMap and what's left of the point data array in parallel, copying over the values
21054 for (var j = 0; j < pointArrayMap.length; ++j, ++ix) {
21055 if (pointArrayMap[j]) {
21056 data[i][pointArrayMap[j]] = val[ix];
21057 }
21058 }
21059 }
21060 if (joinByNull) {
21061 data[i]._i = i;
21062 }
21063 });
21064 }
21065
21066 this.getBox(data);
21067
21068 // Pick up transform definitions for chart
21069 this.chart.mapTransforms = mapTransforms = chartOptions && chartOptions.mapTransforms || mapData && mapData['hc-transform'] || mapTransforms;
21070
21071 // Cache cos/sin of transform rotation angle
21072 if (mapTransforms) {
21073 for (transform in mapTransforms) {
21074 if (mapTransforms.hasOwnProperty(transform) && transform.rotation) {
21075 transform.cosAngle = Math.cos(transform.rotation);
21076 transform.sinAngle = Math.sin(transform.rotation);
21077 }
21078 }
21079 }
21080
21081 if (mapData) {
21082 if (mapData.type === 'FeatureCollection') {
21083 this.mapTitle = mapData.title;
21084 mapData = H.geojson(mapData, this.type, this);
21085 }
21086
21087 this.mapData = mapData;
21088 this.mapMap = {};
21089
21090 for (i = 0; i < mapData.length; i++) {
21091 mapPoint = mapData[i];
21092 props = mapPoint.properties;
21093
21094 mapPoint._i = i;
21095 // Copy the property over to root for faster access
21096 if (joinBy[0] && props && props[joinBy[0]]) {
21097 mapPoint[joinBy[0]] = props[joinBy[0]];
21098 }
21099 mapMap[mapPoint[joinBy[0]]] = mapPoint;
21100 }
21101 this.mapMap = mapMap;
21102
21103 // Registered the point codes that actually hold data
21104 if (data && joinBy[1]) {
21105 each(data, function(point) {
21106 if (mapMap[point[joinBy[1]]]) {
21107 dataUsed.push(mapMap[point[joinBy[1]]]);
21108 }
21109 });
21110 }
21111
21112 if (options.allAreas) {
21113 this.getBox(mapData);
21114 data = data || [];
21115
21116 // Registered the point codes that actually hold data
21117 if (joinBy[1]) {
21118 each(data, function(point) {
21119 dataUsed.push(point[joinBy[1]]);
21120 });
21121 }
21122
21123 // Add those map points that don't correspond to data, which will be drawn as null points
21124 dataUsed = '|' + map(dataUsed, function(point) {
21125 return point && point[joinBy[0]];
21126 }).join('|') + '|'; // String search is faster than array.indexOf
21127
21128 each(mapData, function(mapPoint) {
21129 if (!joinBy[0] || dataUsed.indexOf('|' + mapPoint[joinBy[0]] + '|') === -1) {
21130 data.push(merge(mapPoint, {
21131 value: null
21132 }));
21133 updatePoints = false; // #5050 - adding all areas causes the update optimization of setData to kick in, even though the point order has changed
21134 }
21135 });
21136 } else {
21137 this.getBox(dataUsed); // Issue #4784
21138 }
21139 }
21140 Series.prototype.setData.call(this, data, redraw, animation, updatePoints);
21141 },
21142
21143
21144 /**
21145 * No graph for the map series
21146 */
21147 drawGraph: noop,
21148
21149 /**
21150 * We need the points' bounding boxes in order to draw the data labels, so
21151 * we skip it now and call it from drawPoints instead.
21152 */
21153 drawDataLabels: noop,
21154
21155 /**
21156 * Allow a quick redraw by just translating the area group. Used for zooming and panning
21157 * in capable browsers.
21158 */
21159 doFullTranslate: function() {
21160 return this.isDirtyData || this.chart.isResizing || this.chart.renderer.isVML || !this.baseTrans;
21161 },
21162
21163 /**
21164 * Add the path option for data points. Find the max value for color calculation.
21165 */
21166 translate: function() {
21167 var series = this,
21168 xAxis = series.xAxis,
21169 yAxis = series.yAxis,
21170 doFullTranslate = series.doFullTranslate();
21171
21172 series.generatePoints();
21173
21174 each(series.data, function(point) {
21175
21176 // Record the middle point (loosely based on centroid), determined
21177 // by the middleX and middleY options.
21178 point.plotX = xAxis.toPixels(point._midX, true);
21179 point.plotY = yAxis.toPixels(point._midY, true);
21180
21181 if (doFullTranslate) {
21182
21183 point.shapeType = 'path';
21184 point.shapeArgs = {
21185 d: series.translatePath(point.path)
21186 };
21187 }
21188 });
21189
21190 series.translateColors();
21191 },
21192
21193 /**
21194 * Get presentational attributes
21195 */
21196 pointAttribs: function(point, state) {
21197 var attr = seriesTypes.column.prototype.pointAttribs.call(this, point, state);
21198
21199 // Prevent flickering whan called from setState
21200 if (point.isFading) {
21201 delete attr.fill;
21202 }
21203
21204 // If vector-effect is not supported, we set the stroke-width on the group element
21205 // and let all point graphics inherit. That way we don't have to iterate over all
21206 // points to update the stroke-width on zooming. TODO: Check unstyled
21207 if (supportsVectorEffect) {
21208 attr['vector-effect'] = 'non-scaling-stroke';
21209 } else {
21210 attr['stroke-width'] = 'inherit';
21211 }
21212
21213 return attr;
21214 },
21215
21216 /**
21217 * Use the drawPoints method of column, that is able to handle simple shapeArgs.
21218 * Extend it by assigning the tooltip position.
21219 */
21220 drawPoints: function() {
21221 var series = this,
21222 xAxis = series.xAxis,
21223 yAxis = series.yAxis,
21224 group = series.group,
21225 chart = series.chart,
21226 renderer = chart.renderer,
21227 scaleX,
21228 scaleY,
21229 translateX,
21230 translateY,
21231 baseTrans = this.baseTrans,
21232 transformGroup,
21233 startTranslateX,
21234 startTranslateY,
21235 startScaleX,
21236 startScaleY;
21237
21238 // Set a group that handles transform during zooming and panning in order to preserve clipping
21239 // on series.group
21240 if (!series.transformGroup) {
21241 series.transformGroup = renderer.g()
21242 .attr({
21243 scaleX: 1,
21244 scaleY: 1
21245 })
21246 .add(group);
21247 series.transformGroup.survive = true;
21248 }
21249
21250 // Draw the shapes again
21251 if (series.doFullTranslate()) {
21252
21253 // Individual point actions. TODO: Check unstyled.
21254
21255 if (chart.hasRendered) {
21256 each(series.points, function(point) {
21257
21258 // Restore state color on update/redraw (#3529)
21259 if (point.shapeArgs) {
21260 point.shapeArgs.fill = series.pointAttribs(point, point.state).fill;
21261 }
21262 });
21263 }
21264
21265
21266 // Draw them in transformGroup
21267 series.group = series.transformGroup;
21268 seriesTypes.column.prototype.drawPoints.apply(series);
21269 series.group = group; // Reset
21270
21271 // Add class names
21272 each(series.points, function(point) {
21273 if (point.graphic) {
21274 if (point.name) {
21275 point.graphic.addClass('highcharts-name-' + point.name.replace(/ /g, '-').toLowerCase());
21276 }
21277 if (point.properties && point.properties['hc-key']) {
21278 point.graphic.addClass('highcharts-key-' + point.properties['hc-key'].toLowerCase());
21279 }
21280 }
21281 });
21282
21283 // Set the base for later scale-zooming. The originX and originY properties are the
21284 // axis values in the plot area's upper left corner.
21285 this.baseTrans = {
21286 originX: xAxis.min - xAxis.minPixelPadding / xAxis.transA,
21287 originY: yAxis.min - yAxis.minPixelPadding / yAxis.transA + (yAxis.reversed ? 0 : yAxis.len / yAxis.transA),
21288 transAX: xAxis.transA,
21289 transAY: yAxis.transA
21290 };
21291
21292 // Reset transformation in case we're doing a full translate (#3789)
21293 this.transformGroup.animate({
21294 translateX: 0,
21295 translateY: 0,
21296 scaleX: 1,
21297 scaleY: 1
21298 });
21299
21300 // Just update the scale and transform for better performance
21301 } else {
21302 scaleX = xAxis.transA / baseTrans.transAX;
21303 scaleY = yAxis.transA / baseTrans.transAY;
21304 translateX = xAxis.toPixels(baseTrans.originX, true);
21305 translateY = yAxis.toPixels(baseTrans.originY, true);
21306
21307 // Handle rounding errors in normal view (#3789)
21308 if (scaleX > 0.99 && scaleX < 1.01 && scaleY > 0.99 && scaleY < 1.01) {
21309 scaleX = 1;
21310 scaleY = 1;
21311 translateX = Math.round(translateX);
21312 translateY = Math.round(translateY);
21313 }
21314
21315 // Animate or move to the new zoom level. In order to prevent
21316 // flickering as the different transform components are set out of
21317 // sync (#5991), we run a fake animator attribute and set scale and
21318 // translation synchronously in the same step.
21319 // A possible improvement to the API would be to handle this in the
21320 // renderer or animation engine itself, to ensure that when we are
21321 // animating multiple properties, we make sure that each step for
21322 // each property is performed in the same step. Also, for symbols
21323 // and for transform properties, it should induce a single
21324 // updateTransform and symbolAttr call.
21325 transformGroup = this.transformGroup;
21326 if (chart.renderer.globalAnimation) {
21327 startTranslateX = transformGroup.attr('translateX');
21328 startTranslateY = transformGroup.attr('translateY');
21329 startScaleX = transformGroup.attr('scaleX');
21330 startScaleY = transformGroup.attr('scaleY');
21331 transformGroup
21332 .attr({
21333 animator: 0
21334 })
21335 .animate({
21336 animator: 1
21337 }, {
21338 step: function(now, fx) {
21339 transformGroup.attr({
21340 translateX: startTranslateX +
21341 (translateX - startTranslateX) * fx.pos,
21342 translateY: startTranslateY +
21343 (translateY - startTranslateY) * fx.pos,
21344 scaleX: startScaleX +
21345 (scaleX - startScaleX) * fx.pos,
21346 scaleY: startScaleY +
21347 (scaleY - startScaleY) * fx.pos
21348 });
21349
21350 }
21351 });
21352
21353 // When dragging, animation is off.
21354 } else {
21355 transformGroup.attr({
21356 translateX: translateX,
21357 translateY: translateY,
21358 scaleX: scaleX,
21359 scaleY: scaleY
21360 });
21361 }
21362
21363 }
21364
21365 // Set the stroke-width directly on the group element so the children inherit it. We need to use
21366 // setAttribute directly, because the stroke-widthSetter method expects a stroke color also to be
21367 // set.
21368 if (!supportsVectorEffect) {
21369 series.group.element.setAttribute(
21370 'stroke-width',
21371 series.options[
21372 (series.pointAttrToOptions && series.pointAttrToOptions['stroke-width']) || 'borderWidth'
21373 ] / (scaleX || 1)
21374 );
21375 }
21376
21377 this.drawMapDataLabels();
21378
21379
21380 },
21381
21382 /**
21383 * Draw the data labels. Special for maps is the time that the data labels are drawn (after points),
21384 * and the clipping of the dataLabelsGroup.
21385 */
21386 drawMapDataLabels: function() {
21387
21388 Series.prototype.drawDataLabels.call(this);
21389 if (this.dataLabelsGroup) {
21390 this.dataLabelsGroup.clip(this.chart.clipRect);
21391 }
21392 },
21393
21394 /**
21395 * Override render to throw in an async call in IE8. Otherwise it chokes on the US counties demo.
21396 */
21397 render: function() {
21398 var series = this,
21399 render = Series.prototype.render;
21400
21401 // Give IE8 some time to breathe.
21402 if (series.chart.renderer.isVML && series.data.length > 3000) {
21403 setTimeout(function() {
21404 render.call(series);
21405 });
21406 } else {
21407 render.call(series);
21408 }
21409 },
21410
21411 /**
21412 * The initial animation for the map series. By default, animation is disabled.
21413 * Animation of map shapes is not at all supported in VML browsers.
21414 */
21415 animate: function(init) {
21416 var chart = this.chart,
21417 animation = this.options.animation,
21418 group = this.group,
21419 xAxis = this.xAxis,
21420 yAxis = this.yAxis,
21421 left = xAxis.pos,
21422 top = yAxis.pos;
21423
21424 if (chart.renderer.isSVG) {
21425
21426 if (animation === true) {
21427 animation = {
21428 duration: 1000
21429 };
21430 }
21431
21432 // Initialize the animation
21433 if (init) {
21434
21435 // Scale down the group and place it in the center
21436 group.attr({
21437 translateX: left + xAxis.len / 2,
21438 translateY: top + yAxis.len / 2,
21439 scaleX: 0.001, // #1499
21440 scaleY: 0.001
21441 });
21442
21443 // Run the animation
21444 } else {
21445 group.animate({
21446 translateX: left,
21447 translateY: top,
21448 scaleX: 1,
21449 scaleY: 1
21450 }, animation);
21451
21452 // Delete this function to allow it only once
21453 this.animate = null;
21454 }
21455 }
21456 },
21457
21458 /**
21459 * Animate in the new series from the clicked point in the old series.
21460 * Depends on the drilldown.js module
21461 */
21462 animateDrilldown: function(init) {
21463 var toBox = this.chart.plotBox,
21464 level = this.chart.drilldownLevels[this.chart.drilldownLevels.length - 1],
21465 fromBox = level.bBox,
21466 animationOptions = this.chart.options.drilldown.animation,
21467 scale;
21468
21469 if (!init) {
21470
21471 scale = Math.min(fromBox.width / toBox.width, fromBox.height / toBox.height);
21472 level.shapeArgs = {
21473 scaleX: scale,
21474 scaleY: scale,
21475 translateX: fromBox.x,
21476 translateY: fromBox.y
21477 };
21478
21479 each(this.points, function(point) {
21480 if (point.graphic) {
21481 point.graphic
21482 .attr(level.shapeArgs)
21483 .animate({
21484 scaleX: 1,
21485 scaleY: 1,
21486 translateX: 0,
21487 translateY: 0
21488 }, animationOptions);
21489 }
21490 });
21491
21492 this.animate = null;
21493 }
21494
21495 },
21496
21497 drawLegendSymbol: LegendSymbolMixin.drawRectangle,
21498
21499 /**
21500 * When drilling up, pull out the individual point graphics from the lower series
21501 * and animate them into the origin point in the upper series.
21502 */
21503 animateDrillupFrom: function(level) {
21504 seriesTypes.column.prototype.animateDrillupFrom.call(this, level);
21505 },
21506
21507
21508 /**
21509 * When drilling up, keep the upper series invisible until the lower series has
21510 * moved into place
21511 */
21512 animateDrillupTo: function(init) {
21513 seriesTypes.column.prototype.animateDrillupTo.call(this, init);
21514 }
21515
21516 // Point class
21517 }), extend({
21518 /**
21519 * Extend the Point object to split paths
21520 */
21521 applyOptions: function(options, x) {
21522
21523 var point = Point.prototype.applyOptions.call(this, options, x),
21524 series = this.series,
21525 joinBy = series.joinBy,
21526 mapPoint;
21527
21528 if (series.mapData) {
21529 mapPoint = point[joinBy[1]] !== undefined && series.mapMap[point[joinBy[1]]];
21530 if (mapPoint) {
21531 // This applies only to bubbles
21532 if (series.xyFromShape) {
21533 point.x = mapPoint._midX;
21534 point.y = mapPoint._midY;
21535 }
21536 extend(point, mapPoint); // copy over properties
21537 } else {
21538 point.value = point.value || null;
21539 }
21540 }
21541
21542 return point;
21543 },
21544
21545 /**
21546 * Stop the fade-out
21547 */
21548 onMouseOver: function(e) {
21549 clearTimeout(this.colorInterval);
21550 if (this.value !== null) {
21551 Point.prototype.onMouseOver.call(this, e);
21552 } else { //#3401 Tooltip doesn't hide when hovering over null points
21553 this.series.onMouseOut(e);
21554 }
21555 },
21556
21557 // Todo: check unstyled
21558 /**
21559 * Custom animation for tweening out the colors. Animation reduces blinking when hovering
21560 * over islands and coast lines. We run a custom implementation of animation becuase we
21561 * need to be able to run this independently from other animations like zoom redraw. Also,
21562 * adding color animation to the adapters would introduce almost the same amount of code.
21563 */
21564 onMouseOut: function() {
21565 var point = this,
21566 start = +new Date(),
21567 normalColor = color(this.series.pointAttribs(point).fill),
21568 hoverColor = color(this.series.pointAttribs(point, 'hover').fill),
21569 animation = point.series.options.states.normal.animation,
21570 duration = animation && (animation.duration || 500);
21571
21572 if (duration && normalColor.rgba.length === 4 && hoverColor.rgba.length === 4 && point.state !== 'select') {
21573 clearTimeout(point.colorInterval);
21574 point.colorInterval = setInterval(function() {
21575 var pos = (new Date() - start) / duration,
21576 graphic = point.graphic;
21577 if (pos > 1) {
21578 pos = 1;
21579 }
21580 if (graphic) {
21581 graphic.attr('fill', ColorAxis.prototype.tweenColors.call(0, hoverColor, normalColor, pos));
21582 }
21583 if (pos >= 1) {
21584 clearTimeout(point.colorInterval);
21585 }
21586 }, 13);
21587 point.isFading = true;
21588 }
21589 Point.prototype.onMouseOut.call(point);
21590 point.isFading = null;
21591 },
21592
21593
21594 /**
21595 * Zoom the chart to view a specific area point
21596 */
21597 zoomTo: function() {
21598 var point = this,
21599 series = point.series;
21600
21601 series.xAxis.setExtremes(
21602 point._minX,
21603 point._maxX,
21604 false
21605 );
21606 series.yAxis.setExtremes(
21607 point._minY,
21608 point._maxY,
21609 false
21610 );
21611 series.chart.redraw();
21612 }
21613 }, colorPointMixin));
21614
21615 }(Highcharts));
21616 (function(H) {
21617 /**
21618 * (c) 2010-2016 Torstein Honsi
21619 *
21620 * License: www.highcharts.com/license
21621 */
21622 'use strict';
21623 var addEvent = H.addEvent,
21624 Chart = H.Chart,
21625 doc = H.doc,
21626 each = H.each,
21627 extend = H.extend,
21628 merge = H.merge,
21629 pick = H.pick,
21630 wrap = H.wrap;
21631
21632 function stopEvent(e) {
21633 if (e) {
21634 if (e.preventDefault) {
21635 e.preventDefault();
21636 }
21637 if (e.stopPropagation) {
21638 e.stopPropagation();
21639 }
21640 e.cancelBubble = true;
21641 }
21642 }
21643
21644 // Add events to the Chart object itself
21645 extend(Chart.prototype, {
21646 renderMapNavigation: function() {
21647 var chart = this,
21648 options = this.options.mapNavigation,
21649 buttons = options.buttons,
21650 n,
21651 button,
21652 buttonOptions,
21653 attr,
21654 states,
21655 hoverStates,
21656 selectStates,
21657 outerHandler = function(e) {
21658 this.handler.call(chart, e);
21659 stopEvent(e); // Stop default click event (#4444)
21660 };
21661
21662 if (pick(options.enableButtons, options.enabled) && !chart.renderer.forExport) {
21663 chart.mapNavButtons = [];
21664 for (n in buttons) {
21665 if (buttons.hasOwnProperty(n)) {
21666 buttonOptions = merge(options.buttonOptions, buttons[n]);
21667
21668
21669 // Presentational
21670 attr = buttonOptions.theme;
21671 attr.style = merge(buttonOptions.theme.style, buttonOptions.style); // #3203
21672 states = attr.states;
21673 hoverStates = states && states.hover;
21674 selectStates = states && states.select;
21675
21676
21677 button = chart.renderer.button(
21678 buttonOptions.text,
21679 0,
21680 0,
21681 outerHandler,
21682 attr,
21683 hoverStates,
21684 selectStates,
21685 0,
21686 n === 'zoomIn' ? 'topbutton' : 'bottombutton'
21687 )
21688 .addClass('highcharts-map-navigation')
21689 .attr({
21690 width: buttonOptions.width,
21691 height: buttonOptions.height,
21692 title: chart.options.lang[n],
21693 padding: buttonOptions.padding,
21694 zIndex: 5
21695 })
21696 .add();
21697 button.handler = buttonOptions.onclick;
21698 button.align(extend(buttonOptions, {
21699 width: button.width,
21700 height: 2 * button.height
21701 }), null, buttonOptions.alignTo);
21702 addEvent(button.element, 'dblclick', stopEvent); // Stop double click event (#4444)
21703 chart.mapNavButtons.push(button);
21704 }
21705 }
21706 }
21707 },
21708
21709 /**
21710 * Fit an inner box to an outer. If the inner box overflows left or right, align it to the sides of the
21711 * outer. If it overflows both sides, fit it within the outer. This is a pattern that occurs more places
21712 * in Highcharts, perhaps it should be elevated to a common utility function.
21713 */
21714 fitToBox: function(inner, outer) {
21715 each([
21716 ['x', 'width'],
21717 ['y', 'height']
21718 ], function(dim) {
21719 var pos = dim[0],
21720 size = dim[1];
21721
21722 if (inner[pos] + inner[size] > outer[pos] + outer[size]) { // right overflow
21723 if (inner[size] > outer[size]) { // the general size is greater, fit fully to outer
21724 inner[size] = outer[size];
21725 inner[pos] = outer[pos];
21726 } else { // align right
21727 inner[pos] = outer[pos] + outer[size] - inner[size];
21728 }
21729 }
21730 if (inner[size] > outer[size]) {
21731 inner[size] = outer[size];
21732 }
21733 if (inner[pos] < outer[pos]) {
21734 inner[pos] = outer[pos];
21735 }
21736 });
21737
21738
21739 return inner;
21740 },
21741
21742 /**
21743 * Zoom the map in or out by a certain amount. Less than 1 zooms in, greater than 1 zooms out.
21744 */
21745 mapZoom: function(howMuch, centerXArg, centerYArg, mouseX, mouseY) {
21746 /*if (this.isMapZooming) {
21747 this.mapZoomQueue = arguments;
21748 return;
21749 }*/
21750
21751 var chart = this,
21752 xAxis = chart.xAxis[0],
21753 xRange = xAxis.max - xAxis.min,
21754 centerX = pick(centerXArg, xAxis.min + xRange / 2),
21755 newXRange = xRange * howMuch,
21756 yAxis = chart.yAxis[0],
21757 yRange = yAxis.max - yAxis.min,
21758 centerY = pick(centerYArg, yAxis.min + yRange / 2),
21759 newYRange = yRange * howMuch,
21760 fixToX = mouseX ? ((mouseX - xAxis.pos) / xAxis.len) : 0.5,
21761 fixToY = mouseY ? ((mouseY - yAxis.pos) / yAxis.len) : 0.5,
21762 newXMin = centerX - newXRange * fixToX,
21763 newYMin = centerY - newYRange * fixToY,
21764 newExt = chart.fitToBox({
21765 x: newXMin,
21766 y: newYMin,
21767 width: newXRange,
21768 height: newYRange
21769 }, {
21770 x: xAxis.dataMin,
21771 y: yAxis.dataMin,
21772 width: xAxis.dataMax - xAxis.dataMin,
21773 height: yAxis.dataMax - yAxis.dataMin
21774 }),
21775 zoomOut = newExt.x <= xAxis.dataMin &&
21776 newExt.width >= xAxis.dataMax - xAxis.dataMin &&
21777 newExt.y <= yAxis.dataMin &&
21778 newExt.height >= yAxis.dataMax - yAxis.dataMin;
21779
21780 // When mousewheel zooming, fix the point under the mouse
21781 if (mouseX) {
21782 xAxis.fixTo = [mouseX - xAxis.pos, centerXArg];
21783 }
21784 if (mouseY) {
21785 yAxis.fixTo = [mouseY - yAxis.pos, centerYArg];
21786 }
21787
21788 // Zoom
21789 if (howMuch !== undefined && !zoomOut) {
21790 xAxis.setExtremes(newExt.x, newExt.x + newExt.width, false);
21791 yAxis.setExtremes(newExt.y, newExt.y + newExt.height, false);
21792
21793 // Reset zoom
21794 } else {
21795 xAxis.setExtremes(undefined, undefined, false);
21796 yAxis.setExtremes(undefined, undefined, false);
21797 }
21798
21799 // Prevent zooming until this one is finished animating
21800 /*chart.holdMapZoom = true;
21801 setTimeout(function () {
21802 chart.holdMapZoom = false;
21803 }, 200);*/
21804 /*delay = animation ? animation.duration || 500 : 0;
21805 if (delay) {
21806 chart.isMapZooming = true;
21807 setTimeout(function () {
21808 chart.isMapZooming = false;
21809 if (chart.mapZoomQueue) {
21810 chart.mapZoom.apply(chart, chart.mapZoomQueue);
21811 }
21812 chart.mapZoomQueue = null;
21813 }, delay);
21814 }*/
21815
21816 chart.redraw();
21817 }
21818 });
21819
21820 /**
21821 * Extend the Chart.render method to add zooming and panning
21822 */
21823 wrap(Chart.prototype, 'render', function(proceed) {
21824 var chart = this,
21825 mapNavigation = chart.options.mapNavigation;
21826
21827 // Render the plus and minus buttons. Doing this before the shapes makes getBBox much quicker, at least in Chrome.
21828 chart.renderMapNavigation();
21829
21830 proceed.call(chart);
21831
21832 // Add the double click event
21833 if (pick(mapNavigation.enableDoubleClickZoom, mapNavigation.enabled) || mapNavigation.enableDoubleClickZoomTo) {
21834 addEvent(chart.container, 'dblclick', function(e) {
21835 chart.pointer.onContainerDblClick(e);
21836 });
21837 }
21838
21839 // Add the mousewheel event
21840 if (pick(mapNavigation.enableMouseWheelZoom, mapNavigation.enabled)) {
21841 addEvent(chart.container, doc.onmousewheel === undefined ? 'DOMMouseScroll' : 'mousewheel', function(e) {
21842 chart.pointer.onContainerMouseWheel(e);
21843 stopEvent(e); // Issue #5011, returning false from non-jQuery event does not prevent default
21844 return false;
21845 });
21846 }
21847 });
21848
21849 }(Highcharts));
21850 (function(H) {
21851 /**
21852 * (c) 2010-2016 Torstein Honsi
21853 *
21854 * License: www.highcharts.com/license
21855 */
21856 'use strict';
21857 var extend = H.extend,
21858 pick = H.pick,
21859 Pointer = H.Pointer,
21860 wrap = H.wrap;
21861
21862 // Extend the Pointer
21863 extend(Pointer.prototype, {
21864
21865 /**
21866 * The event handler for the doubleclick event
21867 */
21868 onContainerDblClick: function(e) {
21869 var chart = this.chart;
21870
21871 e = this.normalize(e);
21872
21873 if (chart.options.mapNavigation.enableDoubleClickZoomTo) {
21874 if (chart.pointer.inClass(e.target, 'highcharts-tracker') && chart.hoverPoint) {
21875 chart.hoverPoint.zoomTo();
21876 }
21877 } else if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
21878 chart.mapZoom(
21879 0.5,
21880 chart.xAxis[0].toValue(e.chartX),
21881 chart.yAxis[0].toValue(e.chartY),
21882 e.chartX,
21883 e.chartY
21884 );
21885 }
21886 },
21887
21888 /**
21889 * The event handler for the mouse scroll event
21890 */
21891 onContainerMouseWheel: function(e) {
21892 var chart = this.chart,
21893 delta;
21894
21895 e = this.normalize(e);
21896
21897 // Firefox uses e.detail, WebKit and IE uses wheelDelta
21898 delta = e.detail || -(e.wheelDelta / 120);
21899 if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
21900 chart.mapZoom(
21901 Math.pow(chart.options.mapNavigation.mouseWheelSensitivity, delta),
21902 chart.xAxis[0].toValue(e.chartX),
21903 chart.yAxis[0].toValue(e.chartY),
21904 e.chartX,
21905 e.chartY
21906 );
21907 }
21908 }
21909 });
21910
21911 // The pinchType is inferred from mapNavigation options.
21912 wrap(Pointer.prototype, 'zoomOption', function(proceed) {
21913
21914
21915 var mapNavigation = this.chart.options.mapNavigation;
21916
21917 // Pinch status
21918 if (pick(mapNavigation.enableTouchZoom, mapNavigation.enabled)) {
21919 this.chart.options.chart.pinchType = 'xy';
21920 }
21921
21922 proceed.apply(this, [].slice.call(arguments, 1));
21923
21924 });
21925
21926 // Extend the pinchTranslate method to preserve fixed ratio when zooming
21927 wrap(Pointer.prototype, 'pinchTranslate', function(proceed, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) {
21928 var xBigger;
21929 proceed.call(this, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
21930
21931 // Keep ratio
21932 if (this.chart.options.chart.type === 'map' && this.hasZoom) {
21933 xBigger = transform.scaleX > transform.scaleY;
21934 this.pinchTranslateDirection(!xBigger,
21935 pinchDown,
21936 touches,
21937 transform,
21938 selectionMarker,
21939 clip,
21940 lastValidTouch,
21941 xBigger ? transform.scaleX : transform.scaleY
21942 );
21943 }
21944 });
21945
21946 }(Highcharts));
21947 (function(H) {
21948 /**
21949 * (c) 2010-2016 Torstein Honsi
21950 *
21951 * License: www.highcharts.com/license
21952 */
21953 'use strict';
21954 var seriesType = H.seriesType,
21955 seriesTypes = H.seriesTypes;
21956
21957 // The mapline series type
21958 seriesType('mapline', 'map', {
21959
21960 lineWidth: 1,
21961 fillColor: 'none'
21962
21963 }, {
21964 type: 'mapline',
21965 colorProp: 'stroke',
21966
21967 pointAttrToOptions: {
21968 'stroke': 'color',
21969 'stroke-width': 'lineWidth'
21970 },
21971 /**
21972 * Get presentational attributes
21973 */
21974 pointAttribs: function(point, state) {
21975 var attr = seriesTypes.map.prototype.pointAttribs.call(this, point, state);
21976
21977 // The difference from a map series is that the stroke takes the point color
21978 attr.fill = this.options.fillColor;
21979
21980 return attr;
21981 },
21982
21983 drawLegendSymbol: seriesTypes.line.prototype.drawLegendSymbol
21984 });
21985
21986 }(Highcharts));
21987 (function(H) {
21988 /**
21989 * (c) 2010-2016 Torstein Honsi
21990 *
21991 * License: www.highcharts.com/license
21992 */
21993 'use strict';
21994 var merge = H.merge,
21995 Point = H.Point,
21996 seriesType = H.seriesType;
21997
21998 // The mappoint series type
21999 seriesType('mappoint', 'scatter', {
22000 dataLabels: {
22001 enabled: true,
22002 formatter: function() { // #2945
22003 return this.point.name;
22004 },
22005 crop: false,
22006 defer: false,
22007 overflow: false,
22008 style: {
22009 color: '#000000'
22010 }
22011 }
22012
22013 // Prototype members
22014 }, {
22015 type: 'mappoint',
22016 forceDL: true
22017
22018 // Point class
22019 }, {
22020 applyOptions: function(options, x) {
22021 var mergedOptions = options.lat !== undefined && options.lon !== undefined ? merge(options, this.series.chart.fromLatLonToPoint(options)) : options;
22022 return Point.prototype.applyOptions.call(this, mergedOptions, x);
22023 }
22024 });
22025
22026 }(Highcharts));
22027 (function(H) {
22028 /**
22029 * (c) 2010-2016 Torstein Honsi
22030 *
22031 * License: www.highcharts.com/license
22032 */
22033 'use strict';
22034 var arrayMax = H.arrayMax,
22035 arrayMin = H.arrayMin,
22036 Axis = H.Axis,
22037 color = H.color,
22038 each = H.each,
22039 isNumber = H.isNumber,
22040 noop = H.noop,
22041 pick = H.pick,
22042 pInt = H.pInt,
22043 Point = H.Point,
22044 Series = H.Series,
22045 seriesType = H.seriesType,
22046 seriesTypes = H.seriesTypes;
22047
22048 /* ****************************************************************************
22049 * Start Bubble series code *
22050 *****************************************************************************/
22051
22052 seriesType('bubble', 'scatter', {
22053 dataLabels: {
22054 formatter: function() { // #2945
22055 return this.point.z;
22056 },
22057 inside: true,
22058 verticalAlign: 'middle'
22059 },
22060 // displayNegative: true,
22061 marker: {
22062
22063 // fillOpacity: 0.5,
22064 lineColor: null, // inherit from series.color
22065 lineWidth: 1,
22066
22067 // Avoid offset in Point.setState
22068 radius: null,
22069 states: {
22070 hover: {
22071 radiusPlus: 0
22072 }
22073 }
22074 },
22075 minSize: 8,
22076 maxSize: '20%',
22077 // negativeColor: null,
22078 // sizeBy: 'area'
22079 softThreshold: false,
22080 states: {
22081 hover: {
22082 halo: {
22083 size: 5
22084 }
22085 }
22086 },
22087 tooltip: {
22088 pointFormat: '({point.x}, {point.y}), Size: {point.z}'
22089 },
22090 turboThreshold: 0,
22091 zThreshold: 0,
22092 zoneAxis: 'z'
22093
22094 // Prototype members
22095 }, {
22096 pointArrayMap: ['y', 'z'],
22097 parallelArrays: ['x', 'y', 'z'],
22098 trackerGroups: ['group', 'dataLabelsGroup'],
22099 bubblePadding: true,
22100 zoneAxis: 'z',
22101 markerAttribs: noop,
22102
22103
22104 pointAttribs: function(point, state) {
22105 var markerOptions = this.options.marker,
22106 fillOpacity = pick(markerOptions.fillOpacity, 0.5),
22107 attr = Series.prototype.pointAttribs.call(this, point, state);
22108
22109 if (fillOpacity !== 1) {
22110 attr.fill = color(attr.fill).setOpacity(fillOpacity).get('rgba');
22111 }
22112
22113 return attr;
22114 },
22115
22116
22117 /**
22118 * Get the radius for each point based on the minSize, maxSize and each point's Z value. This
22119 * must be done prior to Series.translate because the axis needs to add padding in
22120 * accordance with the point sizes.
22121 */
22122 getRadii: function(zMin, zMax, minSize, maxSize) {
22123 var len,
22124 i,
22125 pos,
22126 zData = this.zData,
22127 radii = [],
22128 options = this.options,
22129 sizeByArea = options.sizeBy !== 'width',
22130 zThreshold = options.zThreshold,
22131 zRange = zMax - zMin,
22132 value,
22133 radius;
22134
22135 // Set the shape type and arguments to be picked up in drawPoints
22136 for (i = 0, len = zData.length; i < len; i++) {
22137
22138 value = zData[i];
22139
22140 // When sizing by threshold, the absolute value of z determines the size
22141 // of the bubble.
22142 if (options.sizeByAbsoluteValue && value !== null) {
22143 value = Math.abs(value - zThreshold);
22144 zMax = Math.max(zMax - zThreshold, Math.abs(zMin - zThreshold));
22145 zMin = 0;
22146 }
22147
22148 if (value === null) {
22149 radius = null;
22150 // Issue #4419 - if value is less than zMin, push a radius that's always smaller than the minimum size
22151 } else if (value < zMin) {
22152 radius = minSize / 2 - 1;
22153 } else {
22154 // Relative size, a number between 0 and 1
22155 pos = zRange > 0 ? (value - zMin) / zRange : 0.5;
22156
22157 if (sizeByArea && pos >= 0) {
22158 pos = Math.sqrt(pos);
22159 }
22160 radius = Math.ceil(minSize + pos * (maxSize - minSize)) / 2;
22161 }
22162 radii.push(radius);
22163 }
22164 this.radii = radii;
22165 },
22166
22167 /**
22168 * Perform animation on the bubbles
22169 */
22170 animate: function(init) {
22171 var animation = this.options.animation;
22172
22173 if (!init) { // run the animation
22174 each(this.points, function(point) {
22175 var graphic = point.graphic,
22176 shapeArgs = point.shapeArgs;
22177
22178 if (graphic && shapeArgs) {
22179 // start values
22180 graphic.attr('r', 1);
22181
22182 // animate
22183 graphic.animate({
22184 r: shapeArgs.r
22185 }, animation);
22186 }
22187 });
22188
22189 // delete this function to allow it only once
22190 this.animate = null;
22191 }
22192 },
22193
22194 /**
22195 * Extend the base translate method to handle bubble size
22196 */
22197 translate: function() {
22198
22199 var i,
22200 data = this.data,
22201 point,
22202 radius,
22203 radii = this.radii;
22204
22205 // Run the parent method
22206 seriesTypes.scatter.prototype.translate.call(this);
22207
22208 // Set the shape type and arguments to be picked up in drawPoints
22209 i = data.length;
22210
22211 while (i--) {
22212 point = data[i];
22213 radius = radii ? radii[i] : 0; // #1737
22214
22215 if (isNumber(radius) && radius >= this.minPxSize / 2) {
22216 // Shape arguments
22217 point.shapeType = 'circle';
22218 point.shapeArgs = {
22219 x: point.plotX,
22220 y: point.plotY,
22221 r: radius
22222 };
22223
22224 // Alignment box for the data label
22225 point.dlBox = {
22226 x: point.plotX - radius,
22227 y: point.plotY - radius,
22228 width: 2 * radius,
22229 height: 2 * radius
22230 };
22231 } else { // below zThreshold
22232 point.shapeArgs = point.plotY = point.dlBox = undefined; // #1691
22233 }
22234 }
22235 },
22236
22237 /**
22238 * Get the series' symbol in the legend
22239 *
22240 * @param {Object} legend The legend object
22241 * @param {Object} item The series (this) or point
22242 */
22243 drawLegendSymbol: function(legend, item) {
22244 var renderer = this.chart.renderer,
22245 radius = renderer.fontMetrics(
22246 legend.itemStyle && legend.itemStyle.fontSize,
22247 item.legendItem
22248 ).f / 2;
22249
22250 item.legendSymbol = renderer.circle(
22251 radius,
22252 legend.baseline - radius,
22253 radius
22254 ).attr({
22255 zIndex: 3
22256 }).add(item.legendGroup);
22257 item.legendSymbol.isMarker = true;
22258
22259 },
22260
22261 drawPoints: seriesTypes.column.prototype.drawPoints,
22262 alignDataLabel: seriesTypes.column.prototype.alignDataLabel,
22263 buildKDTree: noop,
22264 applyZones: noop
22265
22266 // Point class
22267 }, {
22268 haloPath: function(size) {
22269 return Point.prototype.haloPath.call(
22270 this,
22271 this.shapeArgs.r + size
22272 );
22273 },
22274 ttBelow: false
22275 });
22276
22277 /**
22278 * Add logic to pad each axis with the amount of pixels
22279 * necessary to avoid the bubbles to overflow.
22280 */
22281 Axis.prototype.beforePadding = function() {
22282 var axis = this,
22283 axisLength = this.len,
22284 chart = this.chart,
22285 pxMin = 0,
22286 pxMax = axisLength,
22287 isXAxis = this.isXAxis,
22288 dataKey = isXAxis ? 'xData' : 'yData',
22289 min = this.min,
22290 extremes = {},
22291 smallestSize = Math.min(chart.plotWidth, chart.plotHeight),
22292 zMin = Number.MAX_VALUE,
22293 zMax = -Number.MAX_VALUE,
22294 range = this.max - min,
22295 transA = axisLength / range,
22296 activeSeries = [];
22297
22298 // Handle padding on the second pass, or on redraw
22299 each(this.series, function(series) {
22300
22301 var seriesOptions = series.options,
22302 zData;
22303
22304 if (series.bubblePadding && (series.visible || !chart.options.chart.ignoreHiddenSeries)) {
22305
22306 // Correction for #1673
22307 axis.allowZoomOutside = true;
22308
22309 // Cache it
22310 activeSeries.push(series);
22311
22312 if (isXAxis) { // because X axis is evaluated first
22313
22314 // For each series, translate the size extremes to pixel values
22315 each(['minSize', 'maxSize'], function(prop) {
22316 var length = seriesOptions[prop],
22317 isPercent = /%$/.test(length);
22318
22319 length = pInt(length);
22320 extremes[prop] = isPercent ?
22321 smallestSize * length / 100 :
22322 length;
22323
22324 });
22325 series.minPxSize = extremes.minSize;
22326 // Prioritize min size if conflict to make sure bubbles are
22327 // always visible. #5873
22328 series.maxPxSize = Math.max(extremes.maxSize, extremes.minSize);
22329
22330 // Find the min and max Z
22331 zData = series.zData;
22332 if (zData.length) { // #1735
22333 zMin = pick(seriesOptions.zMin, Math.min(
22334 zMin,
22335 Math.max(
22336 arrayMin(zData),
22337 seriesOptions.displayNegative === false ? seriesOptions.zThreshold : -Number.MAX_VALUE
22338 )
22339 ));
22340 zMax = pick(seriesOptions.zMax, Math.max(zMax, arrayMax(zData)));
22341 }
22342 }
22343 }
22344 });
22345
22346 each(activeSeries, function(series) {
22347
22348 var data = series[dataKey],
22349 i = data.length,
22350 radius;
22351
22352 if (isXAxis) {
22353 series.getRadii(zMin, zMax, series.minPxSize, series.maxPxSize);
22354 }
22355
22356 if (range > 0) {
22357 while (i--) {
22358 if (isNumber(data[i]) && axis.dataMin <= data[i] && data[i] <= axis.dataMax) {
22359 radius = series.radii[i];
22360 pxMin = Math.min(((data[i] - min) * transA) - radius, pxMin);
22361 pxMax = Math.max(((data[i] - min) * transA) + radius, pxMax);
22362 }
22363 }
22364 }
22365 });
22366
22367 if (activeSeries.length && range > 0 && !this.isLog) {
22368 pxMax -= axisLength;
22369 transA *= (axisLength + pxMin - pxMax) / axisLength;
22370 each([
22371 ['min', 'userMin', pxMin],
22372 ['max', 'userMax', pxMax]
22373 ], function(keys) {
22374 if (pick(axis.options[keys[0]], axis[keys[1]]) === undefined) {
22375 axis[keys[0]] += keys[2] / transA;
22376 }
22377 });
22378 }
22379 };
22380
22381 /* ****************************************************************************
22382 * End Bubble series code *
22383 *****************************************************************************/
22384
22385 }(Highcharts));
22386 (function(H) {
22387 /**
22388 * (c) 2010-2016 Torstein Honsi
22389 *
22390 * License: www.highcharts.com/license
22391 */
22392 'use strict';
22393 var merge = H.merge,
22394 Point = H.Point,
22395 seriesType = H.seriesType,
22396 seriesTypes = H.seriesTypes;
22397
22398 // The mapbubble series type
22399 if (seriesTypes.bubble) {
22400
22401 seriesType('mapbubble', 'bubble', {
22402 animationLimit: 500,
22403 tooltip: {
22404 pointFormat: '{point.name}: {point.z}'
22405 }
22406
22407 // Prototype members
22408 }, {
22409 xyFromShape: true,
22410 type: 'mapbubble',
22411 pointArrayMap: ['z'], // If one single value is passed, it is interpreted as z
22412 /**
22413 * Return the map area identified by the dataJoinBy option
22414 */
22415 getMapData: seriesTypes.map.prototype.getMapData,
22416 getBox: seriesTypes.map.prototype.getBox,
22417 setData: seriesTypes.map.prototype.setData
22418
22419 // Point class
22420 }, {
22421 applyOptions: function(options, x) {
22422 var point;
22423 if (options && options.lat !== undefined && options.lon !== undefined) {
22424 point = Point.prototype.applyOptions.call(
22425 this,
22426 merge(options, this.series.chart.fromLatLonToPoint(options)),
22427 x
22428 );
22429 } else {
22430 point = seriesTypes.map.prototype.pointClass.prototype.applyOptions.call(this, options, x);
22431 }
22432 return point;
22433 },
22434 ttBelow: false
22435 });
22436 }
22437
22438 }(Highcharts));
22439 (function(H) {
22440 /**
22441 * (c) 2010-2016 Torstein Honsi
22442 *
22443 * License: www.highcharts.com/license
22444 */
22445 'use strict';
22446 var Chart = H.Chart,
22447 each = H.each,
22448 extend = H.extend,
22449 error = H.error,
22450 format = H.format,
22451 merge = H.merge,
22452 win = H.win,
22453 wrap = H.wrap;
22454 /**
22455 * Test for point in polygon. Polygon defined as array of [x,y] points.
22456 */
22457 function pointInPolygon(point, polygon) {
22458 var i,
22459 j,
22460 rel1,
22461 rel2,
22462 c = false,
22463 x = point.x,
22464 y = point.y;
22465
22466 for (i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
22467 rel1 = polygon[i][1] > y;
22468 rel2 = polygon[j][1] > y;
22469 if (rel1 !== rel2 && (x < (polygon[j][0] - polygon[i][0]) * (y - polygon[i][1]) / (polygon[j][1] - polygon[i][1]) + polygon[i][0])) {
22470 c = !c;
22471 }
22472 }
22473
22474 return c;
22475 }
22476
22477 /**
22478 * Get point from latLon using specified transform definition
22479 */
22480 Chart.prototype.transformFromLatLon = function(latLon, transform) {
22481 if (win.proj4 === undefined) {
22482 error(21);
22483 return {
22484 x: 0,
22485 y: null
22486 };
22487 }
22488
22489 var projected = win.proj4(transform.crs, [latLon.lon, latLon.lat]),
22490 cosAngle = transform.cosAngle || (transform.rotation && Math.cos(transform.rotation)),
22491 sinAngle = transform.sinAngle || (transform.rotation && Math.sin(transform.rotation)),
22492 rotated = transform.rotation ? [projected[0] * cosAngle + projected[1] * sinAngle, -projected[0] * sinAngle + projected[1] * cosAngle] : projected;
22493
22494 return {
22495 x: ((rotated[0] - (transform.xoffset || 0)) * (transform.scale || 1) + (transform.xpan || 0)) * (transform.jsonres || 1) + (transform.jsonmarginX || 0),
22496 y: (((transform.yoffset || 0) - rotated[1]) * (transform.scale || 1) + (transform.ypan || 0)) * (transform.jsonres || 1) - (transform.jsonmarginY || 0)
22497 };
22498 };
22499
22500 /**
22501 * Get latLon from point using specified transform definition
22502 */
22503 Chart.prototype.transformToLatLon = function(point, transform) {
22504 if (win.proj4 === undefined) {
22505 error(21);
22506 return;
22507 }
22508
22509 var normalized = {
22510 x: ((point.x - (transform.jsonmarginX || 0)) / (transform.jsonres || 1) - (transform.xpan || 0)) / (transform.scale || 1) + (transform.xoffset || 0),
22511 y: ((-point.y - (transform.jsonmarginY || 0)) / (transform.jsonres || 1) + (transform.ypan || 0)) / (transform.scale || 1) + (transform.yoffset || 0)
22512 },
22513 cosAngle = transform.cosAngle || (transform.rotation && Math.cos(transform.rotation)),
22514 sinAngle = transform.sinAngle || (transform.rotation && Math.sin(transform.rotation)),
22515 // Note: Inverted sinAngle to reverse rotation direction
22516 projected = win.proj4(transform.crs, 'WGS84', transform.rotation ? {
22517 x: normalized.x * cosAngle + normalized.y * -sinAngle,
22518 y: normalized.x * sinAngle + normalized.y * cosAngle
22519 } : normalized);
22520
22521 return {
22522 lat: projected.y,
22523 lon: projected.x
22524 };
22525 };
22526
22527 Chart.prototype.fromPointToLatLon = function(point) {
22528 var transforms = this.mapTransforms,
22529 transform;
22530
22531 if (!transforms) {
22532 error(22);
22533 return;
22534 }
22535
22536 for (transform in transforms) {
22537 if (transforms.hasOwnProperty(transform) && transforms[transform].hitZone &&
22538 pointInPolygon({
22539 x: point.x,
22540 y: -point.y
22541 }, transforms[transform].hitZone.coordinates[0])) {
22542 return this.transformToLatLon(point, transforms[transform]);
22543 }
22544 }
22545
22546 return this.transformToLatLon(point, transforms['default']); // eslint-disable-line dot-notation
22547 };
22548
22549 Chart.prototype.fromLatLonToPoint = function(latLon) {
22550 var transforms = this.mapTransforms,
22551 transform,
22552 coords;
22553
22554 if (!transforms) {
22555 error(22);
22556 return {
22557 x: 0,
22558 y: null
22559 };
22560 }
22561
22562 for (transform in transforms) {
22563 if (transforms.hasOwnProperty(transform) && transforms[transform].hitZone) {
22564 coords = this.transformFromLatLon(latLon, transforms[transform]);
22565 if (pointInPolygon({
22566 x: coords.x,
22567 y: -coords.y
22568 }, transforms[transform].hitZone.coordinates[0])) {
22569 return coords;
22570 }
22571 }
22572 }
22573
22574 return this.transformFromLatLon(latLon, transforms['default']); // eslint-disable-line dot-notation
22575 };
22576
22577 /**
22578 * Convert a geojson object to map data of a given Highcharts type (map, mappoint or mapline).
22579 */
22580 H.geojson = function(geojson, hType, series) {
22581 var mapData = [],
22582 path = [],
22583 polygonToPath = function(polygon) {
22584 var i,
22585 len = polygon.length;
22586 path.push('M');
22587 for (i = 0; i < len; i++) {
22588 if (i === 1) {
22589 path.push('L');
22590 }
22591 path.push(polygon[i][0], -polygon[i][1]);
22592 }
22593 };
22594
22595 hType = hType || 'map';
22596
22597 each(geojson.features, function(feature) {
22598
22599 var geometry = feature.geometry,
22600 type = geometry.type,
22601 coordinates = geometry.coordinates,
22602 properties = feature.properties,
22603 point;
22604
22605 path = [];
22606
22607 if (hType === 'map' || hType === 'mapbubble') {
22608 if (type === 'Polygon') {
22609 each(coordinates, polygonToPath);
22610 path.push('Z');
22611
22612 } else if (type === 'MultiPolygon') {
22613 each(coordinates, function(items) {
22614 each(items, polygonToPath);
22615 });
22616 path.push('Z');
22617 }
22618
22619 if (path.length) {
22620 point = {
22621 path: path
22622 };
22623 }
22624
22625 } else if (hType === 'mapline') {
22626 if (type === 'LineString') {
22627 polygonToPath(coordinates);
22628 } else if (type === 'MultiLineString') {
22629 each(coordinates, polygonToPath);
22630 }
22631
22632 if (path.length) {
22633 point = {
22634 path: path
22635 };
22636 }
22637
22638 } else if (hType === 'mappoint') {
22639 if (type === 'Point') {
22640 point = {
22641 x: coordinates[0],
22642 y: -coordinates[1]
22643 };
22644 }
22645 }
22646 if (point) {
22647 mapData.push(extend(point, {
22648 name: properties.name || properties.NAME,
22649 properties: properties
22650 }));
22651 }
22652
22653 });
22654
22655 // Create a credits text that includes map source, to be picked up in Chart.addCredits
22656 if (series && geojson.copyrightShort) {
22657 series.chart.mapCredits = format(series.chart.options.credits.mapText, {
22658 geojson: geojson
22659 });
22660 series.chart.mapCreditsFull = format(series.chart.options.credits.mapTextFull, {
22661 geojson: geojson
22662 });
22663 }
22664
22665 return mapData;
22666 };
22667
22668 /**
22669 * Override addCredits to include map source by default
22670 */
22671 wrap(Chart.prototype, 'addCredits', function(proceed, credits) {
22672
22673 credits = merge(true, this.options.credits, credits);
22674
22675 // Disable credits link if map credits enabled. This to allow for in-text anchors.
22676 if (this.mapCredits) {
22677 credits.href = null;
22678 }
22679
22680 proceed.call(this, credits);
22681
22682 // Add full map credits to hover
22683 if (this.credits && this.mapCreditsFull) {
22684 this.credits.attr({
22685 title: this.mapCreditsFull
22686 });
22687 }
22688 });
22689
22690 }(Highcharts));
22691 (function(H) {
22692 /**
22693 * (c) 2010-2016 Torstein Honsi
22694 *
22695 * License: www.highcharts.com/license
22696 */
22697 'use strict';
22698 var Chart = H.Chart,
22699 defaultOptions = H.defaultOptions,
22700 each = H.each,
22701 extend = H.extend,
22702 merge = H.merge,
22703 pick = H.pick,
22704 Renderer = H.Renderer,
22705 SVGRenderer = H.SVGRenderer,
22706 VMLRenderer = H.VMLRenderer;
22707
22708
22709 // Add language
22710 extend(defaultOptions.lang, {
22711 zoomIn: 'Zoom in',
22712 zoomOut: 'Zoom out'
22713 });
22714
22715
22716 // Set the default map navigation options
22717 defaultOptions.mapNavigation = {
22718 buttonOptions: {
22719 alignTo: 'plotBox',
22720 align: 'left',
22721 verticalAlign: 'top',
22722 x: 0,
22723 width: 18,
22724 height: 18,
22725 padding: 5,
22726
22727 style: {
22728 fontSize: '15px',
22729 fontWeight: 'bold'
22730 },
22731 theme: {
22732 'stroke-width': 1,
22733 'text-align': 'center'
22734 }
22735
22736 },
22737 buttons: {
22738 zoomIn: {
22739 onclick: function() {
22740 this.mapZoom(0.5);
22741 },
22742 text: '+',
22743 y: 0
22744 },
22745 zoomOut: {
22746 onclick: function() {
22747 this.mapZoom(2);
22748 },
22749 text: '-',
22750 y: 28
22751 }
22752 },
22753 mouseWheelSensitivity: 1.1
22754 // enabled: false,
22755 // enableButtons: null, // inherit from enabled
22756 // enableTouchZoom: null, // inherit from enabled
22757 // enableDoubleClickZoom: null, // inherit from enabled
22758 // enableDoubleClickZoomTo: false
22759 // enableMouseWheelZoom: null, // inherit from enabled
22760 };
22761
22762 /**
22763 * Utility for reading SVG paths directly.
22764 */
22765 H.splitPath = function(path) {
22766 var i;
22767
22768 // Move letters apart
22769 path = path.replace(/([A-Za-z])/g, ' $1 ');
22770 // Trim
22771 path = path.replace(/^\s*/, '').replace(/\s*$/, '');
22772
22773 // Split on spaces and commas
22774 path = path.split(/[ ,]+/); // Extra comma to escape gulp.scripts task
22775
22776 // Parse numbers
22777 for (i = 0; i < path.length; i++) {
22778 if (!/[a-zA-Z]/.test(path[i])) {
22779 path[i] = parseFloat(path[i]);
22780 }
22781 }
22782 return path;
22783 };
22784
22785 // A placeholder for map definitions
22786 H.maps = {};
22787
22788
22789
22790
22791
22792 // Create symbols for the zoom buttons
22793 function selectiveRoundedRect(x, y, w, h, rTopLeft, rTopRight, rBottomRight, rBottomLeft) {
22794 return ['M', x + rTopLeft, y,
22795 // top side
22796 'L', x + w - rTopRight, y,
22797 // top right corner
22798 'C', x + w - rTopRight / 2, y, x + w, y + rTopRight / 2, x + w, y + rTopRight,
22799 // right side
22800 'L', x + w, y + h - rBottomRight,
22801 // bottom right corner
22802 'C', x + w, y + h - rBottomRight / 2, x + w - rBottomRight / 2, y + h, x + w - rBottomRight, y + h,
22803 // bottom side
22804 'L', x + rBottomLeft, y + h,
22805 // bottom left corner
22806 'C', x + rBottomLeft / 2, y + h, x, y + h - rBottomLeft / 2, x, y + h - rBottomLeft,
22807 // left side
22808 'L', x, y + rTopLeft,
22809 // top left corner
22810 'C', x, y + rTopLeft / 2, x + rTopLeft / 2, y, x + rTopLeft, y,
22811 'Z'
22812 ];
22813 }
22814 SVGRenderer.prototype.symbols.topbutton = function(x, y, w, h, attr) {
22815 return selectiveRoundedRect(x - 1, y - 1, w, h, attr.r, attr.r, 0, 0);
22816 };
22817 SVGRenderer.prototype.symbols.bottombutton = function(x, y, w, h, attr) {
22818 return selectiveRoundedRect(x - 1, y - 1, w, h, 0, 0, attr.r, attr.r);
22819 };
22820 // The symbol callbacks are generated on the SVGRenderer object in all browsers. Even
22821 // VML browsers need this in order to generate shapes in export. Now share
22822 // them with the VMLRenderer.
22823 if (Renderer === VMLRenderer) {
22824 each(['topbutton', 'bottombutton'], function(shape) {
22825 VMLRenderer.prototype.symbols[shape] = SVGRenderer.prototype.symbols[shape];
22826 });
22827 }
22828
22829
22830 /**
22831 * A wrapper for Chart with all the default values for a Map
22832 */
22833 H.Map = H.mapChart = function(a, b, c) {
22834
22835 var hasRenderToArg = typeof a === 'string' || a.nodeName,
22836 options = arguments[hasRenderToArg ? 1 : 0],
22837 hiddenAxis = {
22838 endOnTick: false,
22839 visible: false,
22840 minPadding: 0,
22841 maxPadding: 0,
22842 startOnTick: false
22843 },
22844 seriesOptions,
22845 defaultCreditsOptions = H.getOptions().credits;
22846
22847 /* For visual testing
22848 hiddenAxis.gridLineWidth = 1;
22849 hiddenAxis.gridZIndex = 10;
22850 hiddenAxis.tickPositions = undefined;
22851 // */
22852
22853 // Don't merge the data
22854 seriesOptions = options.series;
22855 options.series = null;
22856
22857 options = merge({
22858 chart: {
22859 panning: 'xy',
22860 type: 'map'
22861 },
22862 credits: {
22863 mapText: pick(defaultCreditsOptions.mapText, ' \u00a9 <a href="{geojson.copyrightUrl}">{geojson.copyrightShort}</a>'),
22864 mapTextFull: pick(defaultCreditsOptions.mapTextFull, '{geojson.copyright}')
22865 },
22866 tooltip: {
22867 followTouchMove: false
22868 },
22869 xAxis: hiddenAxis,
22870 yAxis: merge(hiddenAxis, {
22871 reversed: true
22872 })
22873 },
22874 options, // user's options
22875
22876 { // forced options
22877 chart: {
22878 inverted: false,
22879 alignTicks: false
22880 }
22881 }
22882 );
22883
22884 options.series = seriesOptions;
22885
22886
22887 return hasRenderToArg ?
22888 new Chart(a, options, c) :
22889 new Chart(options, b);
22890 };
22891
22892 }(Highcharts));
22893 (function(H) {
22894 /**
22895 * (c) 2010-2016 Torstein Honsi
22896 *
22897 * License: www.highcharts.com/license
22898 */
22899 'use strict';
22900 var colorPointMixin = H.colorPointMixin,
22901 colorSeriesMixin = H.colorSeriesMixin,
22902 each = H.each,
22903 LegendSymbolMixin = H.LegendSymbolMixin,
22904 merge = H.merge,
22905 noop = H.noop,
22906 pick = H.pick,
22907 Series = H.Series,
22908 seriesType = H.seriesType,
22909 seriesTypes = H.seriesTypes;
22910
22911 // The Heatmap series type
22912 seriesType('heatmap', 'scatter', {
22913 animation: false,
22914 borderWidth: 0,
22915
22916 nullColor: '#f7f7f7',
22917
22918 dataLabels: {
22919 formatter: function() { // #2945
22920 return this.point.value;
22921 },
22922 inside: true,
22923 verticalAlign: 'middle',
22924 crop: false,
22925 overflow: false,
22926 padding: 0 // #3837
22927 },
22928 marker: null,
22929 pointRange: null, // dynamically set to colsize by default
22930 tooltip: {
22931 pointFormat: '{point.x}, {point.y}: {point.value}<br/>'
22932 },
22933 states: {
22934 normal: {
22935 animation: true
22936 },
22937 hover: {
22938 halo: false, // #3406, halo is not required on heatmaps
22939 brightness: 0.2
22940 }
22941 }
22942 }, merge(colorSeriesMixin, {
22943 pointArrayMap: ['y', 'value'],
22944 hasPointSpecificOptions: true,
22945 supportsDrilldown: true,
22946 getExtremesFromAll: true,
22947 directTouch: true,
22948
22949 /**
22950 * Override the init method to add point ranges on both axes.
22951 */
22952 init: function() {
22953 var options;
22954 seriesTypes.scatter.prototype.init.apply(this, arguments);
22955
22956 options = this.options;
22957 options.pointRange = pick(options.pointRange, options.colsize || 1); // #3758, prevent resetting in setData
22958 this.yAxis.axisPointRange = options.rowsize || 1; // general point range
22959 },
22960 translate: function() {
22961 var series = this,
22962 options = series.options,
22963 xAxis = series.xAxis,
22964 yAxis = series.yAxis,
22965 between = function(x, a, b) {
22966 return Math.min(Math.max(a, x), b);
22967 };
22968
22969 series.generatePoints();
22970
22971 each(series.points, function(point) {
22972 var xPad = (options.colsize || 1) / 2,
22973 yPad = (options.rowsize || 1) / 2,
22974 x1 = between(Math.round(xAxis.len - xAxis.translate(point.x - xPad, 0, 1, 0, 1)), -xAxis.len, 2 * xAxis.len),
22975 x2 = between(Math.round(xAxis.len - xAxis.translate(point.x + xPad, 0, 1, 0, 1)), -xAxis.len, 2 * xAxis.len),
22976 y1 = between(Math.round(yAxis.translate(point.y - yPad, 0, 1, 0, 1)), -yAxis.len, 2 * yAxis.len),
22977 y2 = between(Math.round(yAxis.translate(point.y + yPad, 0, 1, 0, 1)), -yAxis.len, 2 * yAxis.len);
22978
22979 // Set plotX and plotY for use in K-D-Tree and more
22980 point.plotX = point.clientX = (x1 + x2) / 2;
22981 point.plotY = (y1 + y2) / 2;
22982
22983 point.shapeType = 'rect';
22984 point.shapeArgs = {
22985 x: Math.min(x1, x2),
22986 y: Math.min(y1, y2),
22987 width: Math.abs(x2 - x1),
22988 height: Math.abs(y2 - y1)
22989 };
22990 });
22991
22992 series.translateColors();
22993 },
22994 drawPoints: function() {
22995 seriesTypes.column.prototype.drawPoints.call(this);
22996
22997 each(this.points, function(point) {
22998 point.graphic.attr(this.colorAttribs(point, point.state));
22999 }, this);
23000 },
23001 animate: noop,
23002 getBox: noop,
23003 drawLegendSymbol: LegendSymbolMixin.drawRectangle,
23004 alignDataLabel: seriesTypes.column.prototype.alignDataLabel,
23005 getExtremes: function() {
23006 // Get the extremes from the value data
23007 Series.prototype.getExtremes.call(this, this.valueData);
23008 this.valueMin = this.dataMin;
23009 this.valueMax = this.dataMax;
23010
23011 // Get the extremes from the y data
23012 Series.prototype.getExtremes.call(this);
23013 }
23014
23015 }), colorPointMixin);
23016
23017 }(Highcharts));
23018 (function(H) {
23019 /**
23020 * (c) 2010-2016 Torstein Honsi
23021 *
23022 * License: www.highcharts.com/license
23023 */
23024 'use strict';
23025 var addEvent = H.addEvent,
23026 Chart = H.Chart,
23027 createElement = H.createElement,
23028 css = H.css,
23029 defaultOptions = H.defaultOptions,
23030 defaultPlotOptions = H.defaultPlotOptions,
23031 each = H.each,
23032 extend = H.extend,
23033 fireEvent = H.fireEvent,
23034 hasTouch = H.hasTouch,
23035 inArray = H.inArray,
23036 isObject = H.isObject,
23037 Legend = H.Legend,
23038 merge = H.merge,
23039 pick = H.pick,
23040 Point = H.Point,
23041 Series = H.Series,
23042 seriesTypes = H.seriesTypes,
23043 svg = H.svg,
23044 TrackerMixin;
23045
23046 /**
23047 * TrackerMixin for points and graphs.
23048 *
23049 * @mixin
23050 */
23051 TrackerMixin = H.TrackerMixin = {
23052
23053 /**
23054 * Draw the tracker for a point.
23055 */
23056 drawTrackerPoint: function() {
23057 var series = this,
23058 chart = series.chart,
23059 pointer = chart.pointer,
23060 onMouseOver = function(e) {
23061 var target = e.target,
23062 point;
23063
23064 while (target && !point) {
23065 point = target.point;
23066 target = target.parentNode;
23067 }
23068
23069 if (point !== undefined && point !== chart.hoverPoint) { // undefined on graph in scatterchart
23070 point.onMouseOver(e);
23071 }
23072 };
23073
23074 // Add reference to the point
23075 each(series.points, function(point) {
23076 if (point.graphic) {
23077 point.graphic.element.point = point;
23078 }
23079 if (point.dataLabel) {
23080 if (point.dataLabel.div) {
23081 point.dataLabel.div.point = point;
23082 } else {
23083 point.dataLabel.element.point = point;
23084 }
23085 }
23086 });
23087
23088 // Add the event listeners, we need to do this only once
23089 if (!series._hasTracking) {
23090 each(series.trackerGroups, function(key) {
23091 if (series[key]) { // we don't always have dataLabelsGroup
23092 series[key]
23093 .addClass('highcharts-tracker')
23094 .on('mouseover', onMouseOver)
23095 .on('mouseout', function(e) {
23096 pointer.onTrackerMouseOut(e);
23097 });
23098 if (hasTouch) {
23099 series[key].on('touchstart', onMouseOver);
23100 }
23101
23102
23103 if (series.options.cursor) {
23104 series[key]
23105 .css(css)
23106 .css({
23107 cursor: series.options.cursor
23108 });
23109 }
23110
23111 }
23112 });
23113 series._hasTracking = true;
23114 }
23115 },
23116
23117 /**
23118 * Draw the tracker object that sits above all data labels and markers to
23119 * track mouse events on the graph or points. For the line type charts
23120 * the tracker uses the same graphPath, but with a greater stroke width
23121 * for better control.
23122 */
23123 drawTrackerGraph: function() {
23124 var series = this,
23125 options = series.options,
23126 trackByArea = options.trackByArea,
23127 trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath),
23128 trackerPathLength = trackerPath.length,
23129 chart = series.chart,
23130 pointer = chart.pointer,
23131 renderer = chart.renderer,
23132 snap = chart.options.tooltip.snap,
23133 tracker = series.tracker,
23134 i,
23135 onMouseOver = function() {
23136 if (chart.hoverSeries !== series) {
23137 series.onMouseOver();
23138 }
23139 },
23140 /*
23141 * Empirical lowest possible opacities for TRACKER_FILL for an element to stay invisible but clickable
23142 * IE6: 0.002
23143 * IE7: 0.002
23144 * IE8: 0.002
23145 * IE9: 0.00000000001 (unlimited)
23146 * IE10: 0.0001 (exporting only)
23147 * FF: 0.00000000001 (unlimited)
23148 * Chrome: 0.000001
23149 * Safari: 0.000001
23150 * Opera: 0.00000000001 (unlimited)
23151 */
23152 TRACKER_FILL = 'rgba(192,192,192,' + (svg ? 0.0001 : 0.002) + ')';
23153
23154 // Extend end points. A better way would be to use round linecaps,
23155 // but those are not clickable in VML.
23156 if (trackerPathLength && !trackByArea) {
23157 i = trackerPathLength + 1;
23158 while (i--) {
23159 if (trackerPath[i] === 'M') { // extend left side
23160 trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], 'L');
23161 }
23162 if ((i && trackerPath[i] === 'M') || i === trackerPathLength) { // extend right side
23163 trackerPath.splice(i, 0, 'L', trackerPath[i - 2] + snap, trackerPath[i - 1]);
23164 }
23165 }
23166 }
23167
23168 // handle single points
23169 /*for (i = 0; i < singlePoints.length; i++) {
23170 singlePoint = singlePoints[i];
23171 trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY,
23172 L, singlePoint.plotX + snap, singlePoint.plotY);
23173 }*/
23174
23175 // draw the tracker
23176 if (tracker) {
23177 tracker.attr({
23178 d: trackerPath
23179 });
23180 } else if (series.graph) { // create
23181
23182 series.tracker = renderer.path(trackerPath)
23183 .attr({
23184 'stroke-linejoin': 'round', // #1225
23185 visibility: series.visible ? 'visible' : 'hidden',
23186 stroke: TRACKER_FILL,
23187 fill: trackByArea ? TRACKER_FILL : 'none',
23188 'stroke-width': series.graph.strokeWidth() + (trackByArea ? 0 : 2 * snap),
23189 zIndex: 2
23190 })
23191 .add(series.group);
23192
23193 // The tracker is added to the series group, which is clipped, but is covered
23194 // by the marker group. So the marker group also needs to capture events.
23195 each([series.tracker, series.markerGroup], function(tracker) {
23196 tracker.addClass('highcharts-tracker')
23197 .on('mouseover', onMouseOver)
23198 .on('mouseout', function(e) {
23199 pointer.onTrackerMouseOut(e);
23200 });
23201
23202
23203 if (options.cursor) {
23204 tracker.css({
23205 cursor: options.cursor
23206 });
23207 }
23208
23209
23210 if (hasTouch) {
23211 tracker.on('touchstart', onMouseOver);
23212 }
23213 });
23214 }
23215 }
23216 };
23217 /* End TrackerMixin */
23218
23219
23220 /**
23221 * Add tracking event listener to the series group, so the point graphics
23222 * themselves act as trackers
23223 */
23224
23225 if (seriesTypes.column) {
23226 seriesTypes.column.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
23227 }
23228
23229 if (seriesTypes.pie) {
23230 seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
23231 }
23232
23233 if (seriesTypes.scatter) {
23234 seriesTypes.scatter.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
23235 }
23236
23237 /*
23238 * Extend Legend for item events
23239 */
23240 extend(Legend.prototype, {
23241
23242 setItemEvents: function(item, legendItem, useHTML) {
23243 var legend = this,
23244 chart = legend.chart,
23245 activeClass = 'highcharts-legend-' + (item.series ? 'point' : 'series') + '-active';
23246
23247 // Set the events on the item group, or in case of useHTML, the item itself (#1249)
23248 (useHTML ? legendItem : item.legendGroup).on('mouseover', function() {
23249 item.setState('hover');
23250
23251 // A CSS class to dim or hide other than the hovered series
23252 chart.seriesGroup.addClass(activeClass);
23253
23254
23255 legendItem.css(legend.options.itemHoverStyle);
23256
23257 })
23258 .on('mouseout', function() {
23259
23260 legendItem.css(item.visible ? legend.itemStyle : legend.itemHiddenStyle);
23261
23262
23263 // A CSS class to dim or hide other than the hovered series
23264 chart.seriesGroup.removeClass(activeClass);
23265
23266 item.setState();
23267 })
23268 .on('click', function(event) {
23269 var strLegendItemClick = 'legendItemClick',
23270 fnLegendItemClick = function() {
23271 if (item.setVisible) {
23272 item.setVisible();
23273 }
23274 };
23275
23276 // Pass over the click/touch event. #4.
23277 event = {
23278 browserEvent: event
23279 };
23280
23281 // click the name or symbol
23282 if (item.firePointEvent) { // point
23283 item.firePointEvent(strLegendItemClick, event, fnLegendItemClick);
23284 } else {
23285 fireEvent(item, strLegendItemClick, event, fnLegendItemClick);
23286 }
23287 });
23288 },
23289
23290 createCheckboxForItem: function(item) {
23291 var legend = this;
23292
23293 item.checkbox = createElement('input', {
23294 type: 'checkbox',
23295 checked: item.selected,
23296 defaultChecked: item.selected // required by IE7
23297 }, legend.options.itemCheckboxStyle, legend.chart.container);
23298
23299 addEvent(item.checkbox, 'click', function(event) {
23300 var target = event.target;
23301 fireEvent(
23302 item.series || item,
23303 'checkboxClick', { // #3712
23304 checked: target.checked,
23305 item: item
23306 },
23307 function() {
23308 item.select();
23309 }
23310 );
23311 });
23312 }
23313 });
23314
23315
23316
23317 // Add pointer cursor to legend itemstyle in defaultOptions
23318 defaultOptions.legend.itemStyle.cursor = 'pointer';
23319
23320
23321
23322 /*
23323 * Extend the Chart object with interaction
23324 */
23325
23326 extend(Chart.prototype, /** @lends Chart.prototype */ {
23327 /**
23328 * Display the zoom button
23329 */
23330 showResetZoom: function() {
23331 var chart = this,
23332 lang = defaultOptions.lang,
23333 btnOptions = chart.options.chart.resetZoomButton,
23334 theme = btnOptions.theme,
23335 states = theme.states,
23336 alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox';
23337
23338 function zoomOut() {
23339 chart.zoomOut();
23340 }
23341
23342 this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, zoomOut, theme, states && states.hover)
23343 .attr({
23344 align: btnOptions.position.align,
23345 title: lang.resetZoomTitle
23346 })
23347 .addClass('highcharts-reset-zoom')
23348 .add()
23349 .align(btnOptions.position, false, alignTo);
23350
23351 },
23352
23353 /**
23354 * Zoom out to 1:1
23355 */
23356 zoomOut: function() {
23357 var chart = this;
23358 fireEvent(chart, 'selection', {
23359 resetSelection: true
23360 }, function() {
23361 chart.zoom();
23362 });
23363 },
23364
23365 /**
23366 * Zoom into a given portion of the chart given by axis coordinates
23367 * @param {Object} event
23368 */
23369 zoom: function(event) {
23370 var chart = this,
23371 hasZoomed,
23372 pointer = chart.pointer,
23373 displayButton = false,
23374 resetZoomButton;
23375
23376 // If zoom is called with no arguments, reset the axes
23377 if (!event || event.resetSelection) {
23378 each(chart.axes, function(axis) {
23379 hasZoomed = axis.zoom();
23380 });
23381 } else { // else, zoom in on all axes
23382 each(event.xAxis.concat(event.yAxis), function(axisData) {
23383 var axis = axisData.axis,
23384 isXAxis = axis.isXAxis;
23385
23386 // don't zoom more than minRange
23387 if (pointer[isXAxis ? 'zoomX' : 'zoomY']) {
23388 hasZoomed = axis.zoom(axisData.min, axisData.max);
23389 if (axis.displayBtn) {
23390 displayButton = true;
23391 }
23392 }
23393 });
23394 }
23395
23396 // Show or hide the Reset zoom button
23397 resetZoomButton = chart.resetZoomButton;
23398 if (displayButton && !resetZoomButton) {
23399 chart.showResetZoom();
23400 } else if (!displayButton && isObject(resetZoomButton)) {
23401 chart.resetZoomButton = resetZoomButton.destroy();
23402 }
23403
23404
23405 // Redraw
23406 if (hasZoomed) {
23407 chart.redraw(
23408 pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation
23409 );
23410 }
23411 },
23412
23413 /**
23414 * Pan the chart by dragging the mouse across the pane. This function is called
23415 * on mouse move, and the distance to pan is computed from chartX compared to
23416 * the first chartX position in the dragging operation.
23417 */
23418 pan: function(e, panning) {
23419
23420 var chart = this,
23421 hoverPoints = chart.hoverPoints,
23422 doRedraw;
23423
23424 // remove active points for shared tooltip
23425 if (hoverPoints) {
23426 each(hoverPoints, function(point) {
23427 point.setState();
23428 });
23429 }
23430
23431 each(panning === 'xy' ? [1, 0] : [1], function(isX) { // xy is used in maps
23432 var axis = chart[isX ? 'xAxis' : 'yAxis'][0],
23433 horiz = axis.horiz,
23434 reversed = axis.reversed,
23435 mousePos = e[horiz ? 'chartX' : 'chartY'],
23436 mouseDown = horiz ? 'mouseDownX' : 'mouseDownY',
23437 startPos = chart[mouseDown],
23438 halfPointRange = (axis.pointRange || 0) / (reversed ? -2 : 2),
23439 extremes = axis.getExtremes(),
23440 newMin = axis.toValue(startPos - mousePos, true) + halfPointRange,
23441 newMax = axis.toValue(startPos + axis.len - mousePos, true) - halfPointRange,
23442 goingLeft = startPos > mousePos, // #3613
23443 tmp;
23444
23445 // Swap min/max for reversed axes (#5997)
23446 if (reversed) {
23447 goingLeft = !goingLeft;
23448 tmp = newMin;
23449 newMin = newMax;
23450 newMax = tmp;
23451 }
23452
23453 if (axis.series.length &&
23454 (goingLeft || newMin > Math.min(extremes.dataMin, extremes.min)) &&
23455 (!goingLeft || newMax < Math.max(extremes.dataMax, extremes.max))) {
23456 axis.setExtremes(newMin, newMax, false, false, {
23457 trigger: 'pan'
23458 });
23459 doRedraw = true;
23460 }
23461
23462 chart[mouseDown] = mousePos; // set new reference for next run
23463 });
23464
23465 if (doRedraw) {
23466 chart.redraw(false);
23467 }
23468 css(chart.container, {
23469 cursor: 'move'
23470 });
23471 }
23472 });
23473
23474 /*
23475 * Extend the Point object with interaction
23476 */
23477 extend(Point.prototype, /** @lends Point.prototype */ {
23478 /**
23479 * Toggle the selection status of a point
23480 * @param {Boolean} selected Whether to select or unselect the point.
23481 * @param {Boolean} accumulate Whether to add to the previous selection. By default,
23482 * this happens if the control key (Cmd on Mac) was pressed during clicking.
23483 */
23484 select: function(selected, accumulate) {
23485 var point = this,
23486 series = point.series,
23487 chart = series.chart;
23488
23489 selected = pick(selected, !point.selected);
23490
23491 // fire the event with the default handler
23492 point.firePointEvent(selected ? 'select' : 'unselect', {
23493 accumulate: accumulate
23494 }, function() {
23495 point.selected = point.options.selected = selected;
23496 series.options.data[inArray(point, series.data)] = point.options;
23497
23498 point.setState(selected && 'select');
23499
23500 // unselect all other points unless Ctrl or Cmd + click
23501 if (!accumulate) {
23502 each(chart.getSelectedPoints(), function(loopPoint) {
23503 if (loopPoint.selected && loopPoint !== point) {
23504 loopPoint.selected = loopPoint.options.selected = false;
23505 series.options.data[inArray(loopPoint, series.data)] = loopPoint.options;
23506 loopPoint.setState('');
23507 loopPoint.firePointEvent('unselect');
23508 }
23509 });
23510 }
23511 });
23512 },
23513
23514 /**
23515 * Runs on mouse over the point
23516 *
23517 * @param {Object} e The event arguments
23518 * @param {Boolean} byProximity Falsy for kd points that are closest to the mouse, or to
23519 * actually hovered points. True for other points in shared tooltip.
23520 */
23521 onMouseOver: function(e, byProximity) {
23522 var point = this,
23523 series = point.series,
23524 chart = series.chart,
23525 tooltip = chart.tooltip,
23526 hoverPoint = chart.hoverPoint;
23527
23528 if (point.series) { // It may have been destroyed, #4130
23529 // In shared tooltip, call mouse over when point/series is actually hovered: (#5766)
23530 if (!byProximity) {
23531 // set normal state to previous series
23532 if (hoverPoint && hoverPoint !== point) {
23533 hoverPoint.onMouseOut();
23534 }
23535 if (chart.hoverSeries !== series) {
23536 series.onMouseOver();
23537 }
23538 chart.hoverPoint = point;
23539 }
23540
23541 // update the tooltip
23542 if (tooltip && (!tooltip.shared || series.noSharedTooltip)) {
23543 // hover point only for non shared points: (#5766)
23544 point.setState('hover');
23545 tooltip.refresh(point, e);
23546 } else if (!tooltip) {
23547 point.setState('hover');
23548 }
23549
23550 // trigger the event
23551 point.firePointEvent('mouseOver');
23552 }
23553 },
23554
23555 /**
23556 * Runs on mouse out from the point
23557 */
23558 onMouseOut: function() {
23559 var chart = this.series.chart,
23560 hoverPoints = chart.hoverPoints;
23561
23562 this.firePointEvent('mouseOut');
23563
23564 if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887, #2240
23565 this.setState();
23566 chart.hoverPoint = null;
23567 }
23568 },
23569
23570 /**
23571 * Import events from the series' and point's options. Only do it on
23572 * demand, to save processing time on hovering.
23573 */
23574 importEvents: function() {
23575 if (!this.hasImportedEvents) {
23576 var point = this,
23577 options = merge(point.series.options.point, point.options),
23578 events = options.events,
23579 eventType;
23580
23581 point.events = events;
23582
23583 for (eventType in events) {
23584 addEvent(point, eventType, events[eventType]);
23585 }
23586 this.hasImportedEvents = true;
23587
23588 }
23589 },
23590
23591 /**
23592 * Set the point's state
23593 * @param {String} state
23594 */
23595 setState: function(state, move) {
23596 var point = this,
23597 plotX = Math.floor(point.plotX), // #4586
23598 plotY = point.plotY,
23599 series = point.series,
23600 stateOptions = series.options.states[state] || {},
23601 markerOptions = defaultPlotOptions[series.type].marker &&
23602 series.options.marker,
23603 normalDisabled = markerOptions && markerOptions.enabled === false,
23604 markerStateOptions = (markerOptions && markerOptions.states &&
23605 markerOptions.states[state]) || {},
23606 stateDisabled = markerStateOptions.enabled === false,
23607 stateMarkerGraphic = series.stateMarkerGraphic,
23608 pointMarker = point.marker || {},
23609 chart = series.chart,
23610 halo = series.halo,
23611 haloOptions,
23612 markerAttribs,
23613 hasMarkers = markerOptions && series.markerAttribs,
23614 newSymbol;
23615
23616 state = state || ''; // empty string
23617
23618 if (
23619 // already has this state
23620 (state === point.state && !move) ||
23621 // selected points don't respond to hover
23622 (point.selected && state !== 'select') ||
23623 // series' state options is disabled
23624 (stateOptions.enabled === false) ||
23625 // general point marker's state options is disabled
23626 (state && (stateDisabled || (normalDisabled && markerStateOptions.enabled === false))) ||
23627 // individual point marker's state options is disabled
23628 (state && pointMarker.states && pointMarker.states[state] && pointMarker.states[state].enabled === false) // #1610
23629
23630 ) {
23631 return;
23632 }
23633
23634 if (hasMarkers) {
23635 markerAttribs = series.markerAttribs(point, state);
23636 }
23637
23638 // Apply hover styles to the existing point
23639 if (point.graphic) {
23640
23641 if (point.state) {
23642 point.graphic.removeClass('highcharts-point-' + point.state);
23643 }
23644 if (state) {
23645 point.graphic.addClass('highcharts-point-' + state);
23646 }
23647
23648 /*attribs = radius ? { // new symbol attributes (#507, #612)
23649 x: plotX - radius,
23650 y: plotY - radius,
23651 width: 2 * radius,
23652 height: 2 * radius
23653 } : {};*/
23654
23655
23656 //attribs = merge(series.pointAttribs(point, state), attribs);
23657 point.graphic.attr(series.pointAttribs(point, state));
23658
23659
23660 if (markerAttribs) {
23661 point.graphic.animate(
23662 markerAttribs,
23663 pick(
23664 chart.options.chart.animation, // Turn off globally
23665 markerStateOptions.animation,
23666 markerOptions.animation
23667 )
23668 );
23669 }
23670
23671 // Zooming in from a range with no markers to a range with markers
23672 if (stateMarkerGraphic) {
23673 stateMarkerGraphic.hide();
23674 }
23675 } else {
23676 // if a graphic is not applied to each point in the normal state, create a shared
23677 // graphic for the hover state
23678 if (state && markerStateOptions) {
23679 newSymbol = pointMarker.symbol || series.symbol;
23680
23681 // If the point has another symbol than the previous one, throw away the
23682 // state marker graphic and force a new one (#1459)
23683 if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) {
23684 stateMarkerGraphic = stateMarkerGraphic.destroy();
23685 }
23686
23687 // Add a new state marker graphic
23688 if (!stateMarkerGraphic) {
23689 if (newSymbol) {
23690 series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol(
23691 newSymbol,
23692 markerAttribs.x,
23693 markerAttribs.y,
23694 markerAttribs.width,
23695 markerAttribs.height
23696 )
23697 .add(series.markerGroup);
23698 stateMarkerGraphic.currentSymbol = newSymbol;
23699 }
23700
23701 // Move the existing graphic
23702 } else {
23703 stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054
23704 x: markerAttribs.x,
23705 y: markerAttribs.y
23706 });
23707 }
23708
23709 if (stateMarkerGraphic) {
23710 stateMarkerGraphic.attr(series.pointAttribs(point, state));
23711 }
23712
23713 }
23714
23715 if (stateMarkerGraphic) {
23716 stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? 'show' : 'hide'](); // #2450
23717 stateMarkerGraphic.element.point = point; // #4310
23718 }
23719 }
23720
23721 // Show me your halo
23722 haloOptions = stateOptions.halo;
23723 if (haloOptions && haloOptions.size) {
23724 if (!halo) {
23725 series.halo = halo = chart.renderer.path()
23726 // #5818, #5903
23727 .add(hasMarkers ? series.markerGroup : series.group);
23728 }
23729 halo[move ? 'animate' : 'attr']({
23730 d: point.haloPath(haloOptions.size)
23731 });
23732 halo.attr({
23733 'class': 'highcharts-halo highcharts-color-' + pick(point.colorIndex, series.colorIndex)
23734 });
23735
23736
23737 halo.attr(extend({
23738 'fill': point.color || series.color,
23739 'fill-opacity': haloOptions.opacity,
23740 'zIndex': -1 // #4929, IE8 added halo above everything
23741 }, haloOptions.attributes));
23742
23743 } else if (halo) {
23744 halo.animate({
23745 d: point.haloPath(0)
23746 }); // Hide
23747 }
23748
23749 point.state = state;
23750 },
23751
23752 /**
23753 * Get the circular path definition for the halo
23754 * @param {Number} size The radius of the circular halo.
23755 * @returns {Array} The path definition
23756 */
23757 haloPath: function(size) {
23758 var series = this.series,
23759 chart = series.chart;
23760
23761 return chart.renderer.symbols.circle(
23762 Math.floor(this.plotX) - size,
23763 this.plotY - size,
23764 size * 2,
23765 size * 2
23766 );
23767 }
23768 });
23769
23770 /*
23771 * Extend the Series object with interaction
23772 */
23773
23774 extend(Series.prototype, /** @lends Series.prototype */ {
23775 /**
23776 * Series mouse over handler
23777 */
23778 onMouseOver: function() {
23779 var series = this,
23780 chart = series.chart,
23781 hoverSeries = chart.hoverSeries;
23782
23783 // set normal state to previous series
23784 if (hoverSeries && hoverSeries !== series) {
23785 hoverSeries.onMouseOut();
23786 }
23787
23788 // trigger the event, but to save processing time,
23789 // only if defined
23790 if (series.options.events.mouseOver) {
23791 fireEvent(series, 'mouseOver');
23792 }
23793
23794 // hover this
23795 series.setState('hover');
23796 chart.hoverSeries = series;
23797 },
23798
23799 /**
23800 * Series mouse out handler
23801 */
23802 onMouseOut: function() {
23803 // trigger the event only if listeners exist
23804 var series = this,
23805 options = series.options,
23806 chart = series.chart,
23807 tooltip = chart.tooltip,
23808 hoverPoint = chart.hoverPoint;
23809
23810 chart.hoverSeries = null; // #182, set to null before the mouseOut event fires
23811
23812 // trigger mouse out on the point, which must be in this series
23813 if (hoverPoint) {
23814 hoverPoint.onMouseOut();
23815 }
23816
23817 // fire the mouse out event
23818 if (series && options.events.mouseOut) {
23819 fireEvent(series, 'mouseOut');
23820 }
23821
23822
23823 // hide the tooltip
23824 if (tooltip && !options.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) {
23825 tooltip.hide();
23826 }
23827
23828 // set normal state
23829 series.setState();
23830 },
23831
23832 /**
23833 * Set the state of the graph
23834 */
23835 setState: function(state) {
23836 var series = this,
23837 options = series.options,
23838 graph = series.graph,
23839 stateOptions = options.states,
23840 lineWidth = options.lineWidth,
23841 attribs,
23842 i = 0;
23843
23844 state = state || '';
23845
23846 if (series.state !== state) {
23847
23848 // Toggle class names
23849 each([series.group, series.markerGroup], function(group) {
23850 if (group) {
23851 // Old state
23852 if (series.state) {
23853 group.removeClass('highcharts-series-' + series.state);
23854 }
23855 // New state
23856 if (state) {
23857 group.addClass('highcharts-series-' + state);
23858 }
23859 }
23860 });
23861
23862 series.state = state;
23863
23864
23865
23866 if (stateOptions[state] && stateOptions[state].enabled === false) {
23867 return;
23868 }
23869
23870 if (state) {
23871 lineWidth = stateOptions[state].lineWidth || lineWidth + (stateOptions[state].lineWidthPlus || 0); // #4035
23872 }
23873
23874 if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML
23875 attribs = {
23876 'stroke-width': lineWidth
23877 };
23878 // use attr because animate will cause any other animation on the graph to stop
23879 graph.attr(attribs);
23880 while (series['zone-graph-' + i]) {
23881 series['zone-graph-' + i].attr(attribs);
23882 i = i + 1;
23883 }
23884 }
23885
23886 }
23887 },
23888
23889 /**
23890 * Set the visibility of the graph
23891 *
23892 * @param vis {Boolean} True to show the series, false to hide. If undefined,
23893 * the visibility is toggled.
23894 */
23895 setVisible: function(vis, redraw) {
23896 var series = this,
23897 chart = series.chart,
23898 legendItem = series.legendItem,
23899 showOrHide,
23900 ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries,
23901 oldVisibility = series.visible;
23902
23903 // if called without an argument, toggle visibility
23904 series.visible = vis = series.options.visible = series.userOptions.visible = vis === undefined ? !oldVisibility : vis; // #5618
23905 showOrHide = vis ? 'show' : 'hide';
23906
23907 // show or hide elements
23908 each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker', 'tt'], function(key) {
23909 if (series[key]) {
23910 series[key][showOrHide]();
23911 }
23912 });
23913
23914
23915 // hide tooltip (#1361)
23916 if (chart.hoverSeries === series || (chart.hoverPoint && chart.hoverPoint.series) === series) {
23917 series.onMouseOut();
23918 }
23919
23920
23921 if (legendItem) {
23922 chart.legend.colorizeItem(series, vis);
23923 }
23924
23925
23926 // rescale or adapt to resized chart
23927 series.isDirty = true;
23928 // in a stack, all other series are affected
23929 if (series.options.stacking) {
23930 each(chart.series, function(otherSeries) {
23931 if (otherSeries.options.stacking && otherSeries.visible) {
23932 otherSeries.isDirty = true;
23933 }
23934 });
23935 }
23936
23937 // show or hide linked series
23938 each(series.linkedSeries, function(otherSeries) {
23939 otherSeries.setVisible(vis, false);
23940 });
23941
23942 if (ignoreHiddenSeries) {
23943 chart.isDirtyBox = true;
23944 }
23945 if (redraw !== false) {
23946 chart.redraw();
23947 }
23948
23949 fireEvent(series, showOrHide);
23950 },
23951
23952 /**
23953 * Show the graph
23954 */
23955 show: function() {
23956 this.setVisible(true);
23957 },
23958
23959 /**
23960 * Hide the graph
23961 */
23962 hide: function() {
23963 this.setVisible(false);
23964 },
23965
23966
23967 /**
23968 * Set the selected state of the graph
23969 *
23970 * @param selected {Boolean} True to select the series, false to unselect. If
23971 * undefined, the selection state is toggled.
23972 */
23973 select: function(selected) {
23974 var series = this;
23975 // if called without an argument, toggle
23976 series.selected = selected = (selected === undefined) ? !series.selected : selected;
23977
23978 if (series.checkbox) {
23979 series.checkbox.checked = selected;
23980 }
23981
23982 fireEvent(series, selected ? 'select' : 'unselect');
23983 },
23984
23985 drawTracker: TrackerMixin.drawTrackerGraph
23986 });
23987
23988 }(Highcharts));
23989 (function(H) {
23990 /**
23991 * (c) 2010-2016 Torstein Honsi
23992 *
23993 * License: www.highcharts.com/license
23994 */
23995 'use strict';
23996 var Chart = H.Chart,
23997 each = H.each,
23998 inArray = H.inArray,
23999 isObject = H.isObject,
24000 pick = H.pick,
24001 splat = H.splat;
24002
24003 /**
24004 * Update the chart based on the current chart/document size and options for responsiveness
24005 */
24006 Chart.prototype.setResponsive = function(redraw) {
24007 var options = this.options.responsive;
24008
24009 if (options && options.rules) {
24010 each(options.rules, function(rule) {
24011 this.matchResponsiveRule(rule, redraw);
24012 }, this);
24013 }
24014 };
24015
24016 /**
24017 * Handle a single responsiveness rule
24018 */
24019 Chart.prototype.matchResponsiveRule = function(rule, redraw) {
24020 var respRules = this.respRules,
24021 condition = rule.condition,
24022 matches,
24023 fn = condition.callback || function() {
24024 return this.chartWidth <= pick(condition.maxWidth, Number.MAX_VALUE) &&
24025 this.chartHeight <= pick(condition.maxHeight, Number.MAX_VALUE) &&
24026 this.chartWidth >= pick(condition.minWidth, 0) &&
24027 this.chartHeight >= pick(condition.minHeight, 0);
24028 };
24029
24030
24031 if (rule._id === undefined) {
24032 rule._id = H.uniqueKey();
24033 }
24034 matches = fn.call(this);
24035
24036 // Apply a rule
24037 if (!respRules[rule._id] && matches) {
24038
24039 // Store the current state of the options
24040 if (rule.chartOptions) {
24041 respRules[rule._id] = this.currentOptions(rule.chartOptions);
24042 this.update(rule.chartOptions, redraw);
24043 }
24044
24045 // Unapply a rule based on the previous options before the rule
24046 // was applied
24047 } else if (respRules[rule._id] && !matches) {
24048 this.update(respRules[rule._id], redraw);
24049 delete respRules[rule._id];
24050 }
24051 };
24052
24053 /**
24054 * Get the current values for a given set of options. Used before we update
24055 * the chart with a new responsiveness rule.
24056 * TODO: Restore axis options (by id?)
24057 */
24058 Chart.prototype.currentOptions = function(options) {
24059
24060 var ret = {};
24061
24062 /**
24063 * Recurse over a set of options and its current values,
24064 * and store the current values in the ret object.
24065 */
24066 function getCurrent(options, curr, ret) {
24067 var key, i;
24068 for (key in options) {
24069 if (inArray(key, ['series', 'xAxis', 'yAxis']) > -1) {
24070 options[key] = splat(options[key]);
24071
24072 ret[key] = [];
24073 for (i = 0; i < options[key].length; i++) {
24074 ret[key][i] = {};
24075 getCurrent(options[key][i], curr[key][i], ret[key][i]);
24076 }
24077 } else if (isObject(options[key])) {
24078 ret[key] = {};
24079 getCurrent(options[key], curr[key] || {}, ret[key]);
24080 } else {
24081 ret[key] = curr[key] || null;
24082 }
24083 }
24084 }
24085
24086 getCurrent(options, this.options, ret);
24087 return ret;
24088 };
24089
24090 }(Highcharts));
24091 return Highcharts
24092}));