UNPKG

900 kBJavaScriptView Raw
1/**
2 * @license Highmaps JS v5.0.0 (2016-09-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.0',
40 deg2rad: Math.PI * 2 / 360,
41 doc: doc,
42 hasBidiBug: hasBidiBug,
43 isMS: isMS,
44 isWebKit: /AppleWebKit/.test(userAgent),
45 isFirefox: isFirefox,
46 isTouchDevice: /(Mobile|Android|Windows Phone)/.test(userAgent),
47 SVG_NS: SVG_NS,
48 idCounter: 0,
49 chartCount: 0,
50 seriesTypes: {},
51 svg: svg,
52 vml: vml,
53 win: win,
54 charts: [],
55 marginNames: ['plotTop', 'marginRight', 'marginBottom', 'plotLeft'],
56 noop: function() {
57 return undefined;
58 }
59 };
60 return Highcharts;
61 }());
62 (function(H) {
63 /**
64 * (c) 2010-2016 Torstein Honsi
65 *
66 * License: www.highcharts.com/license
67 */
68 'use strict';
69 var timers = [];
70
71 var charts = H.charts,
72 doc = H.doc,
73 win = H.win;
74
75 /**
76 * Provide error messages for debugging, with links to online explanation
77 */
78 H.error = function(code, stop) {
79 var msg = 'Highcharts error #' + code + ': www.highcharts.com/errors/' + code;
80 if (stop) {
81 throw new Error(msg);
82 }
83 // else ...
84 if (win.console) {
85 console.log(msg); // eslint-disable-line no-console
86 }
87 };
88
89 /**
90 * An animator object. One instance applies to one property (attribute or style prop)
91 * on one element.
92 *
93 * @param {object} elem The element to animate. May be a DOM element or a Highcharts SVGElement wrapper.
94 * @param {object} options Animation options, including duration, easing, step and complete.
95 * @param {object} prop The property to animate.
96 */
97 H.Fx = function(elem, options, prop) {
98 this.options = options;
99 this.elem = elem;
100 this.prop = prop;
101 };
102 H.Fx.prototype = {
103
104 /**
105 * Animating a path definition on SVGElement
106 * @returns {undefined}
107 */
108 dSetter: function() {
109 var start = this.paths[0],
110 end = this.paths[1],
111 ret = [],
112 now = this.now,
113 i = start.length,
114 startVal;
115
116 if (now === 1) { // land on the final path without adjustment points appended in the ends
117 ret = this.toD;
118
119 } else if (i === end.length && now < 1) {
120 while (i--) {
121 startVal = parseFloat(start[i]);
122 ret[i] =
123 isNaN(startVal) ? // a letter instruction like M or L
124 start[i] :
125 now * (parseFloat(end[i] - startVal)) + startVal;
126
127 }
128 } else { // if animation is finished or length not matching, land on right value
129 ret = end;
130 }
131 this.elem.attr('d', ret);
132 },
133
134 /**
135 * Update the element with the current animation step
136 * @returns {undefined}
137 */
138 update: function() {
139 var elem = this.elem,
140 prop = this.prop, // if destroyed, it is null
141 now = this.now,
142 step = this.options.step;
143
144 // Animation setter defined from outside
145 if (this[prop + 'Setter']) {
146 this[prop + 'Setter']();
147
148 // Other animations on SVGElement
149 } else if (elem.attr) {
150 if (elem.element) {
151 elem.attr(prop, now);
152 }
153
154 // HTML styles, raw HTML content like container size
155 } else {
156 elem.style[prop] = now + this.unit;
157 }
158
159 if (step) {
160 step.call(elem, now, this);
161 }
162
163 },
164
165 /**
166 * Run an animation
167 */
168 run: function(from, to, unit) {
169 var self = this,
170 timer = function(gotoEnd) {
171 return timer.stopped ? false : self.step(gotoEnd);
172 },
173 i;
174
175 this.startTime = +new Date();
176 this.start = from;
177 this.end = to;
178 this.unit = unit;
179 this.now = this.start;
180 this.pos = 0;
181
182 timer.elem = this.elem;
183
184 if (timer() && timers.push(timer) === 1) {
185 timer.timerId = setInterval(function() {
186
187 for (i = 0; i < timers.length; i++) {
188 if (!timers[i]()) {
189 timers.splice(i--, 1);
190 }
191 }
192
193 if (!timers.length) {
194 clearInterval(timer.timerId);
195 }
196 }, 13);
197 }
198 },
199
200 /**
201 * Run a single step in the animation
202 * @param {Boolean} gotoEnd Whether to go to then endpoint of the animation after abort
203 * @returns {Boolean} True if animation continues
204 */
205 step: function(gotoEnd) {
206 var t = +new Date(),
207 ret,
208 done,
209 options = this.options,
210 elem = this.elem,
211 complete = options.complete,
212 duration = options.duration,
213 curAnim = options.curAnim,
214 i;
215
216 if (elem.attr && !elem.element) { // #2616, element including flag is destroyed
217 ret = false;
218
219 } else if (gotoEnd || t >= duration + this.startTime) {
220 this.now = this.end;
221 this.pos = 1;
222 this.update();
223
224 curAnim[this.prop] = true;
225
226 done = true;
227 for (i in curAnim) {
228 if (curAnim[i] !== true) {
229 done = false;
230 }
231 }
232
233 if (done && complete) {
234 complete.call(elem);
235 }
236 ret = false;
237
238 } else {
239 this.pos = options.easing((t - this.startTime) / duration);
240 this.now = this.start + ((this.end - this.start) * this.pos);
241 this.update();
242 ret = true;
243 }
244 return ret;
245 },
246
247 /**
248 * Prepare start and end values so that the path can be animated one to one
249 */
250 initPath: function(elem, fromD, toD) {
251 fromD = fromD || '';
252 var shift,
253 startX = elem.startX,
254 endX = elem.endX,
255 bezier = fromD.indexOf('C') > -1,
256 numParams = bezier ? 7 : 3,
257 fullLength,
258 slice,
259 i,
260 start = fromD.split(' '),
261 end = toD.slice(), // copy
262 isArea = elem.isArea,
263 positionFactor = isArea ? 2 : 1,
264 reverse;
265
266 /**
267 * In splines make move points have six parameters like bezier curves
268 */
269 function sixify(arr) {
270 i = arr.length;
271 while (i--) {
272 if (arr[i] === 'M' || arr[i] === 'L') {
273 arr.splice(i + 1, 0, arr[i + 1], arr[i + 2], arr[i + 1], arr[i + 2]);
274 }
275 }
276 }
277
278 /**
279 * Insert an array at the given position of another array
280 */
281 function insertSlice(arr, subArr, index) {
282 [].splice.apply(
283 arr, [index, 0].concat(subArr)
284 );
285 }
286
287 /**
288 * If shifting points, prepend a dummy point to the end path.
289 */
290 function prepend(arr, other) {
291 while (arr.length < fullLength) {
292
293 // Move to, line to or curve to?
294 arr[0] = other[fullLength - arr.length];
295
296 // Prepend a copy of the first point
297 insertSlice(arr, arr.slice(0, numParams), 0);
298
299 // For areas, the bottom path goes back again to the left, so we need
300 // to append a copy of the last point.
301 if (isArea) {
302 insertSlice(arr, arr.slice(arr.length - numParams), arr.length);
303 i--;
304 }
305 }
306 arr[0] = 'M';
307 }
308
309 /**
310 * Copy and append last point until the length matches the end length
311 */
312 function append(arr, other) {
313 var i = (fullLength - arr.length) / numParams;
314 while (i > 0 && i--) {
315
316 // Pull out the slice that is going to be appended or inserted. In a line graph,
317 // the positionFactor is 1, and the last point is sliced out. In an area graph,
318 // the positionFactor is 2, causing the middle two points to be sliced out, since
319 // an area path starts at left, follows the upper path then turns and follows the
320 // bottom back.
321 slice = arr.slice().splice(
322 (arr.length / positionFactor) - numParams,
323 numParams * positionFactor
324 );
325
326 // Move to, line to or curve to?
327 slice[0] = other[fullLength - numParams - (i * numParams)];
328
329 // Disable first control point
330 if (bezier) {
331 slice[numParams - 6] = slice[numParams - 2];
332 slice[numParams - 5] = slice[numParams - 1];
333 }
334
335 // Now insert the slice, either in the middle (for areas) or at the end (for lines)
336 insertSlice(arr, slice, arr.length / positionFactor);
337
338 if (isArea) {
339 i--;
340 }
341 }
342 }
343
344 if (bezier) {
345 sixify(start);
346 sixify(end);
347 }
348
349 // For sideways animation, find out how much we need to shift to get the start path Xs
350 // to match the end path Xs.
351 if (startX && endX) {
352 for (i = 0; i < startX.length; i++) {
353 if (startX[i] === endX[0]) { // Moving left, new points coming in on right
354 shift = i;
355 break;
356 } else if (startX[0] === endX[endX.length - startX.length + i]) { // Moving right
357 shift = i;
358 reverse = true;
359 break;
360 }
361 }
362 if (shift === undefined) {
363 start = [];
364 }
365 }
366
367 if (start.length && H.isNumber(shift)) {
368
369 // The common target length for the start and end array, where both
370 // arrays are padded in opposite ends
371 fullLength = end.length + shift * positionFactor * numParams;
372
373 if (!reverse) {
374 prepend(end, start);
375 append(start, end);
376 } else {
377 prepend(start, end);
378 append(end, start);
379 }
380 }
381
382 return [start, end];
383 }
384 }; // End of Fx prototype
385
386
387 /**
388 * Extend an object with the members of another
389 * @param {Object} a The object to be extended
390 * @param {Object} b The object to add to the first one
391 */
392 H.extend = function(a, b) {
393 var n;
394 if (!a) {
395 a = {};
396 }
397 for (n in b) {
398 a[n] = b[n];
399 }
400 return a;
401 };
402
403 /**
404 * Deep merge two or more objects and return a third object. If the first argument is
405 * true, the contents of the second object is copied into the first object.
406 * Previously this function redirected to jQuery.extend(true), but this had two limitations.
407 * First, it deep merged arrays, which lead to workarounds in Highcharts. Second,
408 * it copied properties from extended prototypes.
409 */
410 H.merge = function() {
411 var i,
412 args = arguments,
413 len,
414 ret = {},
415 doCopy = function(copy, original) {
416 var value, key;
417
418 // An object is replacing a primitive
419 if (typeof copy !== 'object') {
420 copy = {};
421 }
422
423 for (key in original) {
424 if (original.hasOwnProperty(key)) {
425 value = original[key];
426
427 // Copy the contents of objects, but not arrays or DOM nodes
428 if (H.isObject(value, true) &&
429 key !== 'renderTo' && typeof value.nodeType !== 'number') {
430 copy[key] = doCopy(copy[key] || {}, value);
431
432 // Primitives and arrays are copied over directly
433 } else {
434 copy[key] = original[key];
435 }
436 }
437 }
438 return copy;
439 };
440
441 // If first argument is true, copy into the existing object. Used in setOptions.
442 if (args[0] === true) {
443 ret = args[1];
444 args = Array.prototype.slice.call(args, 2);
445 }
446
447 // For each argument, extend the return
448 len = args.length;
449 for (i = 0; i < len; i++) {
450 ret = doCopy(ret, args[i]);
451 }
452
453 return ret;
454 };
455
456 /**
457 * Shortcut for parseInt
458 * @param {Object} s
459 * @param {Number} mag Magnitude
460 */
461 H.pInt = function(s, mag) {
462 return parseInt(s, mag || 10);
463 };
464
465 /**
466 * Check for string
467 * @param {Object} s
468 */
469 H.isString = function(s) {
470 return typeof s === 'string';
471 };
472
473 /**
474 * Check for object
475 * @param {Object} obj
476 * @param {Boolean} strict Also checks that the object is not an array
477 */
478 H.isArray = function(obj) {
479 var str = Object.prototype.toString.call(obj);
480 return str === '[object Array]' || str === '[object Array Iterator]';
481 };
482
483 /**
484 * Check for array
485 * @param {Object} obj
486 */
487 H.isObject = function(obj, strict) {
488 return obj && typeof obj === 'object' && (!strict || !H.isArray(obj));
489 };
490
491 /**
492 * Check for number
493 * @param {Object} n
494 */
495 H.isNumber = function(n) {
496 return typeof n === 'number' && !isNaN(n);
497 };
498
499 /**
500 * Remove last occurence of an item from an array
501 * @param {Array} arr
502 * @param {Mixed} item
503 */
504 H.erase = function(arr, item) {
505 var i = arr.length;
506 while (i--) {
507 if (arr[i] === item) {
508 arr.splice(i, 1);
509 break;
510 }
511 }
512 //return arr;
513 };
514
515 /**
516 * Returns true if the object is not null or undefined.
517 * @param {Object} obj
518 */
519 H.defined = function(obj) {
520 return obj !== undefined && obj !== null;
521 };
522
523 /**
524 * Set or get an attribute or an object of attributes. Can't use jQuery attr because
525 * it attempts to set expando properties on the SVG element, which is not allowed.
526 *
527 * @param {Object} elem The DOM element to receive the attribute(s)
528 * @param {String|Object} prop The property or an abject of key-value pairs
529 * @param {String} value The value if a single property is set
530 */
531 H.attr = function(elem, prop, value) {
532 var key,
533 ret;
534
535 // if the prop is a string
536 if (H.isString(prop)) {
537 // set the value
538 if (H.defined(value)) {
539 elem.setAttribute(prop, value);
540
541 // get the value
542 } else if (elem && elem.getAttribute) { // elem not defined when printing pie demo...
543 ret = elem.getAttribute(prop);
544 }
545
546 // else if prop is defined, it is a hash of key/value pairs
547 } else if (H.defined(prop) && H.isObject(prop)) {
548 for (key in prop) {
549 elem.setAttribute(key, prop[key]);
550 }
551 }
552 return ret;
553 };
554 /**
555 * Check if an element is an array, and if not, make it into an array.
556 */
557 H.splat = function(obj) {
558 return H.isArray(obj) ? obj : [obj];
559 };
560
561 /**
562 * Set a timeout if the delay is given, otherwise perform the function synchronously
563 * @param {Function} fn The function to perform
564 * @param {Number} delay Delay in milliseconds
565 * @param {Ojbect} context The context
566 * @returns {Nubmer} An identifier for the timeout
567 */
568 H.syncTimeout = function(fn, delay, context) {
569 if (delay) {
570 return setTimeout(fn, delay, context);
571 }
572 fn.call(0, context);
573 };
574
575
576 /**
577 * Return the first value that is defined.
578 */
579 H.pick = function() {
580 var args = arguments,
581 i,
582 arg,
583 length = args.length;
584 for (i = 0; i < length; i++) {
585 arg = args[i];
586 if (arg !== undefined && arg !== null) {
587 return arg;
588 }
589 }
590 };
591
592 /**
593 * Set CSS on a given element
594 * @param {Object} el
595 * @param {Object} styles Style object with camel case property names
596 */
597 H.css = function(el, styles) {
598 if (H.isMS && !H.svg) { // #2686
599 if (styles && styles.opacity !== undefined) {
600 styles.filter = 'alpha(opacity=' + (styles.opacity * 100) + ')';
601 }
602 }
603 H.extend(el.style, styles);
604 };
605
606 /**
607 * Utility function to create element with attributes and styles
608 * @param {Object} tag
609 * @param {Object} attribs
610 * @param {Object} styles
611 * @param {Object} parent
612 * @param {Object} nopad
613 */
614 H.createElement = function(tag, attribs, styles, parent, nopad) {
615 var el = doc.createElement(tag),
616 css = H.css;
617 if (attribs) {
618 H.extend(el, attribs);
619 }
620 if (nopad) {
621 css(el, {
622 padding: 0,
623 border: 'none',
624 margin: 0
625 });
626 }
627 if (styles) {
628 css(el, styles);
629 }
630 if (parent) {
631 parent.appendChild(el);
632 }
633 return el;
634 };
635
636 /**
637 * Extend a prototyped class by new members
638 * @param {Object} parent
639 * @param {Object} members
640 */
641 H.extendClass = function(Parent, members) {
642 var object = function() {};
643 object.prototype = new Parent();
644 H.extend(object.prototype, members);
645 return object;
646 };
647
648 /**
649 * Pad a string to a given length by adding 0 to the beginning
650 * @param {Number} number
651 * @param {Number} length
652 */
653 H.pad = function(number, length, padder) {
654 return new Array((length || 2) + 1 - String(number).length).join(padder || 0) + number;
655 };
656
657 /**
658 * Return a length based on either the integer value, or a percentage of a base.
659 */
660 H.relativeLength = function(value, base) {
661 return (/%$/).test(value) ? base * parseFloat(value) / 100 : parseFloat(value);
662 };
663
664 /**
665 * Wrap a method with extended functionality, preserving the original function
666 * @param {Object} obj The context object that the method belongs to
667 * @param {String} method The name of the method to extend
668 * @param {Function} func A wrapper function callback. This function is called with the same arguments
669 * as the original function, except that the original function is unshifted and passed as the first
670 * argument.
671 */
672 H.wrap = function(obj, method, func) {
673 var proceed = obj[method];
674 obj[method] = function() {
675 var args = Array.prototype.slice.call(arguments);
676 args.unshift(proceed);
677 return func.apply(this, args);
678 };
679 };
680
681
682 H.getTZOffset = function(timestamp) {
683 var d = H.Date;
684 return ((d.hcGetTimezoneOffset && d.hcGetTimezoneOffset(timestamp)) || d.hcTimezoneOffset || 0) * 60000;
685 };
686
687 /**
688 * Based on http://www.php.net/manual/en/function.strftime.php
689 * @param {String} format
690 * @param {Number} timestamp
691 * @param {Boolean} capitalize
692 */
693 H.dateFormat = function(format, timestamp, capitalize) {
694 if (!H.defined(timestamp) || isNaN(timestamp)) {
695 return H.defaultOptions.lang.invalidDate || '';
696 }
697 format = H.pick(format, '%Y-%m-%d %H:%M:%S');
698
699 var D = H.Date,
700 date = new D(timestamp - H.getTZOffset(timestamp)),
701 key, // used in for constuct below
702 // get the basic time values
703 hours = date[D.hcGetHours](),
704 day = date[D.hcGetDay](),
705 dayOfMonth = date[D.hcGetDate](),
706 month = date[D.hcGetMonth](),
707 fullYear = date[D.hcGetFullYear](),
708 lang = H.defaultOptions.lang,
709 langWeekdays = lang.weekdays,
710 shortWeekdays = lang.shortWeekdays,
711 pad = H.pad,
712
713 // List all format keys. Custom formats can be added from the outside.
714 replacements = H.extend({
715
716 // Day
717 'a': shortWeekdays ? shortWeekdays[day] : langWeekdays[day].substr(0, 3), // Short weekday, like 'Mon'
718 'A': langWeekdays[day], // Long weekday, like 'Monday'
719 'd': pad(dayOfMonth), // Two digit day of the month, 01 to 31
720 'e': pad(dayOfMonth, 2, ' '), // Day of the month, 1 through 31
721 'w': day,
722
723 // Week (none implemented)
724 //'W': weekNumber(),
725
726 // Month
727 'b': lang.shortMonths[month], // Short month, like 'Jan'
728 'B': lang.months[month], // Long month, like 'January'
729 'm': pad(month + 1), // Two digit month number, 01 through 12
730
731 // Year
732 'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009
733 'Y': fullYear, // Four digits year, like 2009
734
735 // Time
736 'H': pad(hours), // Two digits hours in 24h format, 00 through 23
737 'k': hours, // Hours in 24h format, 0 through 23
738 'I': pad((hours % 12) || 12), // Two digits hours in 12h format, 00 through 11
739 'l': (hours % 12) || 12, // Hours in 12h format, 1 through 12
740 'M': pad(date[D.hcGetMinutes]()), // Two digits minutes, 00 through 59
741 'p': hours < 12 ? 'AM' : 'PM', // Upper case AM or PM
742 'P': hours < 12 ? 'am' : 'pm', // Lower case AM or PM
743 'S': pad(date.getSeconds()), // Two digits seconds, 00 through 59
744 'L': pad(Math.round(timestamp % 1000), 3) // Milliseconds (naming from Ruby)
745 }, H.dateFormats);
746
747
748 // do the replaces
749 for (key in replacements) {
750 while (format.indexOf('%' + key) !== -1) { // regex would do it in one line, but this is faster
751 format = format.replace('%' + key, typeof replacements[key] === 'function' ? replacements[key](timestamp) : replacements[key]);
752 }
753 }
754
755 // Optionally capitalize the string and return
756 return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format;
757 };
758
759 /**
760 * Format a single variable. Similar to sprintf, without the % prefix.
761 */
762 H.formatSingle = function(format, val) {
763 var floatRegex = /f$/,
764 decRegex = /\.([0-9])/,
765 lang = H.defaultOptions.lang,
766 decimals;
767
768 if (floatRegex.test(format)) { // float
769 decimals = format.match(decRegex);
770 decimals = decimals ? decimals[1] : -1;
771 if (val !== null) {
772 val = H.numberFormat(
773 val,
774 decimals,
775 lang.decimalPoint,
776 format.indexOf(',') > -1 ? lang.thousandsSep : ''
777 );
778 }
779 } else {
780 val = H.dateFormat(format, val);
781 }
782 return val;
783 };
784
785 /**
786 * Format a string according to a subset of the rules of Python's String.format method.
787 */
788 H.format = function(str, ctx) {
789 var splitter = '{',
790 isInside = false,
791 segment,
792 valueAndFormat,
793 path,
794 i,
795 len,
796 ret = [],
797 val,
798 index;
799
800 while (str) {
801 index = str.indexOf(splitter);
802 if (index === -1) {
803 break;
804 }
805
806 segment = str.slice(0, index);
807 if (isInside) { // we're on the closing bracket looking back
808
809 valueAndFormat = segment.split(':');
810 path = valueAndFormat.shift().split('.'); // get first and leave format
811 len = path.length;
812 val = ctx;
813
814 // Assign deeper paths
815 for (i = 0; i < len; i++) {
816 val = val[path[i]];
817 }
818
819 // Format the replacement
820 if (valueAndFormat.length) {
821 val = H.formatSingle(valueAndFormat.join(':'), val);
822 }
823
824 // Push the result and advance the cursor
825 ret.push(val);
826
827 } else {
828 ret.push(segment);
829
830 }
831 str = str.slice(index + 1); // the rest
832 isInside = !isInside; // toggle
833 splitter = isInside ? '}' : '{'; // now look for next matching bracket
834 }
835 ret.push(str);
836 return ret.join('');
837 };
838
839 /**
840 * Get the magnitude of a number
841 */
842 H.getMagnitude = function(num) {
843 return Math.pow(10, Math.floor(Math.log(num) / Math.LN10));
844 };
845
846 /**
847 * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5
848 * @param {Number} interval
849 * @param {Array} multiples
850 * @param {Number} magnitude
851 * @param {Object} options
852 */
853 H.normalizeTickInterval = function(interval, multiples, magnitude, allowDecimals, preventExceed) {
854 var normalized,
855 i,
856 retInterval = interval;
857
858 // round to a tenfold of 1, 2, 2.5 or 5
859 magnitude = H.pick(magnitude, 1);
860 normalized = interval / magnitude;
861
862 // multiples for a linear scale
863 if (!multiples) {
864 multiples = [1, 2, 2.5, 5, 10];
865
866 // the allowDecimals option
867 if (allowDecimals === false) {
868 if (magnitude === 1) {
869 multiples = [1, 2, 5, 10];
870 } else if (magnitude <= 0.1) {
871 multiples = [1 / magnitude];
872 }
873 }
874 }
875
876 // normalize the interval to the nearest multiple
877 for (i = 0; i < multiples.length; i++) {
878 retInterval = multiples[i];
879 if ((preventExceed && retInterval * magnitude >= interval) || // only allow tick amounts smaller than natural
880 (!preventExceed && (normalized <= (multiples[i] + (multiples[i + 1] || multiples[i])) / 2))) {
881 break;
882 }
883 }
884
885 // multiply back to the correct magnitude
886 retInterval *= magnitude;
887
888 return retInterval;
889 };
890
891
892 /**
893 * Utility method that sorts an object array and keeping the order of equal items.
894 * ECMA script standard does not specify the behaviour when items are equal.
895 */
896 H.stableSort = function(arr, sortFunction) {
897 var length = arr.length,
898 sortValue,
899 i;
900
901 // Add index to each item
902 for (i = 0; i < length; i++) {
903 arr[i].safeI = i; // stable sort index
904 }
905
906 arr.sort(function(a, b) {
907 sortValue = sortFunction(a, b);
908 return sortValue === 0 ? a.safeI - b.safeI : sortValue;
909 });
910
911 // Remove index from items
912 for (i = 0; i < length; i++) {
913 delete arr[i].safeI; // stable sort index
914 }
915 };
916
917 /**
918 * Non-recursive method to find the lowest member of an array. Math.min raises a maximum
919 * call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This
920 * method is slightly slower, but safe.
921 */
922 H.arrayMin = function(data) {
923 var i = data.length,
924 min = data[0];
925
926 while (i--) {
927 if (data[i] < min) {
928 min = data[i];
929 }
930 }
931 return min;
932 };
933
934 /**
935 * Non-recursive method to find the lowest member of an array. Math.min raises a maximum
936 * call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This
937 * method is slightly slower, but safe.
938 */
939 H.arrayMax = function(data) {
940 var i = data.length,
941 max = data[0];
942
943 while (i--) {
944 if (data[i] > max) {
945 max = data[i];
946 }
947 }
948 return max;
949 };
950
951 /**
952 * Utility method that destroys any SVGElement or VMLElement that are properties on the given object.
953 * It loops all properties and invokes destroy if there is a destroy method. The property is
954 * then delete'ed.
955 * @param {Object} The object to destroy properties on
956 * @param {Object} Exception, do not destroy this property, only delete it.
957 */
958 H.destroyObjectProperties = function(obj, except) {
959 var n;
960 for (n in obj) {
961 // If the object is non-null and destroy is defined
962 if (obj[n] && obj[n] !== except && obj[n].destroy) {
963 // Invoke the destroy
964 obj[n].destroy();
965 }
966
967 // Delete the property from the object.
968 delete obj[n];
969 }
970 };
971
972
973 /**
974 * Discard an element by moving it to the bin and delete
975 * @param {Object} The HTML node to discard
976 */
977 H.discardElement = function(element) {
978 var garbageBin = H.garbageBin;
979 // create a garbage bin element, not part of the DOM
980 if (!garbageBin) {
981 garbageBin = H.createElement('div');
982 }
983
984 // move the node and empty bin
985 if (element) {
986 garbageBin.appendChild(element);
987 }
988 garbageBin.innerHTML = '';
989 };
990
991 /**
992 * Fix JS round off float errors
993 * @param {Number} num
994 */
995 H.correctFloat = function(num, prec) {
996 return parseFloat(
997 num.toPrecision(prec || 14)
998 );
999 };
1000
1001 /**
1002 * Set the global animation to either a given value, or fall back to the
1003 * given chart's animation option
1004 * @param {Object} animation
1005 * @param {Object} chart
1006 */
1007 H.setAnimation = function(animation, chart) {
1008 chart.renderer.globalAnimation = H.pick(animation, chart.options.chart.animation, true);
1009 };
1010
1011 /**
1012 * Get the animation in object form, where a disabled animation is always
1013 * returned with duration: 0
1014 */
1015 H.animObject = function(animation) {
1016 return H.isObject(animation) ? H.merge(animation) : {
1017 duration: animation ? 500 : 0
1018 };
1019 };
1020
1021 /**
1022 * The time unit lookup
1023 */
1024 H.timeUnits = {
1025 millisecond: 1,
1026 second: 1000,
1027 minute: 60000,
1028 hour: 3600000,
1029 day: 24 * 3600000,
1030 week: 7 * 24 * 3600000,
1031 month: 28 * 24 * 3600000,
1032 year: 364 * 24 * 3600000
1033 };
1034
1035 /**
1036 * Format a number and return a string based on input settings
1037 * @param {Number} number The input number to format
1038 * @param {Number} decimals The amount of decimals
1039 * @param {String} decimalPoint The decimal point, defaults to the one given in the lang options
1040 * @param {String} thousandsSep The thousands separator, defaults to the one given in the lang options
1041 */
1042 H.numberFormat = function(number, decimals, decimalPoint, thousandsSep) {
1043
1044 number = +number || 0;
1045 decimals = +decimals;
1046
1047 var lang = H.defaultOptions.lang,
1048 origDec = (number.toString().split('.')[1] || '').length,
1049 decimalComponent,
1050 strinteger,
1051 thousands,
1052 absNumber = Math.abs(number),
1053 ret;
1054
1055 if (decimals === -1) {
1056 decimals = Math.min(origDec, 20); // Preserve decimals. Not huge numbers (#3793).
1057 } else if (!H.isNumber(decimals)) {
1058 decimals = 2;
1059 }
1060
1061 // A string containing the positive integer component of the number
1062 strinteger = String(H.pInt(absNumber.toFixed(decimals)));
1063
1064 // Leftover after grouping into thousands. Can be 0, 1 or 3.
1065 thousands = strinteger.length > 3 ? strinteger.length % 3 : 0;
1066
1067 // Language
1068 decimalPoint = H.pick(decimalPoint, lang.decimalPoint);
1069 thousandsSep = H.pick(thousandsSep, lang.thousandsSep);
1070
1071 // Start building the return
1072 ret = number < 0 ? '-' : '';
1073
1074 // Add the leftover after grouping into thousands. For example, in the number 42 000 000,
1075 // this line adds 42.
1076 ret += thousands ? strinteger.substr(0, thousands) + thousandsSep : '';
1077
1078 // Add the remaining thousands groups, joined by the thousands separator
1079 ret += strinteger.substr(thousands).replace(/(\d{3})(?=\d)/g, '$1' + thousandsSep);
1080
1081 // Add the decimal point and the decimal component
1082 if (decimals) {
1083 // Get the decimal component, and add power to avoid rounding errors with float numbers (#4573)
1084 decimalComponent = Math.abs(absNumber - strinteger + Math.pow(10, -Math.max(decimals, origDec) - 1));
1085 ret += decimalPoint + decimalComponent.toFixed(decimals).slice(2);
1086 }
1087
1088 return ret;
1089 };
1090
1091 /**
1092 * Easing definition
1093 * @param {Number} pos Current position, ranging from 0 to 1
1094 */
1095 Math.easeInOutSine = function(pos) {
1096 return -0.5 * (Math.cos(Math.PI * pos) - 1);
1097 };
1098
1099 /**
1100 * Internal method to return CSS value for given element and property
1101 */
1102 H.getStyle = function(el, prop) {
1103
1104 var style;
1105
1106 // For width and height, return the actual inner pixel size (#4913)
1107 if (prop === 'width') {
1108 return Math.min(el.offsetWidth, el.scrollWidth) - H.getStyle(el, 'padding-left') - H.getStyle(el, 'padding-right');
1109 } else if (prop === 'height') {
1110 return Math.min(el.offsetHeight, el.scrollHeight) - H.getStyle(el, 'padding-top') - H.getStyle(el, 'padding-bottom');
1111 }
1112
1113 // Otherwise, get the computed style
1114 style = win.getComputedStyle(el, undefined);
1115 return style && H.pInt(style.getPropertyValue(prop));
1116 };
1117
1118 /**
1119 * Return the index of an item in an array, or -1 if not found
1120 */
1121 H.inArray = function(item, arr) {
1122 return arr.indexOf ? arr.indexOf(item) : [].indexOf.call(arr, item);
1123 };
1124
1125 /**
1126 * Filter an array
1127 */
1128 H.grep = function(elements, callback) {
1129 return [].filter.call(elements, callback);
1130 };
1131
1132 /**
1133 * Map an array
1134 */
1135 H.map = function(arr, fn) {
1136 var results = [],
1137 i = 0,
1138 len = arr.length;
1139
1140 for (; i < len; i++) {
1141 results[i] = fn.call(arr[i], arr[i], i, arr);
1142 }
1143
1144 return results;
1145 };
1146
1147 /**
1148 * Get the element's offset position, corrected by overflow:auto.
1149 */
1150 H.offset = function(el) {
1151 var docElem = doc.documentElement,
1152 box = el.getBoundingClientRect();
1153
1154 return {
1155 top: box.top + (win.pageYOffset || docElem.scrollTop) - (docElem.clientTop || 0),
1156 left: box.left + (win.pageXOffset || docElem.scrollLeft) - (docElem.clientLeft || 0)
1157 };
1158 };
1159
1160 /**
1161 * Stop running animation.
1162 * A possible extension to this would be to stop a single property, when
1163 * we want to continue animating others. Then assign the prop to the timer
1164 * in the Fx.run method, and check for the prop here. This would be an improvement
1165 * in all cases where we stop the animation from .attr. Instead of stopping
1166 * everything, we can just stop the actual attributes we're setting.
1167 */
1168 H.stop = function(el) {
1169
1170 var i = timers.length;
1171
1172 // Remove timers related to this element (#4519)
1173 while (i--) {
1174 if (timers[i].elem === el) {
1175 timers[i].stopped = true; // #4667
1176 }
1177 }
1178 };
1179
1180 /**
1181 * Utility for iterating over an array.
1182 * @param {Array} arr
1183 * @param {Function} fn
1184 */
1185 H.each = function(arr, fn, ctx) { // modern browsers
1186 return Array.prototype.forEach.call(arr, fn, ctx);
1187 };
1188
1189 /**
1190 * Add an event listener
1191 */
1192 H.addEvent = function(el, type, fn) {
1193
1194 var events = el.hcEvents = el.hcEvents || {};
1195
1196 function wrappedFn(e) {
1197 e.target = e.srcElement || win; // #2820
1198 fn.call(el, e);
1199 }
1200
1201 // Handle DOM events in modern browsers
1202 if (el.addEventListener) {
1203 el.addEventListener(type, fn, false);
1204
1205 // Handle old IE implementation
1206 } else if (el.attachEvent) {
1207
1208 if (!el.hcEventsIE) {
1209 el.hcEventsIE = {};
1210 }
1211
1212 // Link wrapped fn with original fn, so we can get this in removeEvent
1213 el.hcEventsIE[fn.toString()] = wrappedFn;
1214
1215 el.attachEvent('on' + type, wrappedFn);
1216 }
1217
1218 if (!events[type]) {
1219 events[type] = [];
1220 }
1221
1222 events[type].push(fn);
1223 };
1224
1225 /**
1226 * Remove event added with addEvent
1227 */
1228 H.removeEvent = function(el, type, fn) {
1229
1230 var events,
1231 hcEvents = el.hcEvents,
1232 index;
1233
1234 function removeOneEvent(type, fn) {
1235 if (el.removeEventListener) {
1236 el.removeEventListener(type, fn, false);
1237 } else if (el.attachEvent) {
1238 fn = el.hcEventsIE[fn.toString()];
1239 el.detachEvent('on' + type, fn);
1240 }
1241 }
1242
1243 function removeAllEvents() {
1244 var types,
1245 len,
1246 n;
1247
1248 if (!el.nodeName) {
1249 return; // break on non-DOM events
1250 }
1251
1252 if (type) {
1253 types = {};
1254 types[type] = true;
1255 } else {
1256 types = hcEvents;
1257 }
1258
1259 for (n in types) {
1260 if (hcEvents[n]) {
1261 len = hcEvents[n].length;
1262 while (len--) {
1263 removeOneEvent(n, hcEvents[n][len]);
1264 }
1265 }
1266 }
1267 }
1268
1269 if (hcEvents) {
1270 if (type) {
1271 events = hcEvents[type] || [];
1272 if (fn) {
1273 index = H.inArray(fn, events);
1274 if (index > -1) {
1275 events.splice(index, 1);
1276 hcEvents[type] = events;
1277 }
1278 removeOneEvent(type, fn);
1279
1280 } else {
1281 removeAllEvents();
1282 hcEvents[type] = [];
1283 }
1284 } else {
1285 removeAllEvents();
1286 el.hcEvents = {};
1287 }
1288 }
1289 };
1290
1291 /**
1292 * Fire an event on a custom object
1293 */
1294 H.fireEvent = function(el, type, eventArguments, defaultFunction) {
1295 var e,
1296 hcEvents = el.hcEvents,
1297 events,
1298 len,
1299 i,
1300 fn;
1301
1302 eventArguments = eventArguments || {};
1303
1304 if (doc.createEvent && (el.dispatchEvent || el.fireEvent)) {
1305 e = doc.createEvent('Events');
1306 e.initEvent(type, true, true);
1307 //e.target = el;
1308
1309 H.extend(e, eventArguments);
1310
1311 if (el.dispatchEvent) {
1312 el.dispatchEvent(e);
1313 } else {
1314 el.fireEvent(type, e);
1315 }
1316
1317 } else if (hcEvents) {
1318
1319 events = hcEvents[type] || [];
1320 len = events.length;
1321
1322 if (!eventArguments.target) { // We're running a custom event
1323
1324 H.extend(eventArguments, {
1325 // Attach a simple preventDefault function to skip default handler if called.
1326 // The built-in defaultPrevented property is not overwritable (#5112)
1327 preventDefault: function() {
1328 eventArguments.defaultPrevented = true;
1329 },
1330 // Setting target to native events fails with clicking the zoom-out button in Chrome.
1331 target: el,
1332 // If the type is not set, we're running a custom event (#2297). If it is set,
1333 // we're running a browser event, and setting it will cause en error in
1334 // IE8 (#2465).
1335 type: type
1336 });
1337 }
1338
1339
1340 for (i = 0; i < len; i++) {
1341 fn = events[i];
1342
1343 // If the event handler return false, prevent the default handler from executing
1344 if (fn && fn.call(el, eventArguments) === false) {
1345 eventArguments.preventDefault();
1346 }
1347 }
1348 }
1349
1350 // Run the default if not prevented
1351 if (defaultFunction && !eventArguments.defaultPrevented) {
1352 defaultFunction(eventArguments);
1353 }
1354 };
1355
1356 /**
1357 * The global animate method, which uses Fx to create individual animators.
1358 */
1359 H.animate = function(el, params, opt) {
1360 var start,
1361 unit = '',
1362 end,
1363 fx,
1364 args,
1365 prop;
1366
1367 if (!H.isObject(opt)) { // Number or undefined/null
1368 args = arguments;
1369 opt = {
1370 duration: args[2],
1371 easing: args[3],
1372 complete: args[4]
1373 };
1374 }
1375 if (!H.isNumber(opt.duration)) {
1376 opt.duration = 400;
1377 }
1378 opt.easing = typeof opt.easing === 'function' ? opt.easing : (Math[opt.easing] || Math.easeInOutSine);
1379 opt.curAnim = H.merge(params);
1380
1381 for (prop in params) {
1382 fx = new H.Fx(el, opt, prop);
1383 end = null;
1384
1385 if (prop === 'd') {
1386 fx.paths = fx.initPath(
1387 el,
1388 el.d,
1389 params.d
1390 );
1391 fx.toD = params.d;
1392 start = 0;
1393 end = 1;
1394 } else if (el.attr) {
1395 start = el.attr(prop);
1396 } else {
1397 start = parseFloat(H.getStyle(el, prop)) || 0;
1398 if (prop !== 'opacity') {
1399 unit = 'px';
1400 }
1401 }
1402
1403 if (!end) {
1404 end = params[prop];
1405 }
1406 if (end.match && end.match('px')) {
1407 end = end.replace(/px/g, ''); // #4351
1408 }
1409 fx.run(start, end, unit);
1410 }
1411 };
1412
1413 /**
1414 * The series type factory.
1415 *
1416 * @param {string} type The series type name.
1417 * @param {string} parent The parent series type name.
1418 * @param {object} options The additional default options that is merged with the parent's options.
1419 * @param {object} props The properties (functions and primitives) to set on the new prototype.
1420 * @param {object} pointProps Members for a series-specific Point prototype if needed.
1421 */
1422 H.seriesType = function(type, parent, options, props, pointProps) { // docs: add to API + extending Highcharts
1423 var defaultOptions = H.getOptions(),
1424 seriesTypes = H.seriesTypes;
1425
1426 // Merge the options
1427 defaultOptions.plotOptions[type] = H.merge(
1428 defaultOptions.plotOptions[parent],
1429 options
1430 );
1431
1432 // Create the class
1433 seriesTypes[type] = H.extendClass(seriesTypes[parent] || function() {}, props);
1434 seriesTypes[type].prototype.type = type;
1435
1436 // Create the point class if needed
1437 if (pointProps) {
1438 seriesTypes[type].prototype.pointClass = H.extendClass(H.Point, pointProps);
1439 }
1440
1441 return seriesTypes[type];
1442 };
1443
1444 /**
1445 * Register Highcharts as a plugin in jQuery
1446 */
1447 if (win.jQuery) {
1448 win.jQuery.fn.highcharts = function() {
1449 var args = [].slice.call(arguments);
1450
1451 if (this[0]) { // this[0] is the renderTo div
1452
1453 // Create the chart
1454 if (args[0]) {
1455 new H[ // eslint-disable-line no-new
1456 H.isString(args[0]) ? args.shift() : 'Chart' // Constructor defaults to Chart
1457 ](this[0], args[0], args[1]);
1458 return this;
1459 }
1460
1461 // When called without parameters or with the return argument, return an existing chart
1462 return charts[H.attr(this[0], 'data-highcharts-chart')];
1463 }
1464 };
1465 }
1466
1467
1468 /**
1469 * Compatibility section to add support for legacy IE. This can be removed if old IE
1470 * support is not needed.
1471 */
1472 if (doc && !doc.defaultView) {
1473 H.getStyle = function(el, prop) {
1474 var val,
1475 alias = {
1476 width: 'clientWidth',
1477 height: 'clientHeight'
1478 }[prop];
1479
1480 if (el.style[prop]) {
1481 return H.pInt(el.style[prop]);
1482 }
1483 if (prop === 'opacity') {
1484 prop = 'filter';
1485 }
1486
1487 // Getting the rendered width and height
1488 if (alias) {
1489 el.style.zoom = 1;
1490 return Math.max(el[alias] - 2 * H.getStyle(el, 'padding'), 0);
1491 }
1492
1493 val = el.currentStyle[prop.replace(/\-(\w)/g, function(a, b) {
1494 return b.toUpperCase();
1495 })];
1496 if (prop === 'filter') {
1497 val = val.replace(
1498 /alpha\(opacity=([0-9]+)\)/,
1499 function(a, b) {
1500 return b / 100;
1501 }
1502 );
1503 }
1504
1505 return val === '' ? 1 : H.pInt(val);
1506 };
1507 }
1508
1509 if (!Array.prototype.forEach) {
1510 H.each = function(arr, fn, ctx) { // legacy
1511 var i = 0,
1512 len = arr.length;
1513 for (; i < len; i++) {
1514 if (fn.call(ctx, arr[i], i, arr) === false) {
1515 return i;
1516 }
1517 }
1518 };
1519 }
1520
1521 if (!Array.prototype.indexOf) {
1522 H.inArray = function(item, arr) {
1523 var len,
1524 i = 0;
1525
1526 if (arr) {
1527 len = arr.length;
1528
1529 for (; i < len; i++) {
1530 if (arr[i] === item) {
1531 return i;
1532 }
1533 }
1534 }
1535
1536 return -1;
1537 };
1538 }
1539
1540 if (!Array.prototype.filter) {
1541 H.grep = function(elements, fn) {
1542 var ret = [],
1543 i = 0,
1544 length = elements.length;
1545
1546 for (; i < length; i++) {
1547 if (fn(elements[i], i)) {
1548 ret.push(elements[i]);
1549 }
1550 }
1551
1552 return ret;
1553 };
1554 }
1555
1556 //--- End compatibility section ---
1557
1558 }(Highcharts));
1559 (function(H) {
1560 /**
1561 * (c) 2010-2016 Torstein Honsi
1562 *
1563 * License: www.highcharts.com/license
1564 */
1565 'use strict';
1566 var each = H.each,
1567 isNumber = H.isNumber,
1568 map = H.map,
1569 merge = H.merge,
1570 pInt = H.pInt;
1571 /**
1572 * Handle color operations. The object methods are chainable.
1573 * @param {String} input The input color in either rbga or hex format
1574 */
1575 H.Color = function(input) {
1576 // Backwards compatibility, allow instanciation without new
1577 if (!(this instanceof H.Color)) {
1578 return new H.Color(input);
1579 }
1580 // Initialize
1581 this.init(input);
1582 };
1583 H.Color.prototype = {
1584
1585 // Collection of parsers. This can be extended from the outside by pushing parsers
1586 // to Highcharts.Color.prototype.parsers.
1587 parsers: [{
1588 // RGBA color
1589 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*\)/,
1590 parse: function(result) {
1591 return [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)];
1592 }
1593 }, {
1594 // HEX color
1595 regex: /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,
1596 parse: function(result) {
1597 return [pInt(result[1], 16), pInt(result[2], 16), pInt(result[3], 16), 1];
1598 }
1599 }, {
1600 // RGB color
1601 regex: /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/,
1602 parse: function(result) {
1603 return [pInt(result[1]), pInt(result[2]), pInt(result[3]), 1];
1604 }
1605 }],
1606
1607 // Collection of named colors. Can be extended from the outside by adding colors
1608 // to Highcharts.Color.prototype.names.
1609 names: {
1610 white: '#ffffff',
1611 black: '#000000'
1612 },
1613
1614 /**
1615 * Parse the input color to rgba array
1616 * @param {String} input
1617 */
1618 init: function(input) {
1619 var result,
1620 rgba,
1621 i,
1622 parser;
1623
1624 this.input = input = this.names[input] || input;
1625
1626 // Gradients
1627 if (input && input.stops) {
1628 this.stops = map(input.stops, function(stop) {
1629 return new H.Color(stop[1]);
1630 });
1631
1632 // Solid colors
1633 } else {
1634 i = this.parsers.length;
1635 while (i-- && !rgba) {
1636 parser = this.parsers[i];
1637 result = parser.regex.exec(input);
1638 if (result) {
1639 rgba = parser.parse(result);
1640 }
1641 }
1642 }
1643 this.rgba = rgba || [];
1644 },
1645
1646 /**
1647 * Return the color a specified format
1648 * @param {String} format
1649 */
1650 get: function(format) {
1651 var input = this.input,
1652 rgba = this.rgba,
1653 ret;
1654
1655 if (this.stops) {
1656 ret = merge(input);
1657 ret.stops = [].concat(ret.stops);
1658 each(this.stops, function(stop, i) {
1659 ret.stops[i] = [ret.stops[i][0], stop.get(format)];
1660 });
1661
1662 // it's NaN if gradient colors on a column chart
1663 } else if (rgba && isNumber(rgba[0])) {
1664 if (format === 'rgb' || (!format && rgba[3] === 1)) {
1665 ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')';
1666 } else if (format === 'a') {
1667 ret = rgba[3];
1668 } else {
1669 ret = 'rgba(' + rgba.join(',') + ')';
1670 }
1671 } else {
1672 ret = input;
1673 }
1674 return ret;
1675 },
1676
1677 /**
1678 * Brighten the color
1679 * @param {Number} alpha
1680 */
1681 brighten: function(alpha) {
1682 var i,
1683 rgba = this.rgba;
1684
1685 if (this.stops) {
1686 each(this.stops, function(stop) {
1687 stop.brighten(alpha);
1688 });
1689
1690 } else if (isNumber(alpha) && alpha !== 0) {
1691 for (i = 0; i < 3; i++) {
1692 rgba[i] += pInt(alpha * 255);
1693
1694 if (rgba[i] < 0) {
1695 rgba[i] = 0;
1696 }
1697 if (rgba[i] > 255) {
1698 rgba[i] = 255;
1699 }
1700 }
1701 }
1702 return this;
1703 },
1704
1705 /**
1706 * Set the color's opacity to a given alpha value
1707 * @param {Number} alpha
1708 */
1709 setOpacity: function(alpha) {
1710 this.rgba[3] = alpha;
1711 return this;
1712 }
1713 };
1714 H.color = function(input) {
1715 return new H.Color(input);
1716 };
1717
1718 }(Highcharts));
1719 (function(H) {
1720 /**
1721 * (c) 2010-2016 Torstein Honsi
1722 *
1723 * License: www.highcharts.com/license
1724 */
1725 'use strict';
1726 var color = H.color,
1727 each = H.each,
1728 getTZOffset = H.getTZOffset,
1729 isTouchDevice = H.isTouchDevice,
1730 merge = H.merge,
1731 pick = H.pick,
1732 svg = H.svg,
1733 win = H.win;
1734
1735 /* ****************************************************************************
1736 * Handle the options *
1737 *****************************************************************************/
1738 H.defaultOptions = {
1739
1740 colors: '#7cb5ec #434348 #90ed7d #f7a35c #8085e9 #f15c80 #e4d354 #2b908f #f45b5b #91e8e1'.split(' '),
1741
1742 symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'],
1743 lang: {
1744 loading: 'Loading...',
1745 months: ['January', 'February', 'March', 'April', 'May', 'June', 'July',
1746 'August', 'September', 'October', 'November', 'December'
1747 ],
1748 shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
1749 weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
1750 // invalidDate: '',
1751 decimalPoint: '.',
1752 numericSymbols: ['k', 'M', 'G', 'T', 'P', 'E'], // SI prefixes used in axis labels
1753 resetZoom: 'Reset zoom',
1754 resetZoomTitle: 'Reset zoom level 1:1',
1755 thousandsSep: ' '
1756 },
1757 global: {
1758 useUTC: true,
1759 //timezoneOffset: 0,
1760
1761 VMLRadialGradientURL: 'http://code.highcharts.com@product.cdnpath@/5.0.0/gfx/vml-radial-gradient.png'
1762
1763 },
1764 chart: {
1765 //animation: true,
1766 //alignTicks: false,
1767 //reflow: true,
1768 //className: null,
1769 //events: { load, selection },
1770 //margin: [null],
1771 //marginTop: null,
1772 //marginRight: null,
1773 //marginBottom: null,
1774 //marginLeft: null,
1775 borderRadius: 0,
1776
1777 defaultSeriesType: 'line',
1778 ignoreHiddenSeries: true,
1779 //inverted: false,
1780 spacing: [10, 10, 15, 10],
1781 //spacingTop: 10,
1782 //spacingRight: 10,
1783 //spacingBottom: 15,
1784 //spacingLeft: 10,
1785 //zoomType: ''
1786 resetZoomButton: {
1787 theme: {
1788 zIndex: 20
1789 },
1790 position: {
1791 align: 'right',
1792 x: -10,
1793 //verticalAlign: 'top',
1794 y: 10
1795 }
1796 // relativeTo: 'plot'
1797 },
1798 width: null,
1799 height: null,
1800
1801
1802 borderColor: '#335cad',
1803 //borderWidth: 0,
1804 //style: {
1805 // fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font
1806 // fontSize: '12px'
1807 //},
1808 backgroundColor: '#ffffff',
1809 //plotBackgroundColor: null,
1810 plotBorderColor: '#cccccc'
1811 //plotBorderWidth: 0,
1812 //plotShadow: false
1813
1814 },
1815
1816 title: {
1817 text: 'Chart title',
1818 align: 'center',
1819 // floating: false,
1820 margin: 15,
1821 // x: 0,
1822 // verticalAlign: 'top',
1823 // y: null,
1824
1825 style: {
1826 color: '#333333',
1827 fontSize: '18px'
1828 },
1829
1830 widthAdjust: -44
1831
1832 },
1833 subtitle: {
1834 text: '',
1835 align: 'center',
1836 // floating: false
1837 // x: 0,
1838 // verticalAlign: 'top',
1839 // y: null,
1840
1841 style: {
1842 color: '#666666'
1843 },
1844
1845 widthAdjust: -44
1846 },
1847
1848 plotOptions: {},
1849 labels: {
1850 //items: [],
1851 style: {
1852 //font: defaultFont,
1853 position: 'absolute',
1854 color: '#333333'
1855 }
1856 },
1857 legend: {
1858 enabled: true,
1859 align: 'center',
1860 //floating: false,
1861 layout: 'horizontal',
1862 labelFormatter: function() {
1863 return this.name;
1864 },
1865 //borderWidth: 0,
1866 borderColor: '#999999',
1867 borderRadius: 0,
1868 navigation: {
1869
1870 activeColor: '#003399',
1871 inactiveColor: '#cccccc'
1872
1873 // animation: true,
1874 // arrowSize: 12
1875 // style: {} // text styles
1876 },
1877 // margin: 20,
1878 // reversed: false,
1879 // backgroundColor: null,
1880 /*style: {
1881 padding: '5px'
1882 },*/
1883
1884 itemStyle: {
1885 color: '#333333',
1886 fontSize: '12px',
1887 fontWeight: 'bold'
1888 },
1889 itemHoverStyle: {
1890 //cursor: 'pointer', removed as of #601
1891 color: '#000000'
1892 },
1893 itemHiddenStyle: {
1894 color: '#cccccc'
1895 },
1896 shadow: false,
1897
1898 itemCheckboxStyle: {
1899 position: 'absolute',
1900 width: '13px', // for IE precision
1901 height: '13px'
1902 },
1903 // itemWidth: undefined,
1904 squareSymbol: true,
1905 // symbolRadius: 0,
1906 // symbolWidth: 16,
1907 symbolPadding: 5,
1908 verticalAlign: 'bottom',
1909 // width: undefined,
1910 x: 0,
1911 y: 0,
1912 title: {
1913 //text: null,
1914
1915 style: {
1916 fontWeight: 'bold'
1917 }
1918
1919 }
1920 },
1921
1922 loading: {
1923 // hideDuration: 100,
1924 // showDuration: 0,
1925
1926 labelStyle: {
1927 fontWeight: 'bold',
1928 position: 'relative',
1929 top: '45%'
1930 },
1931 style: {
1932 position: 'absolute',
1933 backgroundColor: '#ffffff',
1934 opacity: 0.5,
1935 textAlign: 'center'
1936 }
1937
1938 },
1939
1940 tooltip: {
1941 enabled: true,
1942 animation: svg,
1943 //crosshairs: null,
1944 borderRadius: 3,
1945 dateTimeLabelFormats: {
1946 millisecond: '%A, %b %e, %H:%M:%S.%L',
1947 second: '%A, %b %e, %H:%M:%S',
1948 minute: '%A, %b %e, %H:%M',
1949 hour: '%A, %b %e, %H:%M',
1950 day: '%A, %b %e, %Y',
1951 week: 'Week from %A, %b %e, %Y',
1952 month: '%B %Y',
1953 year: '%Y'
1954 },
1955 footerFormat: '',
1956 //formatter: defaultFormatter,
1957 /* todo: em font-size when finished comparing against HC4
1958 headerFormat: '<span style="font-size: 0.85em">{point.key}</span><br/>',
1959 */
1960 padding: 8,
1961
1962 //shape: 'callout',
1963 //shared: false,
1964 snap: isTouchDevice ? 25 : 10,
1965
1966 backgroundColor: color('#f7f7f7').setOpacity(0.85).get(),
1967 borderWidth: 1,
1968 headerFormat: '<span style="font-size: 10px">{point.key}</span><br/>',
1969 pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b><br/>',
1970 shadow: true,
1971 style: {
1972 color: '#333333',
1973 cursor: 'default',
1974 fontSize: '12px',
1975 pointerEvents: 'none', // #1686 http://caniuse.com/#feat=pointer-events
1976 whiteSpace: 'nowrap'
1977 }
1978
1979 //xDateFormat: '%A, %b %e, %Y',
1980 //valueDecimals: null,
1981 //valuePrefix: '',
1982 //valueSuffix: ''
1983 },
1984
1985 credits: {
1986 enabled: true,
1987 href: 'http://www.highcharts.com',
1988 position: {
1989 align: 'right',
1990 x: -10,
1991 verticalAlign: 'bottom',
1992 y: -5
1993 },
1994
1995 style: {
1996 cursor: 'pointer',
1997 color: '#999999',
1998 fontSize: '9px'
1999 },
2000
2001 text: 'Highcharts.com'
2002 }
2003 };
2004
2005
2006
2007 /**
2008 * Set the time methods globally based on the useUTC option. Time method can be either
2009 * local time or UTC (default).
2010 */
2011 function setTimeMethods() {
2012 var globalOptions = H.defaultOptions.global,
2013 Date,
2014 useUTC = globalOptions.useUTC,
2015 GET = useUTC ? 'getUTC' : 'get',
2016 SET = useUTC ? 'setUTC' : 'set';
2017
2018 H.Date = Date = globalOptions.Date || win.Date; // Allow using a different Date class
2019 Date.hcTimezoneOffset = useUTC && globalOptions.timezoneOffset;
2020 Date.hcGetTimezoneOffset = useUTC && globalOptions.getTimezoneOffset;
2021 Date.hcMakeTime = function(year, month, date, hours, minutes, seconds) {
2022 var d;
2023 if (useUTC) {
2024 d = Date.UTC.apply(0, arguments);
2025 d += getTZOffset(d);
2026 } else {
2027 d = new Date(
2028 year,
2029 month,
2030 pick(date, 1),
2031 pick(hours, 0),
2032 pick(minutes, 0),
2033 pick(seconds, 0)
2034 ).getTime();
2035 }
2036 return d;
2037 };
2038 each(['Minutes', 'Hours', 'Day', 'Date', 'Month', 'FullYear'], function(s) {
2039 Date['hcGet' + s] = GET + s;
2040 });
2041 each(['Milliseconds', 'Seconds', 'Minutes', 'Hours', 'Date', 'Month', 'FullYear'], function(s) {
2042 Date['hcSet' + s] = SET + s;
2043 });
2044 }
2045
2046 /**
2047 * Merge the default options with custom options and return the new options structure
2048 * @param {Object} options The new custom options
2049 */
2050 H.setOptions = function(options) {
2051
2052 // Copy in the default options
2053 H.defaultOptions = merge(true, H.defaultOptions, options);
2054
2055 // Apply UTC
2056 setTimeMethods();
2057
2058 return H.defaultOptions;
2059 };
2060
2061 /**
2062 * Get the updated default options. Until 3.0.7, merely exposing defaultOptions for outside modules
2063 * wasn't enough because the setOptions method created a new object.
2064 */
2065 H.getOptions = function() {
2066 return H.defaultOptions;
2067 };
2068
2069
2070 // Series defaults
2071 H.defaultPlotOptions = H.defaultOptions.plotOptions;
2072
2073 // set the default time methods
2074 setTimeMethods();
2075
2076 }(Highcharts));
2077 (function(H) {
2078 /**
2079 * (c) 2010-2016 Torstein Honsi
2080 *
2081 * License: www.highcharts.com/license
2082 */
2083 'use strict';
2084 var SVGElement,
2085 SVGRenderer,
2086
2087 addEvent = H.addEvent,
2088 animate = H.animate,
2089 attr = H.attr,
2090 charts = H.charts,
2091 color = H.color,
2092 css = H.css,
2093 createElement = H.createElement,
2094 defined = H.defined,
2095 deg2rad = H.deg2rad,
2096 destroyObjectProperties = H.destroyObjectProperties,
2097 doc = H.doc,
2098 each = H.each,
2099 extend = H.extend,
2100 erase = H.erase,
2101 grep = H.grep,
2102 hasTouch = H.hasTouch,
2103 isArray = H.isArray,
2104 isFirefox = H.isFirefox,
2105 isMS = H.isMS,
2106 isObject = H.isObject,
2107 isString = H.isString,
2108 isWebKit = H.isWebKit,
2109 merge = H.merge,
2110 noop = H.noop,
2111 pick = H.pick,
2112 pInt = H.pInt,
2113 removeEvent = H.removeEvent,
2114 splat = H.splat,
2115 stop = H.stop,
2116 svg = H.svg,
2117 SVG_NS = H.SVG_NS,
2118 win = H.win;
2119
2120 /**
2121 * A wrapper object for SVG elements
2122 */
2123 SVGElement = H.SVGElement = function() {
2124 return this;
2125 };
2126 SVGElement.prototype = {
2127
2128 // Default base for animation
2129 opacity: 1,
2130 SVG_NS: SVG_NS,
2131 // For labels, these CSS properties are applied to the <text> node directly
2132 textProps: ['direction', 'fontSize', 'fontWeight', 'fontFamily', 'fontStyle', 'color',
2133 'lineHeight', 'width', 'textDecoration', 'textOverflow', 'textShadow'
2134 ],
2135
2136 /**
2137 * Initialize the SVG renderer
2138 * @param {Object} renderer
2139 * @param {String} nodeName
2140 */
2141 init: function(renderer, nodeName) {
2142 var wrapper = this;
2143 wrapper.element = nodeName === 'span' ?
2144 createElement(nodeName) :
2145 doc.createElementNS(wrapper.SVG_NS, nodeName);
2146 wrapper.renderer = renderer;
2147 },
2148
2149 /**
2150 * Animate a given attribute
2151 * @param {Object} params
2152 * @param {Number} options Options include duration, easing, step and complete
2153 * @param {Function} complete Function to perform at the end of animation
2154 */
2155 animate: function(params, options, complete) {
2156 var animOptions = pick(options, this.renderer.globalAnimation, true);
2157 stop(this); // stop regardless of animation actually running, or reverting to .attr (#607)
2158 if (animOptions) {
2159 if (complete) { // allows using a callback with the global animation without overwriting it
2160 animOptions.complete = complete;
2161 }
2162 animate(this, params, animOptions);
2163 } else {
2164 this.attr(params, null, complete);
2165 }
2166 return this;
2167 },
2168
2169 /**
2170 * Build an SVG gradient out of a common JavaScript configuration object
2171 */
2172 colorGradient: function(color, prop, elem) {
2173 var renderer = this.renderer,
2174 colorObject,
2175 gradName,
2176 gradAttr,
2177 radAttr,
2178 gradients,
2179 gradientObject,
2180 stops,
2181 stopColor,
2182 stopOpacity,
2183 radialReference,
2184 n,
2185 id,
2186 key = [],
2187 value;
2188
2189 // Apply linear or radial gradients
2190 if (color.linearGradient) {
2191 gradName = 'linearGradient';
2192 } else if (color.radialGradient) {
2193 gradName = 'radialGradient';
2194 }
2195
2196 if (gradName) {
2197 gradAttr = color[gradName];
2198 gradients = renderer.gradients;
2199 stops = color.stops;
2200 radialReference = elem.radialReference;
2201
2202 // Keep < 2.2 kompatibility
2203 if (isArray(gradAttr)) {
2204 color[gradName] = gradAttr = {
2205 x1: gradAttr[0],
2206 y1: gradAttr[1],
2207 x2: gradAttr[2],
2208 y2: gradAttr[3],
2209 gradientUnits: 'userSpaceOnUse'
2210 };
2211 }
2212
2213 // Correct the radial gradient for the radial reference system
2214 if (gradName === 'radialGradient' && radialReference && !defined(gradAttr.gradientUnits)) {
2215 radAttr = gradAttr; // Save the radial attributes for updating
2216 gradAttr = merge(gradAttr,
2217 renderer.getRadialAttr(radialReference, radAttr), {
2218 gradientUnits: 'userSpaceOnUse'
2219 }
2220 );
2221 }
2222
2223 // Build the unique key to detect whether we need to create a new element (#1282)
2224 for (n in gradAttr) {
2225 if (n !== 'id') {
2226 key.push(n, gradAttr[n]);
2227 }
2228 }
2229 for (n in stops) {
2230 key.push(stops[n]);
2231 }
2232 key = key.join(',');
2233
2234 // Check if a gradient object with the same config object is created within this renderer
2235 if (gradients[key]) {
2236 id = gradients[key].attr('id');
2237
2238 } else {
2239
2240 // Set the id and create the element
2241 gradAttr.id = id = 'highcharts-' + H.idCounter++;
2242 gradients[key] = gradientObject = renderer.createElement(gradName)
2243 .attr(gradAttr)
2244 .add(renderer.defs);
2245
2246 gradientObject.radAttr = radAttr;
2247
2248 // The gradient needs to keep a list of stops to be able to destroy them
2249 gradientObject.stops = [];
2250 each(stops, function(stop) {
2251 var stopObject;
2252 if (stop[1].indexOf('rgba') === 0) {
2253 colorObject = H.color(stop[1]);
2254 stopColor = colorObject.get('rgb');
2255 stopOpacity = colorObject.get('a');
2256 } else {
2257 stopColor = stop[1];
2258 stopOpacity = 1;
2259 }
2260 stopObject = renderer.createElement('stop').attr({
2261 offset: stop[0],
2262 'stop-color': stopColor,
2263 'stop-opacity': stopOpacity
2264 }).add(gradientObject);
2265
2266 // Add the stop element to the gradient
2267 gradientObject.stops.push(stopObject);
2268 });
2269 }
2270
2271 // Set the reference to the gradient object
2272 value = 'url(' + renderer.url + '#' + id + ')';
2273 elem.setAttribute(prop, value);
2274 elem.gradient = key;
2275
2276 // Allow the color to be concatenated into tooltips formatters etc. (#2995)
2277 color.toString = function() {
2278 return value;
2279 };
2280 }
2281 },
2282
2283 /**
2284 * Apply a polyfill to the text-stroke CSS property, by copying the text element
2285 * and apply strokes to the copy.
2286 *
2287 * Contrast checks at http://jsfiddle.net/highcharts/43soe9m1/2/
2288 */
2289 applyTextShadow: function(textShadow) {
2290 var elem = this.element,
2291 tspans,
2292 hasContrast = textShadow.indexOf('contrast') !== -1,
2293 styles = {},
2294 forExport = this.renderer.forExport,
2295 // IE10 and IE11 report textShadow in elem.style even though it doesn't work. Check
2296 // this again with new IE release. In exports, the rendering is passed to PhantomJS.
2297 supports = this.renderer.forExport || (elem.style.textShadow !== undefined && !isMS);
2298
2299 // When the text shadow is set to contrast, use dark stroke for light text and vice versa
2300 if (hasContrast) {
2301 styles.textShadow = textShadow = textShadow.replace(/contrast/g, this.renderer.getContrast(elem.style.fill));
2302 }
2303
2304 // Safari with retina displays as well as PhantomJS bug (#3974). Firefox does not tolerate this,
2305 // it removes the text shadows.
2306 if (isWebKit || forExport) {
2307 styles.textRendering = 'geometricPrecision';
2308 }
2309
2310 /* Selective side-by-side testing in supported browser (http://jsfiddle.net/highcharts/73L1ptrh/)
2311 if (elem.textContent.indexOf('2.') === 0) {
2312 elem.style['text-shadow'] = 'none';
2313 supports = false;
2314 }
2315 // */
2316
2317 // No reason to polyfill, we've got native support
2318 if (supports) {
2319 this.css(styles); // Apply altered textShadow or textRendering workaround
2320 } else {
2321
2322 this.fakeTS = true; // Fake text shadow
2323
2324 // In order to get the right y position of the clones,
2325 // copy over the y setter
2326 this.ySetter = this.xSetter;
2327
2328 tspans = [].slice.call(elem.getElementsByTagName('tspan'));
2329 each(textShadow.split(/\s?,\s?/g), function(textShadow) {
2330 var firstChild = elem.firstChild,
2331 color,
2332 strokeWidth;
2333
2334 textShadow = textShadow.split(' ');
2335 color = textShadow[textShadow.length - 1];
2336
2337 // Approximately tune the settings to the text-shadow behaviour
2338 strokeWidth = textShadow[textShadow.length - 2];
2339
2340 if (strokeWidth) {
2341 each(tspans, function(tspan, y) {
2342 var clone;
2343
2344 // Let the first line start at the correct X position
2345 if (y === 0) {
2346 tspan.setAttribute('x', elem.getAttribute('x'));
2347 y = elem.getAttribute('y');
2348 tspan.setAttribute('y', y || 0);
2349 if (y === null) {
2350 elem.setAttribute('y', 0);
2351 }
2352 }
2353
2354 // Create the clone and apply shadow properties
2355 clone = tspan.cloneNode(1);
2356 attr(clone, {
2357 'class': 'highcharts-text-shadow',
2358 'fill': color,
2359 'stroke': color,
2360 'stroke-opacity': 1 / Math.max(pInt(strokeWidth), 3),
2361 'stroke-width': strokeWidth,
2362 'stroke-linejoin': 'round'
2363 });
2364 elem.insertBefore(clone, firstChild);
2365 });
2366 }
2367 });
2368 }
2369 },
2370
2371 /**
2372 * Set or get a given attribute
2373 * @param {Object|String} hash
2374 * @param {Mixed|Undefined} val
2375 */
2376 attr: function(hash, val, complete) {
2377 var key,
2378 value,
2379 element = this.element,
2380 hasSetSymbolSize,
2381 ret = this,
2382 skipAttr,
2383 setter;
2384
2385 // single key-value pair
2386 if (typeof hash === 'string' && val !== undefined) {
2387 key = hash;
2388 hash = {};
2389 hash[key] = val;
2390 }
2391
2392 // used as a getter: first argument is a string, second is undefined
2393 if (typeof hash === 'string') {
2394 ret = (this[hash + 'Getter'] || this._defaultGetter).call(this, hash, element);
2395
2396 // setter
2397 } else {
2398
2399 for (key in hash) {
2400 value = hash[key];
2401 skipAttr = false;
2402
2403
2404
2405 if (this.symbolName && /^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)/.test(key)) {
2406 if (!hasSetSymbolSize) {
2407 this.symbolAttr(hash);
2408 hasSetSymbolSize = true;
2409 }
2410 skipAttr = true;
2411 }
2412
2413 if (this.rotation && (key === 'x' || key === 'y')) {
2414 this.doTransform = true;
2415 }
2416
2417 if (!skipAttr) {
2418 setter = this[key + 'Setter'] || this._defaultSetter;
2419 setter.call(this, value, key, element);
2420
2421
2422 // Let the shadow follow the main element
2423 if (this.shadows && /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(key)) {
2424 this.updateShadows(key, value, setter);
2425 }
2426
2427 }
2428 }
2429
2430 // Update transform. Do this outside the loop to prevent redundant updating for batch setting
2431 // of attributes.
2432 if (this.doTransform) {
2433 this.updateTransform();
2434 this.doTransform = false;
2435 }
2436
2437 }
2438
2439 // In accordance with animate, run a complete callback
2440 if (complete) {
2441 complete();
2442 }
2443
2444 return ret;
2445 },
2446
2447
2448 /**
2449 * Update the shadow elements with new attributes
2450 * @param {String} key The attribute name
2451 * @param {String|Number} value The value of the attribute
2452 * @param {Function} setter The setter function, inherited from the parent wrapper
2453 * @returns {undefined}
2454 */
2455 updateShadows: function(key, value, setter) {
2456 var shadows = this.shadows,
2457 i = shadows.length;
2458
2459 while (i--) {
2460 setter.call(
2461 shadows[i],
2462 key === 'height' ?
2463 Math.max(value - (shadows[i].cutHeight || 0), 0) :
2464 key === 'd' ? this.d : value,
2465 key,
2466 shadows[i]
2467 );
2468 }
2469 },
2470
2471
2472 /**
2473 * Add a class name to an element
2474 */
2475 addClass: function(className, replace) {
2476 var currentClassName = this.attr('class') || '';
2477
2478 if (currentClassName.indexOf(className) === -1) {
2479 if (!replace) {
2480 className = (currentClassName + (currentClassName ? ' ' : '') + className).replace(' ', ' ');
2481 }
2482 this.attr('class', className);
2483 }
2484 return this;
2485 },
2486 hasClass: function(className) {
2487 return attr(this.element, 'class').indexOf(className) !== -1;
2488 },
2489 removeClass: function(className) {
2490 attr(this.element, 'class', (attr(this.element, 'class') || '').replace(className, ''));
2491 return this;
2492 },
2493
2494 /**
2495 * If one of the symbol size affecting parameters are changed,
2496 * check all the others only once for each call to an element's
2497 * .attr() method
2498 * @param {Object} hash
2499 */
2500 symbolAttr: function(hash) {
2501 var wrapper = this;
2502
2503 each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR', 'anchorX', 'anchorY'], function(key) {
2504 wrapper[key] = pick(hash[key], wrapper[key]);
2505 });
2506
2507 wrapper.attr({
2508 d: wrapper.renderer.symbols[wrapper.symbolName](
2509 wrapper.x,
2510 wrapper.y,
2511 wrapper.width,
2512 wrapper.height,
2513 wrapper
2514 )
2515 });
2516 },
2517
2518 /**
2519 * Apply a clipping path to this object
2520 * @param {String} id
2521 */
2522 clip: function(clipRect) {
2523 return this.attr('clip-path', clipRect ? 'url(' + this.renderer.url + '#' + clipRect.id + ')' : 'none');
2524 },
2525
2526 /**
2527 * Calculate the coordinates needed for drawing a rectangle crisply and return the
2528 * calculated attributes
2529 * @param {Number} strokeWidth
2530 * @param {Number} x
2531 * @param {Number} y
2532 * @param {Number} width
2533 * @param {Number} height
2534 */
2535 crisp: function(rect, strokeWidth) {
2536
2537 var wrapper = this,
2538 key,
2539 attribs = {},
2540 normalizer;
2541
2542 strokeWidth = strokeWidth || rect.strokeWidth || 0;
2543 normalizer = Math.round(strokeWidth) % 2 / 2; // Math.round because strokeWidth can sometimes have roundoff errors
2544
2545 // normalize for crisp edges
2546 rect.x = Math.floor(rect.x || wrapper.x || 0) + normalizer;
2547 rect.y = Math.floor(rect.y || wrapper.y || 0) + normalizer;
2548 rect.width = Math.floor((rect.width || wrapper.width || 0) - 2 * normalizer);
2549 rect.height = Math.floor((rect.height || wrapper.height || 0) - 2 * normalizer);
2550 if (defined(rect.strokeWidth)) {
2551 rect.strokeWidth = strokeWidth;
2552 }
2553
2554 for (key in rect) {
2555 if (wrapper[key] !== rect[key]) { // only set attribute if changed
2556 wrapper[key] = attribs[key] = rect[key];
2557 }
2558 }
2559
2560 return attribs;
2561 },
2562
2563 /**
2564 * Set styles for the element
2565 * @param {Object} styles
2566 */
2567 css: function(styles) {
2568 var elemWrapper = this,
2569 oldStyles = elemWrapper.styles,
2570 newStyles = {},
2571 elem = elemWrapper.element,
2572 textWidth,
2573 n,
2574 serializedCss = '',
2575 hyphenate,
2576 hasNew = !oldStyles;
2577
2578 // convert legacy
2579 if (styles && styles.color) {
2580 styles.fill = styles.color;
2581 }
2582
2583 // Filter out existing styles to increase performance (#2640)
2584 if (oldStyles) {
2585 for (n in styles) {
2586 if (styles[n] !== oldStyles[n]) {
2587 newStyles[n] = styles[n];
2588 hasNew = true;
2589 }
2590 }
2591 }
2592 if (hasNew) {
2593 textWidth = elemWrapper.textWidth =
2594 (styles && styles.width && elem.nodeName.toLowerCase() === 'text' && pInt(styles.width)) ||
2595 elemWrapper.textWidth; // #3501
2596
2597 // Merge the new styles with the old ones
2598 if (oldStyles) {
2599 styles = extend(
2600 oldStyles,
2601 newStyles
2602 );
2603 }
2604
2605 // store object
2606 elemWrapper.styles = styles;
2607
2608 if (textWidth && (!svg && elemWrapper.renderer.forExport)) {
2609 delete styles.width;
2610 }
2611
2612 // serialize and set style attribute
2613 if (isMS && !svg) {
2614 css(elemWrapper.element, styles);
2615 } else {
2616 hyphenate = function(a, b) {
2617 return '-' + b.toLowerCase();
2618 };
2619 for (n in styles) {
2620 serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + styles[n] + ';';
2621 }
2622 attr(elem, 'style', serializedCss); // #1881
2623 }
2624
2625
2626 // Rebuild text after added
2627 if (elemWrapper.added && textWidth) {
2628 elemWrapper.renderer.buildText(elemWrapper);
2629 }
2630 }
2631
2632 return elemWrapper;
2633 },
2634
2635
2636 strokeWidth: function() {
2637 return this['stroke-width'] || 0;
2638 },
2639
2640
2641 /**
2642 * Add an event listener
2643 * @param {String} eventType
2644 * @param {Function} handler
2645 */
2646 on: function(eventType, handler) {
2647 var svgElement = this,
2648 element = svgElement.element;
2649
2650 // touch
2651 if (hasTouch && eventType === 'click') {
2652 element.ontouchstart = function(e) {
2653 svgElement.touchEventFired = Date.now();
2654 e.preventDefault();
2655 handler.call(element, e);
2656 };
2657 element.onclick = function(e) {
2658 if (win.navigator.userAgent.indexOf('Android') === -1 || Date.now() - (svgElement.touchEventFired || 0) > 1100) { // #2269
2659 handler.call(element, e);
2660 }
2661 };
2662 } else {
2663 // simplest possible event model for internal use
2664 element['on' + eventType] = handler;
2665 }
2666 return this;
2667 },
2668
2669 /**
2670 * Set the coordinates needed to draw a consistent radial gradient across
2671 * pie slices regardless of positioning inside the chart. The format is
2672 * [centerX, centerY, diameter] in pixels.
2673 */
2674 setRadialReference: function(coordinates) {
2675 var existingGradient = this.renderer.gradients[this.element.gradient];
2676
2677 this.element.radialReference = coordinates;
2678
2679 // On redrawing objects with an existing gradient, the gradient needs
2680 // to be repositioned (#3801)
2681 if (existingGradient && existingGradient.radAttr) {
2682 existingGradient.animate(
2683 this.renderer.getRadialAttr(
2684 coordinates,
2685 existingGradient.radAttr
2686 )
2687 );
2688 }
2689
2690 return this;
2691 },
2692
2693 /**
2694 * Move an object and its children by x and y values
2695 * @param {Number} x
2696 * @param {Number} y
2697 */
2698 translate: function(x, y) {
2699 return this.attr({
2700 translateX: x,
2701 translateY: y
2702 });
2703 },
2704
2705 /**
2706 * Invert a group, rotate and flip
2707 */
2708 invert: function(inverted) {
2709 var wrapper = this;
2710 wrapper.inverted = inverted;
2711 wrapper.updateTransform();
2712 return wrapper;
2713 },
2714
2715 /**
2716 * Private method to update the transform attribute based on internal
2717 * properties
2718 */
2719 updateTransform: function() {
2720 var wrapper = this,
2721 translateX = wrapper.translateX || 0,
2722 translateY = wrapper.translateY || 0,
2723 scaleX = wrapper.scaleX,
2724 scaleY = wrapper.scaleY,
2725 inverted = wrapper.inverted,
2726 rotation = wrapper.rotation,
2727 element = wrapper.element,
2728 transform;
2729
2730 // flipping affects translate as adjustment for flipping around the group's axis
2731 if (inverted) {
2732 translateX += wrapper.attr('width');
2733 translateY += wrapper.attr('height');
2734 }
2735
2736 // Apply translate. Nearly all transformed elements have translation, so instead
2737 // of checking for translate = 0, do it always (#1767, #1846).
2738 transform = ['translate(' + translateX + ',' + translateY + ')'];
2739
2740 // apply rotation
2741 if (inverted) {
2742 transform.push('rotate(90) scale(-1,1)');
2743 } else if (rotation) { // text rotation
2744 transform.push('rotate(' + rotation + ' ' + (element.getAttribute('x') || 0) + ' ' + (element.getAttribute('y') || 0) + ')');
2745
2746 // Delete bBox memo when the rotation changes
2747 //delete wrapper.bBox;
2748 }
2749
2750 // apply scale
2751 if (defined(scaleX) || defined(scaleY)) {
2752 transform.push('scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')');
2753 }
2754
2755 if (transform.length) {
2756 element.setAttribute('transform', transform.join(' '));
2757 }
2758 },
2759 /**
2760 * Bring the element to the front
2761 */
2762 toFront: function() {
2763 var element = this.element;
2764 element.parentNode.appendChild(element);
2765 return this;
2766 },
2767
2768
2769 /**
2770 * Break down alignment options like align, verticalAlign, x and y
2771 * to x and y relative to the chart.
2772 *
2773 * @param {Object} alignOptions
2774 * @param {Boolean} alignByTranslate
2775 * @param {String[Object} box The box to align to, needs a width and height. When the
2776 * box is a string, it refers to an object in the Renderer. For example, when
2777 * box is 'spacingBox', it refers to Renderer.spacingBox which holds width, height
2778 * x and y properties.
2779 *
2780 */
2781 align: function(alignOptions, alignByTranslate, box) {
2782 var align,
2783 vAlign,
2784 x,
2785 y,
2786 attribs = {},
2787 alignTo,
2788 renderer = this.renderer,
2789 alignedObjects = renderer.alignedObjects,
2790 alignFactor,
2791 vAlignFactor;
2792
2793 // First call on instanciate
2794 if (alignOptions) {
2795 this.alignOptions = alignOptions;
2796 this.alignByTranslate = alignByTranslate;
2797 if (!box || isString(box)) { // boxes other than renderer handle this internally
2798 this.alignTo = alignTo = box || 'renderer';
2799 erase(alignedObjects, this); // prevent duplicates, like legendGroup after resize
2800 alignedObjects.push(this);
2801 box = null; // reassign it below
2802 }
2803
2804 // When called on resize, no arguments are supplied
2805 } else {
2806 alignOptions = this.alignOptions;
2807 alignByTranslate = this.alignByTranslate;
2808 alignTo = this.alignTo;
2809 }
2810
2811 box = pick(box, renderer[alignTo], renderer);
2812
2813 // Assign variables
2814 align = alignOptions.align;
2815 vAlign = alignOptions.verticalAlign;
2816 x = (box.x || 0) + (alignOptions.x || 0); // default: left align
2817 y = (box.y || 0) + (alignOptions.y || 0); // default: top align
2818
2819 // Align
2820 if (align === 'right') {
2821 alignFactor = 1;
2822 } else if (align === 'center') {
2823 alignFactor = 2;
2824 }
2825 if (alignFactor) {
2826 x += (box.width - (alignOptions.width || 0)) / alignFactor;
2827 }
2828 attribs[alignByTranslate ? 'translateX' : 'x'] = Math.round(x);
2829
2830
2831 // Vertical align
2832 if (vAlign === 'bottom') {
2833 vAlignFactor = 1;
2834 } else if (vAlign === 'middle') {
2835 vAlignFactor = 2;
2836 }
2837 if (vAlignFactor) {
2838 y += (box.height - (alignOptions.height || 0)) / vAlignFactor;
2839 }
2840 attribs[alignByTranslate ? 'translateY' : 'y'] = Math.round(y);
2841
2842 // Animate only if already placed
2843 this[this.placed ? 'animate' : 'attr'](attribs);
2844 this.placed = true;
2845 this.alignAttr = attribs;
2846
2847 return this;
2848 },
2849
2850 /**
2851 * Get the bounding box (width, height, x and y) for the element
2852 */
2853 getBBox: function(reload, rot) {
2854 var wrapper = this,
2855 bBox, // = wrapper.bBox,
2856 renderer = wrapper.renderer,
2857 width,
2858 height,
2859 rotation,
2860 rad,
2861 element = wrapper.element,
2862 styles = wrapper.styles,
2863 fontSize,
2864 textStr = wrapper.textStr,
2865 textShadow,
2866 elemStyle = element.style,
2867 toggleTextShadowShim,
2868 cache = renderer.cache,
2869 cacheKeys = renderer.cacheKeys,
2870 cacheKey;
2871
2872 rotation = pick(rot, wrapper.rotation);
2873 rad = rotation * deg2rad;
2874
2875
2876 fontSize = styles && styles.fontSize;
2877
2878
2879 if (textStr !== undefined) {
2880
2881 cacheKey =
2882
2883 // Since numbers are monospaced, and numerical labels appear a lot in a chart,
2884 // we assume that a label of n characters has the same bounding box as others
2885 // of the same length.
2886 textStr.toString().replace(/[0-9]/g, '0') +
2887
2888 // Properties that affect bounding box
2889 ['', rotation || 0, fontSize, element.style.width].join(',');
2890
2891 }
2892
2893 if (cacheKey && !reload) {
2894 bBox = cache[cacheKey];
2895 }
2896
2897 // No cache found
2898 if (!bBox) {
2899
2900 // SVG elements
2901 if (element.namespaceURI === wrapper.SVG_NS || renderer.forExport) {
2902 try { // Fails in Firefox if the container has display: none.
2903
2904 // When the text shadow shim is used, we need to hide the fake shadows
2905 // to get the correct bounding box (#3872)
2906 toggleTextShadowShim = this.fakeTS && function(display) {
2907 each(element.querySelectorAll('.highcharts-text-shadow'), function(tspan) {
2908 tspan.style.display = display;
2909 });
2910 };
2911
2912 // Workaround for #3842, Firefox reporting wrong bounding box for shadows
2913 if (isFirefox && elemStyle.textShadow) {
2914 textShadow = elemStyle.textShadow;
2915 elemStyle.textShadow = '';
2916 } else if (toggleTextShadowShim) {
2917 toggleTextShadowShim('none');
2918 }
2919
2920 bBox = element.getBBox ?
2921 // SVG: use extend because IE9 is not allowed to change width and height in case
2922 // of rotation (below)
2923 extend({}, element.getBBox()) :
2924 // Legacy IE in export mode
2925 {
2926 width: element.offsetWidth,
2927 height: element.offsetHeight
2928 };
2929
2930 // #3842
2931 if (textShadow) {
2932 elemStyle.textShadow = textShadow;
2933 } else if (toggleTextShadowShim) {
2934 toggleTextShadowShim('');
2935 }
2936 } catch (e) {}
2937
2938 // If the bBox is not set, the try-catch block above failed. The other condition
2939 // is for Opera that returns a width of -Infinity on hidden elements.
2940 if (!bBox || bBox.width < 0) {
2941 bBox = {
2942 width: 0,
2943 height: 0
2944 };
2945 }
2946
2947
2948 // VML Renderer or useHTML within SVG
2949 } else {
2950
2951 bBox = wrapper.htmlGetBBox();
2952
2953 }
2954
2955 // True SVG elements as well as HTML elements in modern browsers using the .useHTML option
2956 // need to compensated for rotation
2957 if (renderer.isSVG) {
2958 width = bBox.width;
2959 height = bBox.height;
2960
2961 // Workaround for wrong bounding box in IE9 and IE10 (#1101, #1505, #1669, #2568)
2962 if (isMS && styles && styles.fontSize === '11px' && height.toPrecision(3) === '16.9') {
2963 bBox.height = height = 14;
2964 }
2965
2966 // Adjust for rotated text
2967 if (rotation) {
2968 bBox.width = Math.abs(height * Math.sin(rad)) + Math.abs(width * Math.cos(rad));
2969 bBox.height = Math.abs(height * Math.cos(rad)) + Math.abs(width * Math.sin(rad));
2970 }
2971 }
2972
2973 // Cache it. When loading a chart in a hidden iframe in Firefox and IE/Edge, the
2974 // bounding box height is 0, so don't cache it (#5620).
2975 if (cacheKey && bBox.height > 0) {
2976
2977 // Rotate (#4681)
2978 while (cacheKeys.length > 250) {
2979 delete cache[cacheKeys.shift()];
2980 }
2981
2982 if (!cache[cacheKey]) {
2983 cacheKeys.push(cacheKey);
2984 }
2985 cache[cacheKey] = bBox;
2986 }
2987 }
2988 return bBox;
2989 },
2990
2991 /**
2992 * Show the element
2993 */
2994 show: function(inherit) {
2995 return this.attr({
2996 visibility: inherit ? 'inherit' : 'visible'
2997 });
2998 },
2999
3000 /**
3001 * Hide the element
3002 */
3003 hide: function() {
3004 return this.attr({
3005 visibility: 'hidden'
3006 });
3007 },
3008
3009 fadeOut: function(duration) {
3010 var elemWrapper = this;
3011 elemWrapper.animate({
3012 opacity: 0
3013 }, {
3014 duration: duration || 150,
3015 complete: function() {
3016 elemWrapper.attr({
3017 y: -9999
3018 }); // #3088, assuming we're only using this for tooltips
3019 }
3020 });
3021 },
3022
3023 /**
3024 * Add the element
3025 * @param {Object|Undefined} parent Can be an element, an element wrapper or undefined
3026 * to append the element to the renderer.box.
3027 */
3028 add: function(parent) {
3029
3030 var renderer = this.renderer,
3031 element = this.element,
3032 inserted;
3033
3034 if (parent) {
3035 this.parentGroup = parent;
3036 }
3037
3038 // mark as inverted
3039 this.parentInverted = parent && parent.inverted;
3040
3041 // build formatted text
3042 if (this.textStr !== undefined) {
3043 renderer.buildText(this);
3044 }
3045
3046 // Mark as added
3047 this.added = true;
3048
3049 // If we're adding to renderer root, or other elements in the group
3050 // have a z index, we need to handle it
3051 if (!parent || parent.handleZ || this.zIndex) {
3052 inserted = this.zIndexSetter();
3053 }
3054
3055 // If zIndex is not handled, append at the end
3056 if (!inserted) {
3057 (parent ? parent.element : renderer.box).appendChild(element);
3058 }
3059
3060 // fire an event for internal hooks
3061 if (this.onAdd) {
3062 this.onAdd();
3063 }
3064
3065 return this;
3066 },
3067
3068 /**
3069 * Removes a child either by removeChild or move to garbageBin.
3070 * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not.
3071 */
3072 safeRemoveChild: function(element) {
3073 var parentNode = element.parentNode;
3074 if (parentNode) {
3075 parentNode.removeChild(element);
3076 }
3077 },
3078
3079 /**
3080 * Destroy the element and element wrapper
3081 */
3082 destroy: function() {
3083 var wrapper = this,
3084 element = wrapper.element || {},
3085 parentToClean = wrapper.renderer.isSVG && element.nodeName === 'SPAN' && wrapper.parentGroup,
3086 grandParent,
3087 key,
3088 i;
3089
3090 // remove events
3091 element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = element.point = null;
3092 stop(wrapper); // stop running animations
3093
3094 if (wrapper.clipPath) {
3095 wrapper.clipPath = wrapper.clipPath.destroy();
3096 }
3097
3098 // Destroy stops in case this is a gradient object
3099 if (wrapper.stops) {
3100 for (i = 0; i < wrapper.stops.length; i++) {
3101 wrapper.stops[i] = wrapper.stops[i].destroy();
3102 }
3103 wrapper.stops = null;
3104 }
3105
3106 // remove element
3107 wrapper.safeRemoveChild(element);
3108
3109
3110 wrapper.destroyShadows();
3111
3112
3113 // In case of useHTML, clean up empty containers emulating SVG groups (#1960, #2393, #2697).
3114 while (parentToClean && parentToClean.div && parentToClean.div.childNodes.length === 0) {
3115 grandParent = parentToClean.parentGroup;
3116 wrapper.safeRemoveChild(parentToClean.div);
3117 delete parentToClean.div;
3118 parentToClean = grandParent;
3119 }
3120
3121 // remove from alignObjects
3122 if (wrapper.alignTo) {
3123 erase(wrapper.renderer.alignedObjects, wrapper);
3124 }
3125
3126 for (key in wrapper) {
3127 delete wrapper[key];
3128 }
3129
3130 return null;
3131 },
3132
3133
3134 /**
3135 * Add a shadow to the element. Must be done after the element is added to the DOM
3136 * @param {Boolean|Object} shadowOptions
3137 */
3138 shadow: function(shadowOptions, group, cutOff) {
3139 var shadows = [],
3140 i,
3141 shadow,
3142 element = this.element,
3143 strokeWidth,
3144 shadowWidth,
3145 shadowElementOpacity,
3146
3147 // compensate for inverted plot area
3148 transform;
3149
3150 if (!shadowOptions) {
3151 this.destroyShadows();
3152
3153 } else if (!this.shadows) {
3154 shadowWidth = pick(shadowOptions.width, 3);
3155 shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth;
3156 transform = this.parentInverted ?
3157 '(-1,-1)' :
3158 '(' + pick(shadowOptions.offsetX, 1) + ', ' + pick(shadowOptions.offsetY, 1) + ')';
3159 for (i = 1; i <= shadowWidth; i++) {
3160 shadow = element.cloneNode(0);
3161 strokeWidth = (shadowWidth * 2) + 1 - (2 * i);
3162 attr(shadow, {
3163 'isShadow': 'true',
3164 'stroke': shadowOptions.color || '#000000',
3165 'stroke-opacity': shadowElementOpacity * i,
3166 'stroke-width': strokeWidth,
3167 'transform': 'translate' + transform,
3168 'fill': 'none'
3169 });
3170 if (cutOff) {
3171 attr(shadow, 'height', Math.max(attr(shadow, 'height') - strokeWidth, 0));
3172 shadow.cutHeight = strokeWidth;
3173 }
3174
3175 if (group) {
3176 group.element.appendChild(shadow);
3177 } else {
3178 element.parentNode.insertBefore(shadow, element);
3179 }
3180
3181 shadows.push(shadow);
3182 }
3183
3184 this.shadows = shadows;
3185 }
3186 return this;
3187
3188 },
3189
3190 destroyShadows: function() {
3191 each(this.shadows || [], function(shadow) {
3192 this.safeRemoveChild(shadow);
3193 }, this);
3194 this.shadows = undefined;
3195 },
3196
3197
3198
3199 xGetter: function(key) {
3200 if (this.element.nodeName === 'circle') {
3201 if (key === 'x') {
3202 key = 'cx';
3203 } else if (key === 'y') {
3204 key = 'cy';
3205 }
3206 }
3207 return this._defaultGetter(key);
3208 },
3209
3210 /**
3211 * Get the current value of an attribute or pseudo attribute, used mainly
3212 * for animation.
3213 */
3214 _defaultGetter: function(key) {
3215 var ret = pick(this[key], this.element ? this.element.getAttribute(key) : null, 0);
3216
3217 if (/^[\-0-9\.]+$/.test(ret)) { // is numerical
3218 ret = parseFloat(ret);
3219 }
3220 return ret;
3221 },
3222
3223
3224 dSetter: function(value, key, element) {
3225 if (value && value.join) { // join path
3226 value = value.join(' ');
3227 }
3228 if (/(NaN| {2}|^$)/.test(value)) {
3229 value = 'M 0 0';
3230 }
3231 element.setAttribute(key, value);
3232
3233 this[key] = value;
3234 },
3235
3236 dashstyleSetter: function(value) {
3237 var i,
3238 strokeWidth = this['stroke-width'];
3239
3240 // If "inherit", like maps in IE, assume 1 (#4981). With HC5 and the new strokeWidth
3241 // function, we should be able to use that instead.
3242 if (strokeWidth === 'inherit') {
3243 strokeWidth = 1;
3244 }
3245 value = value && value.toLowerCase();
3246 if (value) {
3247 value = value
3248 .replace('shortdashdotdot', '3,1,1,1,1,1,')
3249 .replace('shortdashdot', '3,1,1,1')
3250 .replace('shortdot', '1,1,')
3251 .replace('shortdash', '3,1,')
3252 .replace('longdash', '8,3,')
3253 .replace(/dot/g, '1,3,')
3254 .replace('dash', '4,3,')
3255 .replace(/,$/, '')
3256 .split(','); // ending comma
3257
3258 i = value.length;
3259 while (i--) {
3260 value[i] = pInt(value[i]) * strokeWidth;
3261 }
3262 value = value.join(',')
3263 .replace(/NaN/g, 'none'); // #3226
3264 this.element.setAttribute('stroke-dasharray', value);
3265 }
3266 },
3267
3268 alignSetter: function(value) {
3269 var convert = {
3270 left: 'start',
3271 center: 'middle',
3272 right: 'end'
3273 };
3274 this.element.setAttribute('text-anchor', convert[value]);
3275 },
3276 titleSetter: function(value) {
3277 var titleNode = this.element.getElementsByTagName('title')[0];
3278 if (!titleNode) {
3279 titleNode = doc.createElementNS(this.SVG_NS, 'title');
3280 this.element.appendChild(titleNode);
3281 }
3282
3283 // Remove text content if it exists
3284 if (titleNode.firstChild) {
3285 titleNode.removeChild(titleNode.firstChild);
3286 }
3287
3288 titleNode.appendChild(
3289 doc.createTextNode(
3290 (String(pick(value), '')).replace(/<[^>]*>/g, '') // #3276, #3895
3291 )
3292 );
3293 },
3294 textSetter: function(value) {
3295 if (value !== this.textStr) {
3296 // Delete bBox memo when the text changes
3297 delete this.bBox;
3298
3299 this.textStr = value;
3300 if (this.added) {
3301 this.renderer.buildText(this);
3302 }
3303 }
3304 },
3305 fillSetter: function(value, key, element) {
3306 if (typeof value === 'string') {
3307 element.setAttribute(key, value);
3308 } else if (value) {
3309 this.colorGradient(value, key, element);
3310 }
3311 },
3312 visibilitySetter: function(value, key, element) {
3313 // IE9-11 doesn't handle visibilty:inherit well, so we remove the attribute instead (#2881, #3909)
3314 if (value === 'inherit') {
3315 element.removeAttribute(key);
3316 } else {
3317 element.setAttribute(key, value);
3318 }
3319 },
3320 zIndexSetter: function(value, key) {
3321 var renderer = this.renderer,
3322 parentGroup = this.parentGroup,
3323 parentWrapper = parentGroup || renderer,
3324 parentNode = parentWrapper.element || renderer.box,
3325 childNodes,
3326 otherElement,
3327 otherZIndex,
3328 element = this.element,
3329 inserted,
3330 run = this.added,
3331 i;
3332
3333 if (defined(value)) {
3334 element.zIndex = value; // So we can read it for other elements in the group
3335 value = +value;
3336 if (this[key] === value) { // Only update when needed (#3865)
3337 run = false;
3338 }
3339 this[key] = value;
3340 }
3341
3342 // Insert according to this and other elements' zIndex. Before .add() is called,
3343 // nothing is done. Then on add, or by later calls to zIndexSetter, the node
3344 // is placed on the right place in the DOM.
3345 if (run) {
3346 value = this.zIndex;
3347
3348 if (value && parentGroup) {
3349 parentGroup.handleZ = true;
3350 }
3351
3352 childNodes = parentNode.childNodes;
3353 for (i = 0; i < childNodes.length && !inserted; i++) {
3354 otherElement = childNodes[i];
3355 otherZIndex = otherElement.zIndex;
3356 if (otherElement !== element && (
3357 // Insert before the first element with a higher zIndex
3358 pInt(otherZIndex) > value ||
3359 // If no zIndex given, insert before the first element with a zIndex
3360 (!defined(value) && defined(otherZIndex))
3361
3362 )) {
3363 parentNode.insertBefore(element, otherElement);
3364 inserted = true;
3365 }
3366 }
3367 if (!inserted) {
3368 parentNode.appendChild(element);
3369 }
3370 }
3371 return inserted;
3372 },
3373 _defaultSetter: function(value, key, element) {
3374 element.setAttribute(key, value);
3375 }
3376 };
3377
3378 // Some shared setters and getters
3379 SVGElement.prototype.yGetter = SVGElement.prototype.xGetter;
3380 SVGElement.prototype.translateXSetter = SVGElement.prototype.translateYSetter =
3381 SVGElement.prototype.rotationSetter = SVGElement.prototype.verticalAlignSetter =
3382 SVGElement.prototype.scaleXSetter = SVGElement.prototype.scaleYSetter = function(value, key) {
3383 this[key] = value;
3384 this.doTransform = true;
3385 };
3386 // These setters both set the key on the instance itself plus as an attribute
3387 SVGElement.prototype.opacitySetter = SVGElement.prototype.displaySetter = function(value, key, element) {
3388 this[key] = value;
3389 element.setAttribute(key, value);
3390 };
3391
3392
3393 // WebKit and Batik have problems with a stroke-width of zero, so in this case we remove the
3394 // stroke attribute altogether. #1270, #1369, #3065, #3072.
3395 SVGElement.prototype['stroke-widthSetter'] = SVGElement.prototype.strokeSetter = function(value, key, element) {
3396 this[key] = value;
3397 // Only apply the stroke attribute if the stroke width is defined and larger than 0
3398 if (this.stroke && this['stroke-width']) {
3399 SVGElement.prototype.fillSetter.call(this, this.stroke, 'stroke', element); // use prototype as instance may be overridden
3400 element.setAttribute('stroke-width', this['stroke-width']);
3401 this.hasStroke = true;
3402 } else if (key === 'stroke-width' && value === 0 && this.hasStroke) {
3403 element.removeAttribute('stroke');
3404 this.hasStroke = false;
3405 }
3406 };
3407
3408
3409 /**
3410 * The default SVG renderer
3411 */
3412 SVGRenderer = H.SVGRenderer = function() {
3413 this.init.apply(this, arguments);
3414 };
3415 SVGRenderer.prototype = {
3416 Element: SVGElement,
3417 SVG_NS: SVG_NS,
3418 /**
3419 * Initialize the SVGRenderer
3420 * @param {Object} container
3421 * @param {Number} width
3422 * @param {Number} height
3423 * @param {Boolean} forExport
3424 */
3425 init: function(container, width, height, style, forExport, allowHTML) {
3426 var renderer = this,
3427 boxWrapper,
3428 element,
3429 desc;
3430
3431 boxWrapper = renderer.createElement('svg')
3432 .attr({
3433 'version': '1.1',
3434 'class': 'highcharts-root'
3435 })
3436
3437 .css(this.getStyle(style));
3438 element = boxWrapper.element;
3439 container.appendChild(element);
3440
3441 // For browsers other than IE, add the namespace attribute (#1978)
3442 if (container.innerHTML.indexOf('xmlns') === -1) {
3443 attr(element, 'xmlns', this.SVG_NS);
3444 }
3445
3446 // object properties
3447 renderer.isSVG = true;
3448 renderer.box = element;
3449 renderer.boxWrapper = boxWrapper;
3450 renderer.alignedObjects = [];
3451
3452 // Page url used for internal references. #24, #672, #1070
3453 renderer.url = (isFirefox || isWebKit) && doc.getElementsByTagName('base').length ?
3454 win.location.href
3455 .replace(/#.*?$/, '') // remove the hash
3456 .replace(/([\('\)])/g, '\\$1') // escape parantheses and quotes
3457 .replace(/ /g, '%20') : // replace spaces (needed for Safari only)
3458 '';
3459
3460 // Add description
3461 desc = this.createElement('desc').add();
3462 desc.element.appendChild(doc.createTextNode('Created with Highmaps 5.0.0'));
3463
3464
3465 renderer.defs = this.createElement('defs').add();
3466 renderer.allowHTML = allowHTML;
3467 renderer.forExport = forExport;
3468 renderer.gradients = {}; // Object where gradient SvgElements are stored
3469 renderer.cache = {}; // Cache for numerical bounding boxes
3470 renderer.cacheKeys = [];
3471 renderer.imgCount = 0;
3472
3473 renderer.setSize(width, height, false);
3474
3475
3476
3477 // Issue 110 workaround:
3478 // In Firefox, if a div is positioned by percentage, its pixel position may land
3479 // between pixels. The container itself doesn't display this, but an SVG element
3480 // inside this container will be drawn at subpixel precision. In order to draw
3481 // sharp lines, this must be compensated for. This doesn't seem to work inside
3482 // iframes though (like in jsFiddle).
3483 var subPixelFix, rect;
3484 if (isFirefox && container.getBoundingClientRect) {
3485 renderer.subPixelFix = subPixelFix = function() {
3486 css(container, {
3487 left: 0,
3488 top: 0
3489 });
3490 rect = container.getBoundingClientRect();
3491 css(container, {
3492 left: (Math.ceil(rect.left) - rect.left) + 'px',
3493 top: (Math.ceil(rect.top) - rect.top) + 'px'
3494 });
3495 };
3496
3497 // run the fix now
3498 subPixelFix();
3499
3500 // run it on resize
3501 addEvent(win, 'resize', subPixelFix);
3502 }
3503 },
3504
3505
3506
3507 getStyle: function(style) {
3508 this.style = extend({
3509
3510 fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif', // default font
3511 fontSize: '12px'
3512
3513 }, style);
3514 return this.style;
3515 },
3516 setStyle: function(style) {
3517 this.boxWrapper.css(this.getStyle(style));
3518 },
3519
3520
3521 /**
3522 * Detect whether the renderer is hidden. This happens when one of the parent elements
3523 * has display: none. #608.
3524 */
3525 isHidden: function() {
3526 return !this.boxWrapper.getBBox().width;
3527 },
3528
3529 /**
3530 * Destroys the renderer and its allocated members.
3531 */
3532 destroy: function() {
3533 var renderer = this,
3534 rendererDefs = renderer.defs;
3535 renderer.box = null;
3536 renderer.boxWrapper = renderer.boxWrapper.destroy();
3537
3538 // Call destroy on all gradient elements
3539 destroyObjectProperties(renderer.gradients || {});
3540 renderer.gradients = null;
3541
3542 // Defs are null in VMLRenderer
3543 // Otherwise, destroy them here.
3544 if (rendererDefs) {
3545 renderer.defs = rendererDefs.destroy();
3546 }
3547
3548 // Remove sub pixel fix handler
3549 // We need to check that there is a handler, otherwise all functions that are registered for event 'resize' are removed
3550 // See issue #982
3551 if (renderer.subPixelFix) {
3552 removeEvent(win, 'resize', renderer.subPixelFix);
3553 }
3554
3555 renderer.alignedObjects = null;
3556
3557 return null;
3558 },
3559
3560 /**
3561 * Create a wrapper for an SVG element
3562 * @param {Object} nodeName
3563 */
3564 createElement: function(nodeName) {
3565 var wrapper = new this.Element();
3566 wrapper.init(this, nodeName);
3567 return wrapper;
3568 },
3569
3570 /**
3571 * Dummy function for plugins
3572 */
3573 draw: noop,
3574
3575 /**
3576 * Get converted radial gradient attributes
3577 */
3578 getRadialAttr: function(radialReference, gradAttr) {
3579 return {
3580 cx: (radialReference[0] - radialReference[2] / 2) + gradAttr.cx * radialReference[2],
3581 cy: (radialReference[1] - radialReference[2] / 2) + gradAttr.cy * radialReference[2],
3582 r: gradAttr.r * radialReference[2]
3583 };
3584 },
3585
3586 /**
3587 * Parse a simple HTML string into SVG tspans
3588 *
3589 * @param {Object} textNode The parent text SVG node
3590 */
3591 buildText: function(wrapper) {
3592 var textNode = wrapper.element,
3593 renderer = this,
3594 forExport = renderer.forExport,
3595 textStr = pick(wrapper.textStr, '').toString(),
3596 hasMarkup = textStr.indexOf('<') !== -1,
3597 lines,
3598 childNodes = textNode.childNodes,
3599 clsRegex,
3600 styleRegex,
3601 hrefRegex,
3602 wasTooLong,
3603 parentX = attr(textNode, 'x'),
3604 textStyles = wrapper.styles,
3605 width = wrapper.textWidth,
3606 textLineHeight = textStyles && textStyles.lineHeight,
3607 textShadow = textStyles && textStyles.textShadow,
3608 ellipsis = textStyles && textStyles.textOverflow === 'ellipsis',
3609 i = childNodes.length,
3610 tempParent = width && !wrapper.added && this.box,
3611 getLineHeight = function(tspan) {
3612 var fontSizeStyle;
3613
3614 fontSizeStyle = /(px|em)$/.test(tspan && tspan.style.fontSize) ?
3615 tspan.style.fontSize :
3616 ((textStyles && textStyles.fontSize) || renderer.style.fontSize || 12);
3617
3618
3619 return textLineHeight ?
3620 pInt(textLineHeight) :
3621 renderer.fontMetrics(
3622 fontSizeStyle,
3623 tspan
3624 ).h;
3625 },
3626 unescapeAngleBrackets = function(inputStr) {
3627 return inputStr.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
3628 };
3629
3630 /// remove old text
3631 while (i--) {
3632 textNode.removeChild(childNodes[i]);
3633 }
3634
3635 // Skip tspans, add text directly to text node. The forceTSpan is a hook
3636 // used in text outline hack.
3637 if (!hasMarkup && !textShadow && !ellipsis && !width && textStr.indexOf(' ') === -1) {
3638 textNode.appendChild(doc.createTextNode(unescapeAngleBrackets(textStr)));
3639
3640 // Complex strings, add more logic
3641 } else {
3642
3643 clsRegex = /<.*class="([^"]+)".*>/;
3644 styleRegex = /<.*style="([^"]+)".*>/;
3645 hrefRegex = /<.*href="(http[^"]+)".*>/;
3646
3647 if (tempParent) {
3648 tempParent.appendChild(textNode); // attach it to the DOM to read offset width
3649 }
3650
3651 if (hasMarkup) {
3652 lines = textStr
3653 .replace(/<(b|strong)>/g, '<span style="font-weight:bold">')
3654 .replace(/<(i|em)>/g, '<span style="font-style:italic">')
3655 .replace(/<a/g, '<span')
3656 .replace(/<\/(b|strong|i|em|a)>/g, '</span>')
3657 .split(/<br.*?>/g);
3658
3659 } else {
3660 lines = [textStr];
3661 }
3662
3663
3664 // Trim empty lines (#5261)
3665 lines = grep(lines, function(line) {
3666 return line !== '';
3667 });
3668
3669
3670 // build the lines
3671 each(lines, function buildTextLines(line, lineNo) {
3672 var spans,
3673 spanNo = 0;
3674 line = line
3675 .replace(/^\s+|\s+$/g, '') // Trim to prevent useless/costly process on the spaces (#5258)
3676 .replace(/<span/g, '|||<span')
3677 .replace(/<\/span>/g, '</span>|||');
3678 spans = line.split('|||');
3679
3680 each(spans, function buildTextSpans(span) {
3681 if (span !== '' || spans.length === 1) {
3682 var attributes = {},
3683 tspan = doc.createElementNS(renderer.SVG_NS, 'tspan'),
3684 spanCls,
3685 spanStyle; // #390
3686 if (clsRegex.test(span)) {
3687 spanCls = span.match(clsRegex)[1];
3688 attr(tspan, 'class', spanCls);
3689 }
3690 if (styleRegex.test(span)) {
3691 spanStyle = span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2');
3692 attr(tspan, 'style', spanStyle);
3693 }
3694 if (hrefRegex.test(span) && !forExport) { // Not for export - #1529
3695 attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"');
3696 css(tspan, {
3697 cursor: 'pointer'
3698 });
3699 }
3700
3701 span = unescapeAngleBrackets(span.replace(/<(.|\n)*?>/g, '') || ' ');
3702
3703 // Nested tags aren't supported, and cause crash in Safari (#1596)
3704 if (span !== ' ') {
3705
3706 // add the text node
3707 tspan.appendChild(doc.createTextNode(span));
3708
3709 if (!spanNo) { // first span in a line, align it to the left
3710 if (lineNo && parentX !== null) {
3711 attributes.x = parentX;
3712 }
3713 } else {
3714 attributes.dx = 0; // #16
3715 }
3716
3717 // add attributes
3718 attr(tspan, attributes);
3719
3720 // Append it
3721 textNode.appendChild(tspan);
3722
3723 // first span on subsequent line, add the line height
3724 if (!spanNo && lineNo) {
3725
3726 // allow getting the right offset height in exporting in IE
3727 if (!svg && forExport) {
3728 css(tspan, {
3729 display: 'block'
3730 });
3731 }
3732
3733 // Set the line height based on the font size of either
3734 // the text element or the tspan element
3735 attr(
3736 tspan,
3737 'dy',
3738 getLineHeight(tspan)
3739 );
3740 }
3741
3742 /*if (width) {
3743 renderer.breakText(wrapper, width);
3744 }*/
3745
3746 // Check width and apply soft breaks or ellipsis
3747 if (width) {
3748 var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273
3749 noWrap = textStyles.whiteSpace === 'nowrap',
3750 hasWhiteSpace = spans.length > 1 || lineNo || (words.length > 1 && !noWrap),
3751 tooLong,
3752 actualWidth,
3753 rest = [],
3754 dy = getLineHeight(tspan),
3755 rotation = wrapper.rotation,
3756 wordStr = span, // for ellipsis
3757 cursor = wordStr.length, // binary search cursor
3758 bBox;
3759
3760 while ((hasWhiteSpace || ellipsis) && (words.length || rest.length)) {
3761 wrapper.rotation = 0; // discard rotation when computing box
3762 bBox = wrapper.getBBox(true);
3763 actualWidth = bBox.width;
3764
3765 // Old IE cannot measure the actualWidth for SVG elements (#2314)
3766 if (!svg && renderer.forExport) {
3767 actualWidth = renderer.measureSpanWidth(tspan.firstChild.data, wrapper.styles);
3768 }
3769
3770 tooLong = actualWidth > width;
3771
3772 // For ellipsis, do a binary search for the correct string length
3773 if (wasTooLong === undefined) {
3774 wasTooLong = tooLong; // First time
3775 }
3776 if (ellipsis && wasTooLong) {
3777 cursor /= 2;
3778
3779 if (wordStr === '' || (!tooLong && cursor < 0.5)) {
3780 words = []; // All ok, break out
3781 } else {
3782 wordStr = span.substring(0, wordStr.length + (tooLong ? -1 : 1) * Math.ceil(cursor));
3783 words = [wordStr + (width > 3 ? '\u2026' : '')];
3784 tspan.removeChild(tspan.firstChild);
3785 }
3786
3787 // Looping down, this is the first word sequence that is not too long,
3788 // so we can move on to build the next line.
3789 } else if (!tooLong || words.length === 1) {
3790 words = rest;
3791 rest = [];
3792
3793 if (words.length && !noWrap) {
3794 tspan = doc.createElementNS(SVG_NS, 'tspan');
3795 attr(tspan, {
3796 dy: dy,
3797 x: parentX
3798 });
3799 if (spanStyle) { // #390
3800 attr(tspan, 'style', spanStyle);
3801 }
3802 textNode.appendChild(tspan);
3803 }
3804 if (actualWidth > width) { // a single word is pressing it out
3805 width = actualWidth;
3806 }
3807 } else { // append to existing line tspan
3808 tspan.removeChild(tspan.firstChild);
3809 rest.unshift(words.pop());
3810 }
3811 if (words.length) {
3812 tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-')));
3813 }
3814 }
3815 wrapper.rotation = rotation;
3816 }
3817
3818 spanNo++;
3819 }
3820 }
3821 });
3822 });
3823
3824 if (wasTooLong) {
3825 wrapper.attr('title', wrapper.textStr);
3826 }
3827 if (tempParent) {
3828 tempParent.removeChild(textNode); // attach it to the DOM to read offset width
3829 }
3830
3831 // Apply the text shadow
3832 if (textShadow && wrapper.applyTextShadow) {
3833 wrapper.applyTextShadow(textShadow);
3834 }
3835 }
3836 },
3837
3838
3839
3840 /*
3841 breakText: function (wrapper, width) {
3842 var bBox = wrapper.getBBox(),
3843 node = wrapper.element,
3844 textLength = node.textContent.length,
3845 pos = Math.round(width * textLength / bBox.width), // try this position first, based on average character width
3846 increment = 0,
3847 finalPos;
3848
3849 if (bBox.width > width) {
3850 while (finalPos === undefined) {
3851 textLength = node.getSubStringLength(0, pos);
3852
3853 if (textLength <= width) {
3854 if (increment === -1) {
3855 finalPos = pos;
3856 } else {
3857 increment = 1;
3858 }
3859 } else {
3860 if (increment === 1) {
3861 finalPos = pos - 1;
3862 } else {
3863 increment = -1;
3864 }
3865 }
3866 pos += increment;
3867 }
3868 }
3869 console.log('width', width, 'stringWidth', node.getSubStringLength(0, finalPos))
3870 },
3871 */
3872
3873 /**
3874 * Returns white for dark colors and black for bright colors
3875 */
3876 getContrast: function(rgba) {
3877 rgba = color(rgba).rgba;
3878 return rgba[0] + rgba[1] + rgba[2] > 2 * 255 ? '#000000' : '#FFFFFF';
3879 },
3880
3881 /**
3882 * Create a button with preset states
3883 * @param {String} text
3884 * @param {Number} x
3885 * @param {Number} y
3886 * @param {Function} callback
3887 * @param {Object} normalState
3888 * @param {Object} hoverState
3889 * @param {Object} pressedState
3890 */
3891 button: function(text, x, y, callback, normalState, hoverState, pressedState, disabledState, shape) {
3892 var label = this.label(text, x, y, shape, null, null, null, null, 'button'),
3893 curState = 0;
3894
3895 // Default, non-stylable attributes
3896 label.attr(merge({
3897 'padding': 8,
3898 'r': 2
3899 }, normalState));
3900
3901
3902 // Presentational
3903 var normalStyle,
3904 hoverStyle,
3905 pressedStyle,
3906 disabledStyle;
3907
3908 // Normal state - prepare the attributes
3909 normalState = merge({
3910 fill: '#f7f7f7',
3911 stroke: '#cccccc',
3912 'stroke-width': 1,
3913 style: {
3914 color: '#333333',
3915 cursor: 'pointer',
3916 fontWeight: 'normal'
3917 }
3918 }, normalState);
3919 normalStyle = normalState.style;
3920 delete normalState.style;
3921
3922 // Hover state
3923 hoverState = merge(normalState, {
3924 fill: '#e6e6e6'
3925 }, hoverState);
3926 hoverStyle = hoverState.style;
3927 delete hoverState.style;
3928
3929 // Pressed state
3930 pressedState = merge(normalState, {
3931 fill: '#e6ebf5',
3932 style: {
3933 color: '#000000',
3934 fontWeight: 'bold'
3935 }
3936 }, pressedState);
3937 pressedStyle = pressedState.style;
3938 delete pressedState.style;
3939
3940 // Disabled state
3941 disabledState = merge(normalState, {
3942 style: {
3943 color: '#cccccc'
3944 }
3945 }, disabledState);
3946 disabledStyle = disabledState.style;
3947 delete disabledState.style;
3948
3949
3950 // Add the events. IE9 and IE10 need mouseover and mouseout to funciton (#667).
3951 addEvent(label.element, isMS ? 'mouseover' : 'mouseenter', function() {
3952 if (curState !== 3) {
3953 label.setState(1);
3954 }
3955 });
3956 addEvent(label.element, isMS ? 'mouseout' : 'mouseleave', function() {
3957 if (curState !== 3) {
3958 label.setState(curState);
3959 }
3960 });
3961
3962 label.setState = function(state) {
3963 // Hover state is temporary, don't record it
3964 if (state !== 1) {
3965 label.state = curState = state;
3966 }
3967 // Update visuals
3968 label.removeClass(/highcharts-button-(normal|hover|pressed|disabled)/)
3969 .addClass('highcharts-button-' + ['normal', 'hover', 'pressed', 'disabled'][state || 0]);
3970
3971
3972 label.attr([normalState, hoverState, pressedState, disabledState][state || 0])
3973 .css([normalStyle, hoverStyle, pressedStyle, disabledStyle][state || 0]);
3974
3975 };
3976
3977
3978
3979 // Presentational attributes
3980 label
3981 .attr(normalState)
3982 .css(extend({
3983 cursor: 'default'
3984 }, normalStyle));
3985
3986
3987 return label
3988 .on('click', function(e) {
3989 if (curState !== 3) {
3990 callback.call(label, e);
3991 }
3992 });
3993 },
3994
3995 /**
3996 * Make a straight line crisper by not spilling out to neighbour pixels
3997 * @param {Array} points
3998 * @param {Number} width
3999 */
4000 crispLine: function(points, width) {
4001 // points format: ['M', 0, 0, 'L', 100, 0]
4002 // normalize to a crisp line
4003 if (points[1] === points[4]) {
4004 // Substract due to #1129. Now bottom and left axis gridlines behave the same.
4005 points[1] = points[4] = Math.round(points[1]) - (width % 2 / 2);
4006 }
4007 if (points[2] === points[5]) {
4008 points[2] = points[5] = Math.round(points[2]) + (width % 2 / 2);
4009 }
4010 return points;
4011 },
4012
4013
4014 /**
4015 * Draw a path
4016 * @param {Array} path An SVG path in array form
4017 */
4018 path: function(path) {
4019 var attribs = {
4020
4021 fill: 'none'
4022
4023 };
4024 if (isArray(path)) {
4025 attribs.d = path;
4026 } else if (isObject(path)) { // attributes
4027 extend(attribs, path);
4028 }
4029 return this.createElement('path').attr(attribs);
4030 },
4031
4032 /**
4033 * Draw and return an SVG circle
4034 * @param {Number} x The x position
4035 * @param {Number} y The y position
4036 * @param {Number} r The radius
4037 */
4038 circle: function(x, y, r) {
4039 var attribs = isObject(x) ? x : {
4040 x: x,
4041 y: y,
4042 r: r
4043 },
4044 wrapper = this.createElement('circle');
4045
4046 // Setting x or y translates to cx and cy
4047 wrapper.xSetter = wrapper.ySetter = function(value, key, element) {
4048 element.setAttribute('c' + key, value);
4049 };
4050
4051 return wrapper.attr(attribs);
4052 },
4053
4054 /**
4055 * Draw and return an arc
4056 * @param {Number} x X position
4057 * @param {Number} y Y position
4058 * @param {Number} r Radius
4059 * @param {Number} innerR Inner radius like used in donut charts
4060 * @param {Number} start Starting angle
4061 * @param {Number} end Ending angle
4062 */
4063 arc: function(x, y, r, innerR, start, end) {
4064 var arc;
4065
4066 if (isObject(x)) {
4067 y = x.y;
4068 r = x.r;
4069 innerR = x.innerR;
4070 start = x.start;
4071 end = x.end;
4072 x = x.x;
4073 }
4074
4075 // Arcs are defined as symbols for the ability to set
4076 // attributes in attr and animate
4077 arc = this.symbol('arc', x || 0, y || 0, r || 0, r || 0, {
4078 innerR: innerR || 0,
4079 start: start || 0,
4080 end: end || 0
4081 });
4082 arc.r = r; // #959
4083 return arc;
4084 },
4085
4086 /**
4087 * Draw and return a rectangle
4088 * @param {Number} x Left position
4089 * @param {Number} y Top position
4090 * @param {Number} width
4091 * @param {Number} height
4092 * @param {Number} r Border corner radius
4093 * @param {Number} strokeWidth A stroke width can be supplied to allow crisp drawing
4094 */
4095 rect: function(x, y, width, height, r, strokeWidth) {
4096
4097 r = isObject(x) ? x.r : r;
4098
4099 var wrapper = this.createElement('rect'),
4100 attribs = isObject(x) ? x : x === undefined ? {} : {
4101 x: x,
4102 y: y,
4103 width: Math.max(width, 0),
4104 height: Math.max(height, 0)
4105 };
4106
4107
4108 if (strokeWidth !== undefined) {
4109 attribs.strokeWidth = strokeWidth;
4110 attribs = wrapper.crisp(attribs);
4111 }
4112 attribs.fill = 'none';
4113
4114
4115 if (r) {
4116 attribs.r = r;
4117 }
4118
4119 wrapper.rSetter = function(value, key, element) {
4120 attr(element, {
4121 rx: value,
4122 ry: value
4123 });
4124 };
4125
4126 return wrapper.attr(attribs);
4127 },
4128
4129 /**
4130 * Resize the box and re-align all aligned elements
4131 * @param {Object} width
4132 * @param {Object} height
4133 * @param {Boolean} animate
4134 *
4135 */
4136 setSize: function(width, height, animate) {
4137 var renderer = this,
4138 alignedObjects = renderer.alignedObjects,
4139 i = alignedObjects.length;
4140
4141 renderer.width = width;
4142 renderer.height = height;
4143
4144 renderer.boxWrapper.animate({
4145 width: width,
4146 height: height
4147 }, {
4148 step: function() {
4149 this.attr({
4150 viewBox: '0 0 ' + this.attr('width') + ' ' + this.attr('height')
4151 });
4152 },
4153 duration: pick(animate, true) ? undefined : 0
4154 });
4155
4156 while (i--) {
4157 alignedObjects[i].align();
4158 }
4159 },
4160
4161 /**
4162 * Create a group
4163 * @param {String} name The group will be given a class name of 'highcharts-{name}'.
4164 * This can be used for styling and scripting.
4165 */
4166 g: function(name) {
4167 var elem = this.createElement('g');
4168 return name ? elem.attr({
4169 'class': 'highcharts-' + name
4170 }) : elem;
4171 },
4172
4173 /**
4174 * Display an image
4175 * @param {String} src
4176 * @param {Number} x
4177 * @param {Number} y
4178 * @param {Number} width
4179 * @param {Number} height
4180 */
4181 image: function(src, x, y, width, height) {
4182 var attribs = {
4183 preserveAspectRatio: 'none'
4184 },
4185 elemWrapper;
4186
4187 // optional properties
4188 if (arguments.length > 1) {
4189 extend(attribs, {
4190 x: x,
4191 y: y,
4192 width: width,
4193 height: height
4194 });
4195 }
4196
4197 elemWrapper = this.createElement('image').attr(attribs);
4198
4199 // set the href in the xlink namespace
4200 if (elemWrapper.element.setAttributeNS) {
4201 elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink',
4202 'href', src);
4203 } else {
4204 // could be exporting in IE
4205 // using href throws "not supported" in ie7 and under, requries regex shim to fix later
4206 elemWrapper.element.setAttribute('hc-svg-href', src);
4207 }
4208 return elemWrapper;
4209 },
4210
4211 /**
4212 * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object.
4213 *
4214 * @param {Object} symbol
4215 * @param {Object} x
4216 * @param {Object} y
4217 * @param {Object} radius
4218 * @param {Object} options
4219 */
4220 symbol: function(symbol, x, y, width, height, options) {
4221
4222 var ren = this,
4223 obj,
4224
4225 // get the symbol definition function
4226 symbolFn = this.symbols[symbol],
4227
4228 // check if there's a path defined for this symbol
4229 path = defined(x) && symbolFn && symbolFn(
4230 Math.round(x),
4231 Math.round(y),
4232 width,
4233 height,
4234 options
4235 ),
4236 imageRegex = /^url\((.*?)\)$/,
4237 imageSrc,
4238 centerImage,
4239 symbolSizes = {};
4240
4241 if (symbolFn) {
4242 obj = this.path(path);
4243
4244
4245 obj.attr('fill', 'none');
4246
4247
4248 // expando properties for use in animate and attr
4249 extend(obj, {
4250 symbolName: symbol,
4251 x: x,
4252 y: y,
4253 width: width,
4254 height: height
4255 });
4256 if (options) {
4257 extend(obj, options);
4258 }
4259
4260
4261 // image symbols
4262 } else if (imageRegex.test(symbol)) {
4263
4264
4265 imageSrc = symbol.match(imageRegex)[1];
4266
4267 // Create the image synchronously, add attribs async
4268 obj = this.image(imageSrc);
4269
4270 // The image width is not always the same as the symbol width. The image may be centered within the symbol,
4271 // as is the case when image shapes are used as label backgrounds, for example in flags.
4272 obj.imgwidth = pick(symbolSizes[imageSrc] && symbolSizes[imageSrc].width, options && options.width);
4273 obj.imgheight = pick(symbolSizes[imageSrc] && symbolSizes[imageSrc].height, options && options.height);
4274 /**
4275 * Set the size and position
4276 */
4277 centerImage = function() {
4278 obj.attr({
4279 width: obj.width,
4280 height: obj.height
4281 });
4282 };
4283
4284 /**
4285 * Width and height setters that take both the image's physical size and the label size into
4286 * consideration, and translates the image to center within the label.
4287 */
4288 each(['width', 'height'], function(key) {
4289 obj[key + 'Setter'] = function(value, key) {
4290 var attribs = {},
4291 imgSize = this['img' + key];
4292 this[key] = value;
4293 if (defined(imgSize)) {
4294 if (this.element) {
4295 this.element.setAttribute(key, imgSize);
4296 }
4297 if (!this.alignByTranslate) {
4298 attribs[key === 'width' ? 'translateX' : 'translateY'] = (this[key] - imgSize) / 2;
4299 this.attr(attribs);
4300 }
4301 }
4302 };
4303 });
4304
4305
4306 if (defined(x)) {
4307 obj.attr({
4308 x: x,
4309 y: y
4310 });
4311 }
4312 obj.isImg = true;
4313
4314 if (defined(obj.imgwidth) && defined(obj.imgheight)) {
4315 centerImage();
4316 } else {
4317 // Initialize image to be 0 size so export will still function if there's no cached sizes.
4318 obj.attr({
4319 width: 0,
4320 height: 0
4321 });
4322
4323 // Create a dummy JavaScript image to get the width and height. Due to a bug in IE < 8,
4324 // the created element must be assigned to a variable in order to load (#292).
4325 createElement('img', {
4326 onload: function() {
4327
4328 var chart = charts[ren.chartIndex];
4329
4330 // Special case for SVGs on IE11, the width is not accessible until the image is
4331 // part of the DOM (#2854).
4332 if (this.width === 0) {
4333 css(this, {
4334 position: 'absolute',
4335 top: '-999em'
4336 });
4337 doc.body.appendChild(this);
4338 }
4339
4340 // Center the image
4341 symbolSizes[imageSrc] = { // Cache for next
4342 width: this.width,
4343 height: this.height
4344 };
4345 obj.imgwidth = this.width;
4346 obj.imgheight = this.height;
4347
4348 if (obj.element) {
4349 centerImage();
4350 }
4351
4352 // Clean up after #2854 workaround.
4353 if (this.parentNode) {
4354 this.parentNode.removeChild(this);
4355 }
4356
4357 // Fire the load event when all external images are loaded
4358 ren.imgCount--;
4359 if (!ren.imgCount && chart && chart.onload) {
4360 chart.onload();
4361 }
4362 },
4363 src: imageSrc
4364 });
4365 this.imgCount++;
4366 }
4367 }
4368
4369 return obj;
4370 },
4371
4372 /**
4373 * An extendable collection of functions for defining symbol paths.
4374 */
4375 symbols: {
4376 'circle': function(x, y, w, h) {
4377 var cpw = 0.166 * w;
4378 return [
4379 'M', x + w / 2, y,
4380 'C', x + w + cpw, y, x + w + cpw, y + h, x + w / 2, y + h,
4381 'C', x - cpw, y + h, x - cpw, y, x + w / 2, y,
4382 'Z'
4383 ];
4384 },
4385
4386 'square': function(x, y, w, h) {
4387 return [
4388 'M', x, y,
4389 'L', x + w, y,
4390 x + w, y + h,
4391 x, y + h,
4392 'Z'
4393 ];
4394 },
4395
4396 'triangle': function(x, y, w, h) {
4397 return [
4398 'M', x + w / 2, y,
4399 'L', x + w, y + h,
4400 x, y + h,
4401 'Z'
4402 ];
4403 },
4404
4405 'triangle-down': function(x, y, w, h) {
4406 return [
4407 'M', x, y,
4408 'L', x + w, y,
4409 x + w / 2, y + h,
4410 'Z'
4411 ];
4412 },
4413 'diamond': function(x, y, w, h) {
4414 return [
4415 'M', x + w / 2, y,
4416 'L', x + w, y + h / 2,
4417 x + w / 2, y + h,
4418 x, y + h / 2,
4419 'Z'
4420 ];
4421 },
4422 'arc': function(x, y, w, h, options) {
4423 var start = options.start,
4424 radius = options.r || w || h,
4425 end = options.end - 0.001, // to prevent cos and sin of start and end from becoming equal on 360 arcs (related: #1561)
4426 innerRadius = options.innerR,
4427 open = options.open,
4428 cosStart = Math.cos(start),
4429 sinStart = Math.sin(start),
4430 cosEnd = Math.cos(end),
4431 sinEnd = Math.sin(end),
4432 longArc = options.end - start < Math.PI ? 0 : 1;
4433
4434 return [
4435 'M',
4436 x + radius * cosStart,
4437 y + radius * sinStart,
4438 'A', // arcTo
4439 radius, // x radius
4440 radius, // y radius
4441 0, // slanting
4442 longArc, // long or short arc
4443 1, // clockwise
4444 x + radius * cosEnd,
4445 y + radius * sinEnd,
4446 open ? 'M' : 'L',
4447 x + innerRadius * cosEnd,
4448 y + innerRadius * sinEnd,
4449 'A', // arcTo
4450 innerRadius, // x radius
4451 innerRadius, // y radius
4452 0, // slanting
4453 longArc, // long or short arc
4454 0, // clockwise
4455 x + innerRadius * cosStart,
4456 y + innerRadius * sinStart,
4457
4458 open ? '' : 'Z' // close
4459 ];
4460 },
4461
4462 /**
4463 * Callout shape used for default tooltips, also used for rounded rectangles in VML
4464 */
4465 callout: function(x, y, w, h, options) {
4466 var arrowLength = 6,
4467 halfDistance = 6,
4468 r = Math.min((options && options.r) || 0, w, h),
4469 safeDistance = r + halfDistance,
4470 anchorX = options && options.anchorX,
4471 anchorY = options && options.anchorY,
4472 path;
4473
4474 path = [
4475 'M', x + r, y,
4476 'L', x + w - r, y, // top side
4477 'C', x + w, y, x + w, y, x + w, y + r, // top-right corner
4478 'L', x + w, y + h - r, // right side
4479 'C', x + w, y + h, x + w, y + h, x + w - r, y + h, // bottom-right corner
4480 'L', x + r, y + h, // bottom side
4481 'C', x, y + h, x, y + h, x, y + h - r, // bottom-left corner
4482 'L', x, y + r, // left side
4483 'C', x, y, x, y, x + r, y // top-right corner
4484 ];
4485
4486 if (anchorX && anchorX > w && anchorY > y + safeDistance && anchorY < y + h - safeDistance) { // replace right side
4487 path.splice(13, 3,
4488 'L', x + w, anchorY - halfDistance,
4489 x + w + arrowLength, anchorY,
4490 x + w, anchorY + halfDistance,
4491 x + w, y + h - r
4492 );
4493 } else if (anchorX && anchorX < 0 && anchorY > y + safeDistance && anchorY < y + h - safeDistance) { // replace left side
4494 path.splice(33, 3,
4495 'L', x, anchorY + halfDistance,
4496 x - arrowLength, anchorY,
4497 x, anchorY - halfDistance,
4498 x, y + r
4499 );
4500 } else if (anchorY && anchorY > h && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace bottom
4501 path.splice(23, 3,
4502 'L', anchorX + halfDistance, y + h,
4503 anchorX, y + h + arrowLength,
4504 anchorX - halfDistance, y + h,
4505 x + r, y + h
4506 );
4507 } else if (anchorY && anchorY < 0 && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace top
4508 path.splice(3, 3,
4509 'L', anchorX - halfDistance, y,
4510 anchorX, y - arrowLength,
4511 anchorX + halfDistance, y,
4512 w - r, y
4513 );
4514 }
4515 return path;
4516 }
4517 },
4518
4519 /**
4520 * Define a clipping rectangle
4521 * @param {String} id
4522 * @param {Number} x
4523 * @param {Number} y
4524 * @param {Number} width
4525 * @param {Number} height
4526 */
4527 clipRect: function(x, y, width, height) {
4528 var wrapper,
4529 id = 'highcharts-' + H.idCounter++,
4530
4531 clipPath = this.createElement('clipPath').attr({
4532 id: id
4533 }).add(this.defs);
4534
4535 wrapper = this.rect(x, y, width, height, 0).add(clipPath);
4536 wrapper.id = id;
4537 wrapper.clipPath = clipPath;
4538 wrapper.count = 0;
4539
4540 return wrapper;
4541 },
4542
4543
4544
4545
4546
4547 /**
4548 * Add text to the SVG object
4549 * @param {String} str
4550 * @param {Number} x Left position
4551 * @param {Number} y Top position
4552 * @param {Boolean} useHTML Use HTML to render the text
4553 */
4554 text: function(str, x, y, useHTML) {
4555
4556 // declare variables
4557 var renderer = this,
4558 fakeSVG = !svg && renderer.forExport,
4559 wrapper,
4560 attribs = {};
4561
4562 if (useHTML && (renderer.allowHTML || !renderer.forExport)) {
4563 return renderer.html(str, x, y);
4564 }
4565
4566 attribs.x = Math.round(x || 0); // X is always needed for line-wrap logic
4567 if (y) {
4568 attribs.y = Math.round(y);
4569 }
4570 if (str || str === 0) {
4571 attribs.text = str;
4572 }
4573
4574 wrapper = renderer.createElement('text')
4575 .attr(attribs);
4576
4577 // Prevent wrapping from creating false offsetWidths in export in legacy IE (#1079, #1063)
4578 if (fakeSVG) {
4579 wrapper.css({
4580 position: 'absolute'
4581 });
4582 }
4583
4584 if (!useHTML) {
4585 wrapper.xSetter = function(value, key, element) {
4586 var tspans = element.getElementsByTagName('tspan'),
4587 tspan,
4588 parentVal = element.getAttribute(key),
4589 i;
4590 for (i = 0; i < tspans.length; i++) {
4591 tspan = tspans[i];
4592 // If the x values are equal, the tspan represents a linebreak
4593 if (tspan.getAttribute(key) === parentVal) {
4594 tspan.setAttribute(key, value);
4595 }
4596 }
4597 element.setAttribute(key, value);
4598 };
4599 }
4600
4601 return wrapper;
4602 },
4603
4604 /**
4605 * Utility to return the baseline offset and total line height from the font size
4606 */
4607 fontMetrics: function(fontSize, elem) { // eslint-disable-line no-unused-vars
4608 var lineHeight,
4609 baseline;
4610
4611
4612 fontSize = fontSize || (this.style && this.style.fontSize);
4613
4614
4615 fontSize = /px/.test(fontSize) ? pInt(fontSize) : /em/.test(fontSize) ? parseFloat(fontSize) * 12 : 12;
4616
4617 // Empirical values found by comparing font size and bounding box height.
4618 // Applies to the default font family. http://jsfiddle.net/highcharts/7xvn7/
4619 lineHeight = fontSize < 24 ? fontSize + 3 : Math.round(fontSize * 1.2);
4620 baseline = Math.round(lineHeight * 0.8);
4621
4622 return {
4623 h: lineHeight,
4624 b: baseline,
4625 f: fontSize
4626 };
4627 },
4628
4629 /**
4630 * Correct X and Y positioning of a label for rotation (#1764)
4631 */
4632 rotCorr: function(baseline, rotation, alterY) {
4633 var y = baseline;
4634 if (rotation && alterY) {
4635 y = Math.max(y * Math.cos(rotation * deg2rad), 4);
4636 }
4637 return {
4638 x: (-baseline / 3) * Math.sin(rotation * deg2rad),
4639 y: y
4640 };
4641 },
4642
4643 /**
4644 * Add a label, a text item that can hold a colored or gradient background
4645 * as well as a border and shadow.
4646 * @param {string} str
4647 * @param {Number} x
4648 * @param {Number} y
4649 * @param {String} shape
4650 * @param {Number} anchorX In case the shape has a pointer, like a flag, this is the
4651 * coordinates it should be pinned to
4652 * @param {Number} anchorY
4653 * @param {Boolean} baseline Whether to position the label relative to the text baseline,
4654 * like renderer.text, or to the upper border of the rectangle.
4655 * @param {String} className Class name for the group
4656 */
4657 label: function(str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) {
4658
4659 var renderer = this,
4660 wrapper = renderer.g(className !== 'button' && 'label'),
4661 text = wrapper.text = renderer.text('', 0, 0, useHTML)
4662 .attr({
4663 zIndex: 1
4664 }),
4665 box,
4666 bBox,
4667 alignFactor = 0,
4668 padding = 3,
4669 paddingLeft = 0,
4670 width,
4671 height,
4672 wrapperX,
4673 wrapperY,
4674 textAlign,
4675 deferredAttr = {},
4676 strokeWidth,
4677 baselineOffset,
4678 hasBGImage = /^url\((.*?)\)$/.test(shape),
4679 needsBox = hasBGImage,
4680 getCrispAdjust,
4681 updateBoxSize,
4682 updateTextPadding,
4683 boxAttr;
4684
4685 if (className) {
4686 wrapper.addClass('highcharts-' + className);
4687 }
4688
4689
4690 needsBox = hasBGImage;
4691 getCrispAdjust = function() {
4692 return (strokeWidth || 0) % 2 / 2;
4693 };
4694
4695
4696
4697 /**
4698 * This function runs after the label is added to the DOM (when the bounding box is
4699 * available), and after the text of the label is updated to detect the new bounding
4700 * box and reflect it in the border box.
4701 */
4702 updateBoxSize = function() {
4703 var style = text.element.style,
4704 crispAdjust,
4705 attribs = {};
4706
4707 bBox = (width === undefined || height === undefined || textAlign) && defined(text.textStr) &&
4708 text.getBBox(); //#3295 && 3514 box failure when string equals 0
4709 wrapper.width = (width || bBox.width || 0) + 2 * padding + paddingLeft;
4710 wrapper.height = (height || bBox.height || 0) + 2 * padding;
4711
4712 // Update the label-scoped y offset
4713 baselineOffset = padding + renderer.fontMetrics(style && style.fontSize, text).b;
4714
4715
4716 if (needsBox) {
4717
4718 // Create the border box if it is not already present
4719 if (!box) {
4720 wrapper.box = box = renderer.symbols[shape] || hasBGImage ? // Symbol definition exists (#5324)
4721 renderer.symbol(shape) :
4722 renderer.rect();
4723
4724 box.addClass(
4725 (className === 'button' ? '' : 'highcharts-label-box') + // Don't use label className for buttons
4726 (className ? ' highcharts-' + className + '-box' : '')
4727 );
4728
4729 box.add(wrapper);
4730
4731 crispAdjust = getCrispAdjust();
4732 attribs.x = crispAdjust;
4733 attribs.y = (baseline ? -baselineOffset : 0) + crispAdjust;
4734 }
4735
4736 // Apply the box attributes
4737 attribs.width = Math.round(wrapper.width);
4738 attribs.height = Math.round(wrapper.height);
4739
4740 box.attr(extend(attribs, deferredAttr));
4741 deferredAttr = {};
4742 }
4743 };
4744
4745 /**
4746 * This function runs after setting text or padding, but only if padding is changed
4747 */
4748 updateTextPadding = function() {
4749 var textX = paddingLeft + padding,
4750 textY;
4751
4752 // determin y based on the baseline
4753 textY = baseline ? 0 : baselineOffset;
4754
4755 // compensate for alignment
4756 if (defined(width) && bBox && (textAlign === 'center' || textAlign === 'right')) {
4757 textX += {
4758 center: 0.5,
4759 right: 1
4760 }[textAlign] * (width - bBox.width);
4761 }
4762
4763 // update if anything changed
4764 if (textX !== text.x || textY !== text.y) {
4765 text.attr('x', textX);
4766 if (textY !== undefined) {
4767 text.attr('y', textY);
4768 }
4769 }
4770
4771 // record current values
4772 text.x = textX;
4773 text.y = textY;
4774 };
4775
4776 /**
4777 * Set a box attribute, or defer it if the box is not yet created
4778 * @param {Object} key
4779 * @param {Object} value
4780 */
4781 boxAttr = function(key, value) {
4782 if (box) {
4783 box.attr(key, value);
4784 } else {
4785 deferredAttr[key] = value;
4786 }
4787 };
4788
4789 /**
4790 * After the text element is added, get the desired size of the border box
4791 * and add it before the text in the DOM.
4792 */
4793 wrapper.onAdd = function() {
4794 text.add(wrapper);
4795 wrapper.attr({
4796 text: (str || str === 0) ? str : '', // alignment is available now // #3295: 0 not rendered if given as a value
4797 x: x,
4798 y: y
4799 });
4800
4801 if (box && defined(anchorX)) {
4802 wrapper.attr({
4803 anchorX: anchorX,
4804 anchorY: anchorY
4805 });
4806 }
4807 };
4808
4809 /*
4810 * Add specific attribute setters.
4811 */
4812
4813 // only change local variables
4814 wrapper.widthSetter = function(value) {
4815 width = value;
4816 };
4817 wrapper.heightSetter = function(value) {
4818 height = value;
4819 };
4820 wrapper['text-alignSetter'] = function(value) {
4821 textAlign = value;
4822 };
4823 wrapper.paddingSetter = function(value) {
4824 if (defined(value) && value !== padding) {
4825 padding = wrapper.padding = value;
4826 updateTextPadding();
4827 }
4828 };
4829 wrapper.paddingLeftSetter = function(value) {
4830 if (defined(value) && value !== paddingLeft) {
4831 paddingLeft = value;
4832 updateTextPadding();
4833 }
4834 };
4835
4836
4837 // change local variable and prevent setting attribute on the group
4838 wrapper.alignSetter = function(value) {
4839 value = {
4840 left: 0,
4841 center: 0.5,
4842 right: 1
4843 }[value];
4844 if (value !== alignFactor) {
4845 alignFactor = value;
4846 if (bBox) { // Bounding box exists, means we're dynamically changing
4847 wrapper.attr({
4848 x: wrapperX
4849 }); // #5134
4850 }
4851 }
4852 };
4853
4854 // apply these to the box and the text alike
4855 wrapper.textSetter = function(value) {
4856 if (value !== undefined) {
4857 text.textSetter(value);
4858 }
4859 updateBoxSize();
4860 updateTextPadding();
4861 };
4862
4863 // apply these to the box but not to the text
4864 wrapper['stroke-widthSetter'] = function(value, key) {
4865 if (value) {
4866 needsBox = true;
4867 }
4868 strokeWidth = this['stroke-width'] = value;
4869 boxAttr(key, value);
4870 };
4871
4872 wrapper.strokeSetter = wrapper.fillSetter = wrapper.rSetter = function(value, key) {
4873 if (key === 'fill' && value) {
4874 needsBox = true;
4875 }
4876 boxAttr(key, value);
4877 };
4878
4879 wrapper.anchorXSetter = function(value, key) {
4880 anchorX = value;
4881 boxAttr(key, Math.round(value) - getCrispAdjust() - wrapperX);
4882 };
4883 wrapper.anchorYSetter = function(value, key) {
4884 anchorY = value;
4885 boxAttr(key, value - wrapperY);
4886 };
4887
4888 // rename attributes
4889 wrapper.xSetter = function(value) {
4890 wrapper.x = value; // for animation getter
4891 if (alignFactor) {
4892 value -= alignFactor * ((width || bBox.width) + 2 * padding);
4893 }
4894 wrapperX = Math.round(value);
4895 wrapper.attr('translateX', wrapperX);
4896 };
4897 wrapper.ySetter = function(value) {
4898 wrapperY = wrapper.y = Math.round(value);
4899 wrapper.attr('translateY', wrapperY);
4900 };
4901
4902 // Redirect certain methods to either the box or the text
4903 var baseCss = wrapper.css;
4904 return extend(wrapper, {
4905 /**
4906 * Pick up some properties and apply them to the text instead of the wrapper
4907 */
4908 css: function(styles) {
4909 if (styles) {
4910 var textStyles = {};
4911 styles = merge(styles); // create a copy to avoid altering the original object (#537)
4912 each(wrapper.textProps, function(prop) {
4913 if (styles[prop] !== undefined) {
4914 textStyles[prop] = styles[prop];
4915 delete styles[prop];
4916 }
4917 });
4918 text.css(textStyles);
4919 }
4920 return baseCss.call(wrapper, styles);
4921 },
4922 /**
4923 * Return the bounding box of the box, not the group
4924 */
4925 getBBox: function() {
4926 return {
4927 width: bBox.width + 2 * padding,
4928 height: bBox.height + 2 * padding,
4929 x: bBox.x - padding,
4930 y: bBox.y - padding
4931 };
4932 },
4933
4934 /**
4935 * Apply the shadow to the box
4936 */
4937 shadow: function(b) {
4938 if (b) {
4939 updateBoxSize();
4940 if (box) {
4941 box.shadow(b);
4942 }
4943 }
4944 return wrapper;
4945 },
4946
4947 /**
4948 * Destroy and release memory.
4949 */
4950 destroy: function() {
4951
4952 // Added by button implementation
4953 removeEvent(wrapper.element, 'mouseenter');
4954 removeEvent(wrapper.element, 'mouseleave');
4955
4956 if (text) {
4957 text = text.destroy();
4958 }
4959 if (box) {
4960 box = box.destroy();
4961 }
4962 // Call base implementation to destroy the rest
4963 SVGElement.prototype.destroy.call(wrapper);
4964
4965 // Release local pointers (#1298)
4966 wrapper = renderer = updateBoxSize = updateTextPadding = boxAttr = null;
4967 }
4968 });
4969 }
4970 }; // end SVGRenderer
4971
4972
4973 // general renderer
4974 H.Renderer = SVGRenderer;
4975
4976 }(Highcharts));
4977 (function(H) {
4978 /**
4979 * (c) 2010-2016 Torstein Honsi
4980 *
4981 * License: www.highcharts.com/license
4982 */
4983 'use strict';
4984 var attr = H.attr,
4985 createElement = H.createElement,
4986 css = H.css,
4987 defined = H.defined,
4988 each = H.each,
4989 extend = H.extend,
4990 isFirefox = H.isFirefox,
4991 isMS = H.isMS,
4992 isWebKit = H.isWebKit,
4993 pInt = H.pInt,
4994 SVGElement = H.SVGElement,
4995 SVGRenderer = H.SVGRenderer,
4996 win = H.win,
4997 wrap = H.wrap;
4998
4999 // extend SvgElement for useHTML option
5000 extend(SVGElement.prototype, {
5001 /**
5002 * Apply CSS to HTML elements. This is used in text within SVG rendering and
5003 * by the VML renderer
5004 */
5005 htmlCss: function(styles) {
5006 var wrapper = this,
5007 element = wrapper.element,
5008 textWidth = styles && element.tagName === 'SPAN' && styles.width;
5009
5010 if (textWidth) {
5011 delete styles.width;
5012 wrapper.textWidth = textWidth;
5013 wrapper.updateTransform();
5014 }
5015 if (styles && styles.textOverflow === 'ellipsis') {
5016 styles.whiteSpace = 'nowrap';
5017 styles.overflow = 'hidden';
5018 }
5019 wrapper.styles = extend(wrapper.styles, styles);
5020 css(wrapper.element, styles);
5021
5022 return wrapper;
5023 },
5024
5025 /**
5026 * VML and useHTML method for calculating the bounding box based on offsets
5027 * @param {Boolean} refresh Whether to force a fresh value from the DOM or to
5028 * use the cached value
5029 *
5030 * @return {Object} A hash containing values for x, y, width and height
5031 */
5032
5033 htmlGetBBox: function() {
5034 var wrapper = this,
5035 element = wrapper.element;
5036
5037 // faking getBBox in exported SVG in legacy IE
5038 // faking getBBox in exported SVG in legacy IE (is this a duplicate of the fix for #1079?)
5039 if (element.nodeName === 'text') {
5040 element.style.position = 'absolute';
5041 }
5042
5043 return {
5044 x: element.offsetLeft,
5045 y: element.offsetTop,
5046 width: element.offsetWidth,
5047 height: element.offsetHeight
5048 };
5049 },
5050
5051 /**
5052 * VML override private method to update elements based on internal
5053 * properties based on SVG transform
5054 */
5055 htmlUpdateTransform: function() {
5056 // aligning non added elements is expensive
5057 if (!this.added) {
5058 this.alignOnAdd = true;
5059 return;
5060 }
5061
5062 var wrapper = this,
5063 renderer = wrapper.renderer,
5064 elem = wrapper.element,
5065 translateX = wrapper.translateX || 0,
5066 translateY = wrapper.translateY || 0,
5067 x = wrapper.x || 0,
5068 y = wrapper.y || 0,
5069 align = wrapper.textAlign || 'left',
5070 alignCorrection = {
5071 left: 0,
5072 center: 0.5,
5073 right: 1
5074 }[align],
5075 styles = wrapper.styles;
5076
5077 // apply translate
5078 css(elem, {
5079 marginLeft: translateX,
5080 marginTop: translateY
5081 });
5082
5083
5084 if (wrapper.shadows) { // used in labels/tooltip
5085 each(wrapper.shadows, function(shadow) {
5086 css(shadow, {
5087 marginLeft: translateX + 1,
5088 marginTop: translateY + 1
5089 });
5090 });
5091 }
5092
5093
5094 // apply inversion
5095 if (wrapper.inverted) { // wrapper is a group
5096 each(elem.childNodes, function(child) {
5097 renderer.invertChild(child, elem);
5098 });
5099 }
5100
5101 if (elem.tagName === 'SPAN') {
5102
5103 var rotation = wrapper.rotation,
5104 baseline,
5105 textWidth = pInt(wrapper.textWidth),
5106 whiteSpace = styles && styles.whiteSpace,
5107 currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth, wrapper.textAlign].join(',');
5108
5109 if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed
5110
5111
5112 baseline = renderer.fontMetrics(elem.style.fontSize).b;
5113
5114 // Renderer specific handling of span rotation
5115 if (defined(rotation)) {
5116 wrapper.setSpanRotation(rotation, alignCorrection, baseline);
5117 }
5118
5119 // Reset multiline/ellipsis in order to read width (#4928, #5417)
5120 css(elem, {
5121 width: '',
5122 whiteSpace: whiteSpace || 'nowrap'
5123 });
5124
5125 // Update textWidth
5126 if (elem.offsetWidth > textWidth && /[ \-]/.test(elem.textContent || elem.innerText)) { // #983, #1254
5127 css(elem, {
5128 width: textWidth + 'px',
5129 display: 'block',
5130 whiteSpace: whiteSpace || 'normal' // #3331
5131 });
5132 }
5133
5134
5135 wrapper.getSpanCorrection(elem.offsetWidth, baseline, alignCorrection, rotation, align);
5136 }
5137
5138 // apply position with correction
5139 css(elem, {
5140 left: (x + (wrapper.xCorr || 0)) + 'px',
5141 top: (y + (wrapper.yCorr || 0)) + 'px'
5142 });
5143
5144 // force reflow in webkit to apply the left and top on useHTML element (#1249)
5145 if (isWebKit) {
5146 baseline = elem.offsetHeight; // assigned to baseline for lint purpose
5147 }
5148
5149 // record current text transform
5150 wrapper.cTT = currentTextTransform;
5151 }
5152 },
5153
5154 /**
5155 * Set the rotation of an individual HTML span
5156 */
5157 setSpanRotation: function(rotation, alignCorrection, baseline) {
5158 var rotationStyle = {},
5159 cssTransformKey = isMS ? '-ms-transform' : isWebKit ? '-webkit-transform' : isFirefox ? 'MozTransform' : win.opera ? '-o-transform' : '';
5160
5161 rotationStyle[cssTransformKey] = rotationStyle.transform = 'rotate(' + rotation + 'deg)';
5162 rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] = rotationStyle.transformOrigin = (alignCorrection * 100) + '% ' + baseline + 'px';
5163 css(this.element, rotationStyle);
5164 },
5165
5166 /**
5167 * Get the correction in X and Y positioning as the element is rotated.
5168 */
5169 getSpanCorrection: function(width, baseline, alignCorrection) {
5170 this.xCorr = -width * alignCorrection;
5171 this.yCorr = -baseline;
5172 }
5173 });
5174
5175 // Extend SvgRenderer for useHTML option.
5176 extend(SVGRenderer.prototype, {
5177 /**
5178 * Create HTML text node. This is used by the VML renderer as well as the SVG
5179 * renderer through the useHTML option.
5180 *
5181 * @param {String} str
5182 * @param {Number} x
5183 * @param {Number} y
5184 */
5185 html: function(str, x, y) {
5186 var wrapper = this.createElement('span'),
5187 element = wrapper.element,
5188 renderer = wrapper.renderer,
5189 isSVG = renderer.isSVG,
5190 addSetters = function(element, style) {
5191 // These properties are set as attributes on the SVG group, and as
5192 // identical CSS properties on the div. (#3542)
5193 each(['display', 'opacity', 'visibility'], function(prop) {
5194 wrap(element, prop + 'Setter', function(proceed, value, key, elem) {
5195 proceed.call(this, value, key, elem);
5196 style[key] = value;
5197 });
5198 });
5199 };
5200
5201 // Text setter
5202 wrapper.textSetter = function(value) {
5203 if (value !== element.innerHTML) {
5204 delete this.bBox;
5205 }
5206 element.innerHTML = this.textStr = value;
5207 wrapper.htmlUpdateTransform();
5208 };
5209
5210 // Add setters for the element itself (#4938)
5211 if (isSVG) { // #4938, only for HTML within SVG
5212 addSetters(wrapper, wrapper.element.style);
5213 }
5214
5215 // Various setters which rely on update transform
5216 wrapper.xSetter = wrapper.ySetter = wrapper.alignSetter = wrapper.rotationSetter = function(value, key) {
5217 if (key === 'align') {
5218 key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML.
5219 }
5220 wrapper[key] = value;
5221 wrapper.htmlUpdateTransform();
5222 };
5223
5224 // Set the default attributes
5225 wrapper
5226 .attr({
5227 text: str,
5228 x: Math.round(x),
5229 y: Math.round(y)
5230 })
5231 .css({
5232
5233 fontFamily: this.style.fontFamily,
5234 fontSize: this.style.fontSize,
5235
5236 position: 'absolute'
5237 });
5238
5239 // Keep the whiteSpace style outside the wrapper.styles collection
5240 element.style.whiteSpace = 'nowrap';
5241
5242 // Use the HTML specific .css method
5243 wrapper.css = wrapper.htmlCss;
5244
5245 // This is specific for HTML within SVG
5246 if (isSVG) {
5247 wrapper.add = function(svgGroupWrapper) {
5248
5249 var htmlGroup,
5250 container = renderer.box.parentNode,
5251 parentGroup,
5252 parents = [];
5253
5254 this.parentGroup = svgGroupWrapper;
5255
5256 // Create a mock group to hold the HTML elements
5257 if (svgGroupWrapper) {
5258 htmlGroup = svgGroupWrapper.div;
5259 if (!htmlGroup) {
5260
5261 // Read the parent chain into an array and read from top down
5262 parentGroup = svgGroupWrapper;
5263 while (parentGroup) {
5264
5265 parents.push(parentGroup);
5266
5267 // Move up to the next parent group
5268 parentGroup = parentGroup.parentGroup;
5269 }
5270
5271 // Ensure dynamically updating position when any parent is translated
5272 each(parents.reverse(), function(parentGroup) {
5273 var htmlGroupStyle,
5274 cls = attr(parentGroup.element, 'class');
5275
5276 if (cls) {
5277 cls = {
5278 className: cls
5279 };
5280 } // else null
5281
5282 // Create a HTML div and append it to the parent div to emulate
5283 // the SVG group structure
5284 htmlGroup = parentGroup.div = parentGroup.div || createElement('div', cls, {
5285 position: 'absolute',
5286 left: (parentGroup.translateX || 0) + 'px',
5287 top: (parentGroup.translateY || 0) + 'px',
5288 display: parentGroup.display,
5289 opacity: parentGroup.opacity, // #5075
5290 pointerEvents: parentGroup.styles && parentGroup.styles.pointerEvents // #5595
5291 }, htmlGroup || container); // the top group is appended to container
5292
5293 // Shortcut
5294 htmlGroupStyle = htmlGroup.style;
5295
5296 // Set listeners to update the HTML div's position whenever the SVG group
5297 // position is changed
5298 extend(parentGroup, {
5299 translateXSetter: function(value, key) {
5300 htmlGroupStyle.left = value + 'px';
5301 parentGroup[key] = value;
5302 parentGroup.doTransform = true;
5303 },
5304 translateYSetter: function(value, key) {
5305 htmlGroupStyle.top = value + 'px';
5306 parentGroup[key] = value;
5307 parentGroup.doTransform = true;
5308 }
5309 });
5310 addSetters(parentGroup, htmlGroupStyle);
5311 });
5312
5313 }
5314 } else {
5315 htmlGroup = container;
5316 }
5317
5318 htmlGroup.appendChild(element);
5319
5320 // Shared with VML:
5321 wrapper.added = true;
5322 if (wrapper.alignOnAdd) {
5323 wrapper.htmlUpdateTransform();
5324 }
5325
5326 return wrapper;
5327 };
5328 }
5329 return wrapper;
5330 }
5331 });
5332
5333 }(Highcharts));
5334 (function(H) {
5335 /**
5336 * (c) 2010-2016 Torstein Honsi
5337 *
5338 * License: www.highcharts.com/license
5339 */
5340 'use strict';
5341
5342 var VMLRenderer,
5343 VMLRendererExtension,
5344 VMLElement,
5345
5346 createElement = H.createElement,
5347 css = H.css,
5348 defined = H.defined,
5349 deg2rad = H.deg2rad,
5350 discardElement = H.discardElement,
5351 doc = H.doc,
5352 each = H.each,
5353 erase = H.erase,
5354 extend = H.extend,
5355 extendClass = H.extendClass,
5356 isArray = H.isArray,
5357 isNumber = H.isNumber,
5358 isObject = H.isObject,
5359 merge = H.merge,
5360 noop = H.noop,
5361 pick = H.pick,
5362 pInt = H.pInt,
5363 svg = H.svg,
5364 SVGElement = H.SVGElement,
5365 SVGRenderer = H.SVGRenderer,
5366 win = H.win;
5367
5368 /* ****************************************************************************
5369 * *
5370 * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE *
5371 * *
5372 * For applications and websites that don't need IE support, like platform *
5373 * targeted mobile apps and web apps, this code can be removed. *
5374 * *
5375 *****************************************************************************/
5376
5377 /**
5378 * @constructor
5379 */
5380 if (!svg) {
5381
5382 /**
5383 * The VML element wrapper.
5384 */
5385 VMLElement = {
5386
5387 docMode8: doc && doc.documentMode === 8,
5388
5389 /**
5390 * Initialize a new VML element wrapper. It builds the markup as a string
5391 * to minimize DOM traffic.
5392 * @param {Object} renderer
5393 * @param {Object} nodeName
5394 */
5395 init: function(renderer, nodeName) {
5396 var wrapper = this,
5397 markup = ['<', nodeName, ' filled="f" stroked="f"'],
5398 style = ['position: ', 'absolute', ';'],
5399 isDiv = nodeName === 'div';
5400
5401 // divs and shapes need size
5402 if (nodeName === 'shape' || isDiv) {
5403 style.push('left:0;top:0;width:1px;height:1px;');
5404 }
5405 style.push('visibility: ', isDiv ? 'hidden' : 'visible');
5406
5407 markup.push(' style="', style.join(''), '"/>');
5408
5409 // create element with default attributes and style
5410 if (nodeName) {
5411 markup = isDiv || nodeName === 'span' || nodeName === 'img' ?
5412 markup.join('') :
5413 renderer.prepVML(markup);
5414 wrapper.element = createElement(markup);
5415 }
5416
5417 wrapper.renderer = renderer;
5418 },
5419
5420 /**
5421 * Add the node to the given parent
5422 * @param {Object} parent
5423 */
5424 add: function(parent) {
5425 var wrapper = this,
5426 renderer = wrapper.renderer,
5427 element = wrapper.element,
5428 box = renderer.box,
5429 inverted = parent && parent.inverted,
5430
5431 // get the parent node
5432 parentNode = parent ?
5433 parent.element || parent :
5434 box;
5435
5436 if (parent) {
5437 this.parentGroup = parent;
5438 }
5439
5440 // if the parent group is inverted, apply inversion on all children
5441 if (inverted) { // only on groups
5442 renderer.invertChild(element, parentNode);
5443 }
5444
5445 // append it
5446 parentNode.appendChild(element);
5447
5448 // align text after adding to be able to read offset
5449 wrapper.added = true;
5450 if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) {
5451 wrapper.updateTransform();
5452 }
5453
5454 // fire an event for internal hooks
5455 if (wrapper.onAdd) {
5456 wrapper.onAdd();
5457 }
5458
5459 // IE8 Standards can't set the class name before the element is appended
5460 if (this.className) {
5461 this.attr('class', this.className);
5462 }
5463
5464 return wrapper;
5465 },
5466
5467 /**
5468 * VML always uses htmlUpdateTransform
5469 */
5470 updateTransform: SVGElement.prototype.htmlUpdateTransform,
5471
5472 /**
5473 * Set the rotation of a span with oldIE's filter
5474 */
5475 setSpanRotation: function() {
5476 // Adjust for alignment and rotation. Rotation of useHTML content is not yet implemented
5477 // but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+
5478 // has support for CSS3 transform. The getBBox method also needs to be updated
5479 // to compensate for the rotation, like it currently does for SVG.
5480 // Test case: http://jsfiddle.net/highcharts/Ybt44/
5481
5482 var rotation = this.rotation,
5483 costheta = Math.cos(rotation * deg2rad),
5484 sintheta = Math.sin(rotation * deg2rad);
5485
5486 css(this.element, {
5487 filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta,
5488 ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta,
5489 ', sizingMethod=\'auto expand\')'
5490 ].join('') : 'none'
5491 });
5492 },
5493
5494 /**
5495 * Get the positioning correction for the span after rotating.
5496 */
5497 getSpanCorrection: function(width, baseline, alignCorrection, rotation, align) {
5498
5499 var costheta = rotation ? Math.cos(rotation * deg2rad) : 1,
5500 sintheta = rotation ? Math.sin(rotation * deg2rad) : 0,
5501 height = pick(this.elemHeight, this.element.offsetHeight),
5502 quad,
5503 nonLeft = align && align !== 'left';
5504
5505 // correct x and y
5506 this.xCorr = costheta < 0 && -width;
5507 this.yCorr = sintheta < 0 && -height;
5508
5509 // correct for baseline and corners spilling out after rotation
5510 quad = costheta * sintheta < 0;
5511 this.xCorr += sintheta * baseline * (quad ? 1 - alignCorrection : alignCorrection);
5512 this.yCorr -= costheta * baseline * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1);
5513 // correct for the length/height of the text
5514 if (nonLeft) {
5515 this.xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1);
5516 if (rotation) {
5517 this.yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1);
5518 }
5519 css(this.element, {
5520 textAlign: align
5521 });
5522 }
5523 },
5524
5525 /**
5526 * Converts a subset of an SVG path definition to its VML counterpart. Takes an array
5527 * as the parameter and returns a string.
5528 */
5529 pathToVML: function(value) {
5530 // convert paths
5531 var i = value.length,
5532 path = [];
5533
5534 while (i--) {
5535
5536 // Multiply by 10 to allow subpixel precision.
5537 // Substracting half a pixel seems to make the coordinates
5538 // align with SVG, but this hasn't been tested thoroughly
5539 if (isNumber(value[i])) {
5540 path[i] = Math.round(value[i] * 10) - 5;
5541 } else if (value[i] === 'Z') { // close the path
5542 path[i] = 'x';
5543 } else {
5544 path[i] = value[i];
5545
5546 // When the start X and end X coordinates of an arc are too close,
5547 // they are rounded to the same value above. In this case, substract or
5548 // add 1 from the end X and Y positions. #186, #760, #1371, #1410.
5549 if (value.isArc && (value[i] === 'wa' || value[i] === 'at')) {
5550 // Start and end X
5551 if (path[i + 5] === path[i + 7]) {
5552 path[i + 7] += value[i + 7] > value[i + 5] ? 1 : -1;
5553 }
5554 // Start and end Y
5555 if (path[i + 6] === path[i + 8]) {
5556 path[i + 8] += value[i + 8] > value[i + 6] ? 1 : -1;
5557 }
5558 }
5559 }
5560 }
5561
5562
5563 // Loop up again to handle path shortcuts (#2132)
5564 /*while (i++ < path.length) {
5565 if (path[i] === 'H') { // horizontal line to
5566 path[i] = 'L';
5567 path.splice(i + 2, 0, path[i - 1]);
5568 } else if (path[i] === 'V') { // vertical line to
5569 path[i] = 'L';
5570 path.splice(i + 1, 0, path[i - 2]);
5571 }
5572 }*/
5573 return path.join(' ') || 'x';
5574 },
5575
5576 /**
5577 * Set the element's clipping to a predefined rectangle
5578 *
5579 * @param {String} id The id of the clip rectangle
5580 */
5581 clip: function(clipRect) {
5582 var wrapper = this,
5583 clipMembers,
5584 cssRet;
5585
5586 if (clipRect) {
5587 clipMembers = clipRect.members;
5588 erase(clipMembers, wrapper); // Ensure unique list of elements (#1258)
5589 clipMembers.push(wrapper);
5590 wrapper.destroyClip = function() {
5591 erase(clipMembers, wrapper);
5592 };
5593 cssRet = clipRect.getCSS(wrapper);
5594
5595 } else {
5596 if (wrapper.destroyClip) {
5597 wrapper.destroyClip();
5598 }
5599 cssRet = {
5600 clip: wrapper.docMode8 ? 'inherit' : 'rect(auto)'
5601 }; // #1214
5602 }
5603
5604 return wrapper.css(cssRet);
5605
5606 },
5607
5608 /**
5609 * Set styles for the element
5610 * @param {Object} styles
5611 */
5612 css: SVGElement.prototype.htmlCss,
5613
5614 /**
5615 * Removes a child either by removeChild or move to garbageBin.
5616 * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not.
5617 */
5618 safeRemoveChild: function(element) {
5619 // discardElement will detach the node from its parent before attaching it
5620 // to the garbage bin. Therefore it is important that the node is attached and have parent.
5621 if (element.parentNode) {
5622 discardElement(element);
5623 }
5624 },
5625
5626 /**
5627 * Extend element.destroy by removing it from the clip members array
5628 */
5629 destroy: function() {
5630 if (this.destroyClip) {
5631 this.destroyClip();
5632 }
5633
5634 return SVGElement.prototype.destroy.apply(this);
5635 },
5636
5637 /**
5638 * Add an event listener. VML override for normalizing event parameters.
5639 * @param {String} eventType
5640 * @param {Function} handler
5641 */
5642 on: function(eventType, handler) {
5643 // simplest possible event model for internal use
5644 this.element['on' + eventType] = function() {
5645 var evt = win.event;
5646 evt.target = evt.srcElement;
5647 handler(evt);
5648 };
5649 return this;
5650 },
5651
5652 /**
5653 * In stacked columns, cut off the shadows so that they don't overlap
5654 */
5655 cutOffPath: function(path, length) {
5656
5657 var len;
5658
5659 path = path.split(/[ ,]/); // The extra comma tricks the trailing comma remover in "gulp scripts" task
5660 len = path.length;
5661
5662 if (len === 9 || len === 11) {
5663 path[len - 4] = path[len - 2] = pInt(path[len - 2]) - 10 * length;
5664 }
5665 return path.join(' ');
5666 },
5667
5668 /**
5669 * Apply a drop shadow by copying elements and giving them different strokes
5670 * @param {Boolean|Object} shadowOptions
5671 */
5672 shadow: function(shadowOptions, group, cutOff) {
5673 var shadows = [],
5674 i,
5675 element = this.element,
5676 renderer = this.renderer,
5677 shadow,
5678 elemStyle = element.style,
5679 markup,
5680 path = element.path,
5681 strokeWidth,
5682 modifiedPath,
5683 shadowWidth,
5684 shadowElementOpacity;
5685
5686 // some times empty paths are not strings
5687 if (path && typeof path.value !== 'string') {
5688 path = 'x';
5689 }
5690 modifiedPath = path;
5691
5692 if (shadowOptions) {
5693 shadowWidth = pick(shadowOptions.width, 3);
5694 shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth;
5695 for (i = 1; i <= 3; i++) {
5696
5697 strokeWidth = (shadowWidth * 2) + 1 - (2 * i);
5698
5699 // Cut off shadows for stacked column items
5700 if (cutOff) {
5701 modifiedPath = this.cutOffPath(path.value, strokeWidth + 0.5);
5702 }
5703
5704 markup = ['<shape isShadow="true" strokeweight="', strokeWidth,
5705 '" filled="false" path="', modifiedPath,
5706 '" coordsize="10 10" style="', element.style.cssText, '" />'
5707 ];
5708
5709 shadow = createElement(renderer.prepVML(markup),
5710 null, {
5711 left: pInt(elemStyle.left) + pick(shadowOptions.offsetX, 1),
5712 top: pInt(elemStyle.top) + pick(shadowOptions.offsetY, 1)
5713 }
5714 );
5715 if (cutOff) {
5716 shadow.cutOff = strokeWidth + 1;
5717 }
5718
5719 // apply the opacity
5720 markup = ['<stroke color="', shadowOptions.color || '#000000', '" opacity="', shadowElementOpacity * i, '"/>'];
5721 createElement(renderer.prepVML(markup), null, null, shadow);
5722
5723
5724 // insert it
5725 if (group) {
5726 group.element.appendChild(shadow);
5727 } else {
5728 element.parentNode.insertBefore(shadow, element);
5729 }
5730
5731 // record it
5732 shadows.push(shadow);
5733
5734 }
5735
5736 this.shadows = shadows;
5737 }
5738 return this;
5739 },
5740 updateShadows: noop, // Used in SVG only
5741
5742 setAttr: function(key, value) {
5743 if (this.docMode8) { // IE8 setAttribute bug
5744 this.element[key] = value;
5745 } else {
5746 this.element.setAttribute(key, value);
5747 }
5748 },
5749 classSetter: function(value) {
5750 // IE8 Standards mode has problems retrieving the className unless set like this.
5751 // IE8 Standards can't set the class name before the element is appended.
5752 (this.added ? this.element : this).className = value;
5753 },
5754 dashstyleSetter: function(value, key, element) {
5755 var strokeElem = element.getElementsByTagName('stroke')[0] ||
5756 createElement(this.renderer.prepVML(['<stroke/>']), null, null, element);
5757 strokeElem[key] = value || 'solid';
5758 this[key] = value;
5759 /* because changing stroke-width will change the dash length
5760 and cause an epileptic effect */
5761 },
5762 dSetter: function(value, key, element) {
5763 var i,
5764 shadows = this.shadows;
5765 value = value || [];
5766 this.d = value.join && value.join(' '); // used in getter for animation
5767
5768 element.path = value = this.pathToVML(value);
5769
5770 // update shadows
5771 if (shadows) {
5772 i = shadows.length;
5773 while (i--) {
5774 shadows[i].path = shadows[i].cutOff ? this.cutOffPath(value, shadows[i].cutOff) : value;
5775 }
5776 }
5777 this.setAttr(key, value);
5778 },
5779 fillSetter: function(value, key, element) {
5780 var nodeName = element.nodeName;
5781 if (nodeName === 'SPAN') { // text color
5782 element.style.color = value;
5783 } else if (nodeName !== 'IMG') { // #1336
5784 element.filled = value !== 'none';
5785 this.setAttr('fillcolor', this.renderer.color(value, element, key, this));
5786 }
5787 },
5788 'fill-opacitySetter': function(value, key, element) {
5789 createElement(
5790 this.renderer.prepVML(['<', key.split('-')[0], ' opacity="', value, '"/>']),
5791 null,
5792 null,
5793 element
5794 );
5795 },
5796 opacitySetter: noop, // Don't bother - animation is too slow and filters introduce artifacts
5797 rotationSetter: function(value, key, element) {
5798 var style = element.style;
5799 this[key] = style[key] = value; // style is for #1873
5800
5801 // Correction for the 1x1 size of the shape container. Used in gauge needles.
5802 style.left = -Math.round(Math.sin(value * deg2rad) + 1) + 'px';
5803 style.top = Math.round(Math.cos(value * deg2rad)) + 'px';
5804 },
5805 strokeSetter: function(value, key, element) {
5806 this.setAttr('strokecolor', this.renderer.color(value, element, key, this));
5807 },
5808 'stroke-widthSetter': function(value, key, element) {
5809 element.stroked = !!value; // VML "stroked" attribute
5810 this[key] = value; // used in getter, issue #113
5811 if (isNumber(value)) {
5812 value += 'px';
5813 }
5814 this.setAttr('strokeweight', value);
5815 },
5816 titleSetter: function(value, key) {
5817 this.setAttr(key, value);
5818 },
5819 visibilitySetter: function(value, key, element) {
5820
5821 // Handle inherited visibility
5822 if (value === 'inherit') {
5823 value = 'visible';
5824 }
5825
5826 // Let the shadow follow the main element
5827 if (this.shadows) {
5828 each(this.shadows, function(shadow) {
5829 shadow.style[key] = value;
5830 });
5831 }
5832
5833 // Instead of toggling the visibility CSS property, move the div out of the viewport.
5834 // This works around #61 and #586
5835 if (element.nodeName === 'DIV') {
5836 value = value === 'hidden' ? '-999em' : 0;
5837
5838 // In order to redraw, IE7 needs the div to be visible when tucked away
5839 // outside the viewport. So the visibility is actually opposite of
5840 // the expected value. This applies to the tooltip only.
5841 if (!this.docMode8) {
5842 element.style[key] = value ? 'visible' : 'hidden';
5843 }
5844 key = 'top';
5845 }
5846 element.style[key] = value;
5847 },
5848 displaySetter: function(value, key, element) {
5849 element.style[key] = value;
5850 },
5851 xSetter: function(value, key, element) {
5852 this[key] = value; // used in getter
5853
5854 if (key === 'x') {
5855 key = 'left';
5856 } else if (key === 'y') {
5857 key = 'top';
5858 }
5859 /* else {
5860 value = Math.max(0, value); // don't set width or height below zero (#311)
5861 }*/
5862
5863 // clipping rectangle special
5864 if (this.updateClipping) {
5865 this[key] = value; // the key is now 'left' or 'top' for 'x' and 'y'
5866 this.updateClipping();
5867 } else {
5868 // normal
5869 element.style[key] = value;
5870 }
5871 },
5872 zIndexSetter: function(value, key, element) {
5873 element.style[key] = value;
5874 }
5875 };
5876 VMLElement['stroke-opacitySetter'] = VMLElement['fill-opacitySetter'];
5877 H.VMLElement = VMLElement = extendClass(SVGElement, VMLElement);
5878
5879 // Some shared setters
5880 VMLElement.prototype.ySetter =
5881 VMLElement.prototype.widthSetter =
5882 VMLElement.prototype.heightSetter =
5883 VMLElement.prototype.xSetter;
5884
5885
5886 /**
5887 * The VML renderer
5888 */
5889 VMLRendererExtension = { // inherit SVGRenderer
5890
5891 Element: VMLElement,
5892 isIE8: win.navigator.userAgent.indexOf('MSIE 8.0') > -1,
5893
5894
5895 /**
5896 * Initialize the VMLRenderer
5897 * @param {Object} container
5898 * @param {Number} width
5899 * @param {Number} height
5900 */
5901 init: function(container, width, height) {
5902 var renderer = this,
5903 boxWrapper,
5904 box,
5905 css;
5906
5907 renderer.alignedObjects = [];
5908
5909 boxWrapper = renderer.createElement('div')
5910 .css({
5911 position: 'relative'
5912 });
5913 box = boxWrapper.element;
5914 container.appendChild(boxWrapper.element);
5915
5916
5917 // generate the containing box
5918 renderer.isVML = true;
5919 renderer.box = box;
5920 renderer.boxWrapper = boxWrapper;
5921 renderer.gradients = {};
5922 renderer.cache = {}; // Cache for numerical bounding boxes
5923 renderer.cacheKeys = [];
5924 renderer.imgCount = 0;
5925
5926
5927 renderer.setSize(width, height, false);
5928
5929 // The only way to make IE6 and IE7 print is to use a global namespace. However,
5930 // with IE8 the only way to make the dynamic shapes visible in screen and print mode
5931 // seems to be to add the xmlns attribute and the behaviour style inline.
5932 if (!doc.namespaces.hcv) {
5933
5934 doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml');
5935
5936 // Setup default CSS (#2153, #2368, #2384)
5937 css = 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke' +
5938 '{ behavior:url(#default#VML); display: inline-block; } ';
5939 try {
5940 doc.createStyleSheet().cssText = css;
5941 } catch (e) {
5942 doc.styleSheets[0].cssText += css;
5943 }
5944
5945 }
5946 },
5947
5948
5949 /**
5950 * Detect whether the renderer is hidden. This happens when one of the parent elements
5951 * has display: none
5952 */
5953 isHidden: function() {
5954 return !this.box.offsetWidth;
5955 },
5956
5957 /**
5958 * Define a clipping rectangle. In VML it is accomplished by storing the values
5959 * for setting the CSS style to all associated members.
5960 *
5961 * @param {Number} x
5962 * @param {Number} y
5963 * @param {Number} width
5964 * @param {Number} height
5965 */
5966 clipRect: function(x, y, width, height) {
5967
5968 // create a dummy element
5969 var clipRect = this.createElement(),
5970 isObj = isObject(x);
5971
5972 // mimic a rectangle with its style object for automatic updating in attr
5973 return extend(clipRect, {
5974 members: [],
5975 count: 0,
5976 left: (isObj ? x.x : x) + 1,
5977 top: (isObj ? x.y : y) + 1,
5978 width: (isObj ? x.width : width) - 1,
5979 height: (isObj ? x.height : height) - 1,
5980 getCSS: function(wrapper) {
5981 var element = wrapper.element,
5982 nodeName = element.nodeName,
5983 isShape = nodeName === 'shape',
5984 inverted = wrapper.inverted,
5985 rect = this,
5986 top = rect.top - (isShape ? element.offsetTop : 0),
5987 left = rect.left,
5988 right = left + rect.width,
5989 bottom = top + rect.height,
5990 ret = {
5991 clip: 'rect(' +
5992 Math.round(inverted ? left : top) + 'px,' +
5993 Math.round(inverted ? bottom : right) + 'px,' +
5994 Math.round(inverted ? right : bottom) + 'px,' +
5995 Math.round(inverted ? top : left) + 'px)'
5996 };
5997
5998 // issue 74 workaround
5999 if (!inverted && wrapper.docMode8 && nodeName === 'DIV') {
6000 extend(ret, {
6001 width: right + 'px',
6002 height: bottom + 'px'
6003 });
6004 }
6005 return ret;
6006 },
6007
6008 // used in attr and animation to update the clipping of all members
6009 updateClipping: function() {
6010 each(clipRect.members, function(member) {
6011 if (member.element) { // Deleted series, like in stock/members/series-remove demo. Should be removed from members, but this will do.
6012 member.css(clipRect.getCSS(member));
6013 }
6014 });
6015 }
6016 });
6017
6018 },
6019
6020
6021 /**
6022 * Take a color and return it if it's a string, make it a gradient if it's a
6023 * gradient configuration object, and apply opacity.
6024 *
6025 * @param {Object} color The color or config object
6026 */
6027 color: function(color, elem, prop, wrapper) {
6028 var renderer = this,
6029 colorObject,
6030 regexRgba = /^rgba/,
6031 markup,
6032 fillType,
6033 ret = 'none';
6034
6035 // Check for linear or radial gradient
6036 if (color && color.linearGradient) {
6037 fillType = 'gradient';
6038 } else if (color && color.radialGradient) {
6039 fillType = 'pattern';
6040 }
6041
6042
6043 if (fillType) {
6044
6045 var stopColor,
6046 stopOpacity,
6047 gradient = color.linearGradient || color.radialGradient,
6048 x1,
6049 y1,
6050 x2,
6051 y2,
6052 opacity1,
6053 opacity2,
6054 color1,
6055 color2,
6056 fillAttr = '',
6057 stops = color.stops,
6058 firstStop,
6059 lastStop,
6060 colors = [],
6061 addFillNode = function() {
6062 // Add the fill subnode. When colors attribute is used, the meanings of opacity and o:opacity2
6063 // are reversed.
6064 markup = ['<fill colors="' + colors.join(',') + '" opacity="', opacity2, '" o:opacity2="', opacity1,
6065 '" type="', fillType, '" ', fillAttr, 'focus="100%" method="any" />'
6066 ];
6067 createElement(renderer.prepVML(markup), null, null, elem);
6068 };
6069
6070 // Extend from 0 to 1
6071 firstStop = stops[0];
6072 lastStop = stops[stops.length - 1];
6073 if (firstStop[0] > 0) {
6074 stops.unshift([
6075 0,
6076 firstStop[1]
6077 ]);
6078 }
6079 if (lastStop[0] < 1) {
6080 stops.push([
6081 1,
6082 lastStop[1]
6083 ]);
6084 }
6085
6086 // Compute the stops
6087 each(stops, function(stop, i) {
6088 if (regexRgba.test(stop[1])) {
6089 colorObject = H.color(stop[1]);
6090 stopColor = colorObject.get('rgb');
6091 stopOpacity = colorObject.get('a');
6092 } else {
6093 stopColor = stop[1];
6094 stopOpacity = 1;
6095 }
6096
6097 // Build the color attribute
6098 colors.push((stop[0] * 100) + '% ' + stopColor);
6099
6100 // Only start and end opacities are allowed, so we use the first and the last
6101 if (!i) {
6102 opacity1 = stopOpacity;
6103 color2 = stopColor;
6104 } else {
6105 opacity2 = stopOpacity;
6106 color1 = stopColor;
6107 }
6108 });
6109
6110 // Apply the gradient to fills only.
6111 if (prop === 'fill') {
6112
6113 // Handle linear gradient angle
6114 if (fillType === 'gradient') {
6115 x1 = gradient.x1 || gradient[0] || 0;
6116 y1 = gradient.y1 || gradient[1] || 0;
6117 x2 = gradient.x2 || gradient[2] || 0;
6118 y2 = gradient.y2 || gradient[3] || 0;
6119 fillAttr = 'angle="' + (90 - Math.atan(
6120 (y2 - y1) / // y vector
6121 (x2 - x1) // x vector
6122 ) * 180 / Math.PI) + '"';
6123
6124 addFillNode();
6125
6126 // Radial (circular) gradient
6127 } else {
6128
6129 var r = gradient.r,
6130 sizex = r * 2,
6131 sizey = r * 2,
6132 cx = gradient.cx,
6133 cy = gradient.cy,
6134 radialReference = elem.radialReference,
6135 bBox,
6136 applyRadialGradient = function() {
6137 if (radialReference) {
6138 bBox = wrapper.getBBox();
6139 cx += (radialReference[0] - bBox.x) / bBox.width - 0.5;
6140 cy += (radialReference[1] - bBox.y) / bBox.height - 0.5;
6141 sizex *= radialReference[2] / bBox.width;
6142 sizey *= radialReference[2] / bBox.height;
6143 }
6144 fillAttr = 'src="' + H.getOptions().global.VMLRadialGradientURL + '" ' +
6145 'size="' + sizex + ',' + sizey + '" ' +
6146 'origin="0.5,0.5" ' +
6147 'position="' + cx + ',' + cy + '" ' +
6148 'color2="' + color2 + '" ';
6149
6150 addFillNode();
6151 };
6152
6153 // Apply radial gradient
6154 if (wrapper.added) {
6155 applyRadialGradient();
6156 } else {
6157 // We need to know the bounding box to get the size and position right
6158 wrapper.onAdd = applyRadialGradient;
6159 }
6160
6161 // The fill element's color attribute is broken in IE8 standards mode, so we
6162 // need to set the parent shape's fillcolor attribute instead.
6163 ret = color1;
6164 }
6165
6166 // Gradients are not supported for VML stroke, return the first color. #722.
6167 } else {
6168 ret = stopColor;
6169 }
6170
6171 // If the color is an rgba color, split it and add a fill node
6172 // to hold the opacity component
6173 } else if (regexRgba.test(color) && elem.tagName !== 'IMG') {
6174
6175 colorObject = H.color(color);
6176
6177 wrapper[prop + '-opacitySetter'](colorObject.get('a'), prop, elem);
6178
6179 ret = colorObject.get('rgb');
6180
6181
6182 } else {
6183 var propNodes = elem.getElementsByTagName(prop); // 'stroke' or 'fill' node
6184 if (propNodes.length) {
6185 propNodes[0].opacity = 1;
6186 propNodes[0].type = 'solid';
6187 }
6188 ret = color;
6189 }
6190
6191 return ret;
6192 },
6193
6194 /**
6195 * Take a VML string and prepare it for either IE8 or IE6/IE7.
6196 * @param {Array} markup A string array of the VML markup to prepare
6197 */
6198 prepVML: function(markup) {
6199 var vmlStyle = 'display:inline-block;behavior:url(#default#VML);',
6200 isIE8 = this.isIE8;
6201
6202 markup = markup.join('');
6203
6204 if (isIE8) { // add xmlns and style inline
6205 markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />');
6206 if (markup.indexOf('style="') === -1) {
6207 markup = markup.replace('/>', ' style="' + vmlStyle + '" />');
6208 } else {
6209 markup = markup.replace('style="', 'style="' + vmlStyle);
6210 }
6211
6212 } else { // add namespace
6213 markup = markup.replace('<', '<hcv:');
6214 }
6215
6216 return markup;
6217 },
6218
6219 /**
6220 * Create rotated and aligned text
6221 * @param {String} str
6222 * @param {Number} x
6223 * @param {Number} y
6224 */
6225 text: SVGRenderer.prototype.html,
6226
6227 /**
6228 * Create and return a path element
6229 * @param {Array} path
6230 */
6231 path: function(path) {
6232 var attr = {
6233 // subpixel precision down to 0.1 (width and height = 1px)
6234 coordsize: '10 10'
6235 };
6236 if (isArray(path)) {
6237 attr.d = path;
6238 } else if (isObject(path)) { // attributes
6239 extend(attr, path);
6240 }
6241 // create the shape
6242 return this.createElement('shape').attr(attr);
6243 },
6244
6245 /**
6246 * Create and return a circle element. In VML circles are implemented as
6247 * shapes, which is faster than v:oval
6248 * @param {Number} x
6249 * @param {Number} y
6250 * @param {Number} r
6251 */
6252 circle: function(x, y, r) {
6253 var circle = this.symbol('circle');
6254 if (isObject(x)) {
6255 r = x.r;
6256 y = x.y;
6257 x = x.x;
6258 }
6259 circle.isCircle = true; // Causes x and y to mean center (#1682)
6260 circle.r = r;
6261 return circle.attr({
6262 x: x,
6263 y: y
6264 });
6265 },
6266
6267 /**
6268 * Create a group using an outer div and an inner v:group to allow rotating
6269 * and flipping. A simple v:group would have problems with positioning
6270 * child HTML elements and CSS clip.
6271 *
6272 * @param {String} name The name of the group
6273 */
6274 g: function(name) {
6275 var wrapper,
6276 attribs;
6277
6278 // set the class name
6279 if (name) {
6280 attribs = {
6281 'className': 'highcharts-' + name,
6282 'class': 'highcharts-' + name
6283 };
6284 }
6285
6286 // the div to hold HTML and clipping
6287 wrapper = this.createElement('div').attr(attribs);
6288
6289 return wrapper;
6290 },
6291
6292 /**
6293 * VML override to create a regular HTML image
6294 * @param {String} src
6295 * @param {Number} x
6296 * @param {Number} y
6297 * @param {Number} width
6298 * @param {Number} height
6299 */
6300 image: function(src, x, y, width, height) {
6301 var obj = this.createElement('img')
6302 .attr({
6303 src: src
6304 });
6305
6306 if (arguments.length > 1) {
6307 obj.attr({
6308 x: x,
6309 y: y,
6310 width: width,
6311 height: height
6312 });
6313 }
6314 return obj;
6315 },
6316
6317 /**
6318 * For rectangles, VML uses a shape for rect to overcome bugs and rotation problems
6319 */
6320 createElement: function(nodeName) {
6321 return nodeName === 'rect' ? this.symbol(nodeName) : SVGRenderer.prototype.createElement.call(this, nodeName);
6322 },
6323
6324 /**
6325 * In the VML renderer, each child of an inverted div (group) is inverted
6326 * @param {Object} element
6327 * @param {Object} parentNode
6328 */
6329 invertChild: function(element, parentNode) {
6330 var ren = this,
6331 parentStyle = parentNode.style,
6332 imgStyle = element.tagName === 'IMG' && element.style; // #1111
6333
6334 css(element, {
6335 flip: 'x',
6336 left: pInt(parentStyle.width) - (imgStyle ? pInt(imgStyle.top) : 1),
6337 top: pInt(parentStyle.height) - (imgStyle ? pInt(imgStyle.left) : 1),
6338 rotation: -90
6339 });
6340
6341 // Recursively invert child elements, needed for nested composite shapes like box plots and error bars. #1680, #1806.
6342 each(element.childNodes, function(child) {
6343 ren.invertChild(child, element);
6344 });
6345 },
6346
6347 /**
6348 * Symbol definitions that override the parent SVG renderer's symbols
6349 *
6350 */
6351 symbols: {
6352 // VML specific arc function
6353 arc: function(x, y, w, h, options) {
6354 var start = options.start,
6355 end = options.end,
6356 radius = options.r || w || h,
6357 innerRadius = options.innerR,
6358 cosStart = Math.cos(start),
6359 sinStart = Math.sin(start),
6360 cosEnd = Math.cos(end),
6361 sinEnd = Math.sin(end),
6362 ret;
6363
6364 if (end - start === 0) { // no angle, don't show it.
6365 return ['x'];
6366 }
6367
6368 ret = [
6369 'wa', // clockwise arc to
6370 x - radius, // left
6371 y - radius, // top
6372 x + radius, // right
6373 y + radius, // bottom
6374 x + radius * cosStart, // start x
6375 y + radius * sinStart, // start y
6376 x + radius * cosEnd, // end x
6377 y + radius * sinEnd // end y
6378 ];
6379
6380 if (options.open && !innerRadius) {
6381 ret.push(
6382 'e',
6383 'M',
6384 x, // - innerRadius,
6385 y // - innerRadius
6386 );
6387 }
6388
6389 ret.push(
6390 'at', // anti clockwise arc to
6391 x - innerRadius, // left
6392 y - innerRadius, // top
6393 x + innerRadius, // right
6394 y + innerRadius, // bottom
6395 x + innerRadius * cosEnd, // start x
6396 y + innerRadius * sinEnd, // start y
6397 x + innerRadius * cosStart, // end x
6398 y + innerRadius * sinStart, // end y
6399 'x', // finish path
6400 'e' // close
6401 );
6402
6403 ret.isArc = true;
6404 return ret;
6405
6406 },
6407 // Add circle symbol path. This performs significantly faster than v:oval.
6408 circle: function(x, y, w, h, wrapper) {
6409
6410 if (wrapper && defined(wrapper.r)) {
6411 w = h = 2 * wrapper.r;
6412 }
6413
6414 // Center correction, #1682
6415 if (wrapper && wrapper.isCircle) {
6416 x -= w / 2;
6417 y -= h / 2;
6418 }
6419
6420 // Return the path
6421 return [
6422 'wa', // clockwisearcto
6423 x, // left
6424 y, // top
6425 x + w, // right
6426 y + h, // bottom
6427 x + w, // start x
6428 y + h / 2, // start y
6429 x + w, // end x
6430 y + h / 2, // end y
6431 //'x', // finish path
6432 'e' // close
6433 ];
6434 },
6435 /**
6436 * Add rectangle symbol path which eases rotation and omits arcsize problems
6437 * compared to the built-in VML roundrect shape. When borders are not rounded,
6438 * use the simpler square path, else use the callout path without the arrow.
6439 */
6440 rect: function(x, y, w, h, options) {
6441 return SVGRenderer.prototype.symbols[!defined(options) || !options.r ? 'square' : 'callout'].call(0, x, y, w, h, options);
6442 }
6443 }
6444 };
6445 H.VMLRenderer = VMLRenderer = function() {
6446 this.init.apply(this, arguments);
6447 };
6448 VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension);
6449
6450 // general renderer
6451 H.Renderer = VMLRenderer;
6452 }
6453
6454 // This method is used with exporting in old IE, when emulating SVG (see #2314)
6455 SVGRenderer.prototype.measureSpanWidth = function(text, styles) {
6456 var measuringSpan = doc.createElement('span'),
6457 offsetWidth,
6458 textNode = doc.createTextNode(text);
6459
6460 measuringSpan.appendChild(textNode);
6461 css(measuringSpan, styles);
6462 this.box.appendChild(measuringSpan);
6463 offsetWidth = measuringSpan.offsetWidth;
6464 discardElement(measuringSpan); // #2463
6465 return offsetWidth;
6466 };
6467
6468
6469 /* ****************************************************************************
6470 * *
6471 * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE *
6472 * *
6473 *****************************************************************************/
6474
6475
6476 }(Highcharts));
6477 (function(H) {
6478 /**
6479 * (c) 2010-2016 Torstein Honsi
6480 *
6481 * License: www.highcharts.com/license
6482 */
6483 'use strict';
6484 var correctFloat = H.correctFloat,
6485 defined = H.defined,
6486 destroyObjectProperties = H.destroyObjectProperties,
6487 isNumber = H.isNumber,
6488 merge = H.merge,
6489 pick = H.pick,
6490 stop = H.stop,
6491 deg2rad = H.deg2rad;
6492
6493 /**
6494 * The Tick class
6495 */
6496 H.Tick = function(axis, pos, type, noLabel) {
6497 this.axis = axis;
6498 this.pos = pos;
6499 this.type = type || '';
6500 this.isNew = true;
6501
6502 if (!type && !noLabel) {
6503 this.addLabel();
6504 }
6505 };
6506
6507 H.Tick.prototype = {
6508 /**
6509 * Write the tick label
6510 */
6511 addLabel: function() {
6512 var tick = this,
6513 axis = tick.axis,
6514 options = axis.options,
6515 chart = axis.chart,
6516 categories = axis.categories,
6517 names = axis.names,
6518 pos = tick.pos,
6519 labelOptions = options.labels,
6520 str,
6521 tickPositions = axis.tickPositions,
6522 isFirst = pos === tickPositions[0],
6523 isLast = pos === tickPositions[tickPositions.length - 1],
6524 value = categories ?
6525 pick(categories[pos], names[pos], pos) :
6526 pos,
6527 label = tick.label,
6528 tickPositionInfo = tickPositions.info,
6529 dateTimeLabelFormat;
6530
6531 // Set the datetime label format. If a higher rank is set for this position, use that. If not,
6532 // use the general format.
6533 if (axis.isDatetimeAxis && tickPositionInfo) {
6534 dateTimeLabelFormat = options.dateTimeLabelFormats[tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName];
6535 }
6536 // set properties for access in render method
6537 tick.isFirst = isFirst;
6538 tick.isLast = isLast;
6539
6540 // get the string
6541 str = axis.labelFormatter.call({
6542 axis: axis,
6543 chart: chart,
6544 isFirst: isFirst,
6545 isLast: isLast,
6546 dateTimeLabelFormat: dateTimeLabelFormat,
6547 value: axis.isLog ? correctFloat(axis.lin2log(value)) : value
6548 });
6549
6550 // prepare CSS
6551 //css = width && { width: Math.max(1, Math.round(width - 2 * (labelOptions.padding || 10))) + 'px' };
6552
6553 // first call
6554 if (!defined(label)) {
6555
6556 tick.label = label =
6557 defined(str) && labelOptions.enabled ?
6558 chart.renderer.text(
6559 str,
6560 0,
6561 0,
6562 labelOptions.useHTML
6563 )
6564
6565 // without position absolute, IE export sometimes is wrong
6566 .css(merge(labelOptions.style))
6567
6568 .add(axis.labelGroup):
6569 null;
6570 tick.labelLength = label && label.getBBox().width; // Un-rotated length
6571 tick.rotation = 0; // Base value to detect change for new calls to getBBox
6572
6573 // update
6574 } else if (label) {
6575 label.attr({
6576 text: str
6577 });
6578 }
6579 },
6580
6581 /**
6582 * Get the offset height or width of the label
6583 */
6584 getLabelSize: function() {
6585 return this.label ?
6586 this.label.getBBox()[this.axis.horiz ? 'height' : 'width'] :
6587 0;
6588 },
6589
6590 /**
6591 * Handle the label overflow by adjusting the labels to the left and right edge, or
6592 * hide them if they collide into the neighbour label.
6593 */
6594 handleOverflow: function(xy) {
6595 var axis = this.axis,
6596 pxPos = xy.x,
6597 chartWidth = axis.chart.chartWidth,
6598 spacing = axis.chart.spacing,
6599 leftBound = pick(axis.labelLeft, Math.min(axis.pos, spacing[3])),
6600 rightBound = pick(axis.labelRight, Math.max(axis.pos + axis.len, chartWidth - spacing[1])),
6601 label = this.label,
6602 rotation = this.rotation,
6603 factor = {
6604 left: 0,
6605 center: 0.5,
6606 right: 1
6607 }[axis.labelAlign],
6608 labelWidth = label.getBBox().width,
6609 slotWidth = axis.getSlotWidth(),
6610 modifiedSlotWidth = slotWidth,
6611 xCorrection = factor,
6612 goRight = 1,
6613 leftPos,
6614 rightPos,
6615 textWidth,
6616 css = {};
6617
6618 // Check if the label overshoots the chart spacing box. If it does, move it.
6619 // If it now overshoots the slotWidth, add ellipsis.
6620 if (!rotation) {
6621 leftPos = pxPos - factor * labelWidth;
6622 rightPos = pxPos + (1 - factor) * labelWidth;
6623
6624 if (leftPos < leftBound) {
6625 modifiedSlotWidth = xy.x + modifiedSlotWidth * (1 - factor) - leftBound;
6626 } else if (rightPos > rightBound) {
6627 modifiedSlotWidth = rightBound - xy.x + modifiedSlotWidth * factor;
6628 goRight = -1;
6629 }
6630
6631 modifiedSlotWidth = Math.min(slotWidth, modifiedSlotWidth); // #4177
6632 if (modifiedSlotWidth < slotWidth && axis.labelAlign === 'center') {
6633 xy.x += goRight * (slotWidth - modifiedSlotWidth - xCorrection * (slotWidth - Math.min(labelWidth, modifiedSlotWidth)));
6634 }
6635 // If the label width exceeds the available space, set a text width to be
6636 // picked up below. Also, if a width has been set before, we need to set a new
6637 // one because the reported labelWidth will be limited by the box (#3938).
6638 if (labelWidth > modifiedSlotWidth || (axis.autoRotation && (label.styles || {}).width)) {
6639 textWidth = modifiedSlotWidth;
6640 }
6641
6642 // Add ellipsis to prevent rotated labels to be clipped against the edge of the chart
6643 } else if (rotation < 0 && pxPos - factor * labelWidth < leftBound) {
6644 textWidth = Math.round(pxPos / Math.cos(rotation * deg2rad) - leftBound);
6645 } else if (rotation > 0 && pxPos + factor * labelWidth > rightBound) {
6646 textWidth = Math.round((chartWidth - pxPos) / Math.cos(rotation * deg2rad));
6647 }
6648
6649 if (textWidth) {
6650 css.width = textWidth;
6651 if (!(axis.options.labels.style || {}).textOverflow) {
6652 css.textOverflow = 'ellipsis';
6653 }
6654 label.css(css);
6655 }
6656 },
6657
6658 /**
6659 * Get the x and y position for ticks and labels
6660 */
6661 getPosition: function(horiz, pos, tickmarkOffset, old) {
6662 var axis = this.axis,
6663 chart = axis.chart,
6664 cHeight = (old && chart.oldChartHeight) || chart.chartHeight;
6665
6666 return {
6667 x: horiz ?
6668 axis.translate(pos + tickmarkOffset, null, null, old) + axis.transB : axis.left + axis.offset + (axis.opposite ? ((old && chart.oldChartWidth) || chart.chartWidth) - axis.right - axis.left : 0),
6669
6670 y: horiz ?
6671 cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) : cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB
6672 };
6673
6674 },
6675
6676 /**
6677 * Get the x, y position of the tick label
6678 */
6679 getLabelPosition: function(x, y, label, horiz, labelOptions, tickmarkOffset, index, step) {
6680 var axis = this.axis,
6681 transA = axis.transA,
6682 reversed = axis.reversed,
6683 staggerLines = axis.staggerLines,
6684 rotCorr = axis.tickRotCorr || {
6685 x: 0,
6686 y: 0
6687 },
6688 yOffset = labelOptions.y,
6689 line;
6690
6691 if (!defined(yOffset)) {
6692 if (axis.side === 0) {
6693 yOffset = label.rotation ? -8 : -label.getBBox().height;
6694 } else if (axis.side === 2) {
6695 yOffset = rotCorr.y + 8;
6696 } else {
6697 // #3140, #3140
6698 yOffset = Math.cos(label.rotation * deg2rad) * (rotCorr.y - label.getBBox(false, 0).height / 2);
6699 }
6700 }
6701
6702 x = x + labelOptions.x + rotCorr.x - (tickmarkOffset && horiz ?
6703 tickmarkOffset * transA * (reversed ? -1 : 1) : 0);
6704 y = y + yOffset - (tickmarkOffset && !horiz ?
6705 tickmarkOffset * transA * (reversed ? 1 : -1) : 0);
6706
6707 // Correct for staggered labels
6708 if (staggerLines) {
6709 line = (index / (step || 1) % staggerLines);
6710 if (axis.opposite) {
6711 line = staggerLines - line - 1;
6712 }
6713 y += line * (axis.labelOffset / staggerLines);
6714 }
6715
6716 return {
6717 x: x,
6718 y: Math.round(y)
6719 };
6720 },
6721
6722 /**
6723 * Extendible method to return the path of the marker
6724 */
6725 getMarkPath: function(x, y, tickLength, tickWidth, horiz, renderer) {
6726 return renderer.crispLine([
6727 'M',
6728 x,
6729 y,
6730 'L',
6731 x + (horiz ? 0 : -tickLength),
6732 y + (horiz ? tickLength : 0)
6733 ], tickWidth);
6734 },
6735
6736 /**
6737 * Put everything in place
6738 *
6739 * @param index {Number}
6740 * @param old {Boolean} Use old coordinates to prepare an animation into new position
6741 */
6742 render: function(index, old, opacity) {
6743 var tick = this,
6744 axis = tick.axis,
6745 options = axis.options,
6746 chart = axis.chart,
6747 renderer = chart.renderer,
6748 horiz = axis.horiz,
6749 type = tick.type,
6750 label = tick.label,
6751 pos = tick.pos,
6752 labelOptions = options.labels,
6753 gridLine = tick.gridLine,
6754 tickPrefix = type ? type + 'Tick' : 'tick',
6755 tickSize = axis.tickSize(tickPrefix),
6756 gridLinePath,
6757 mark = tick.mark,
6758 isNewMark = !mark,
6759 step = labelOptions.step,
6760 attribs = {},
6761 show = true,
6762 tickmarkOffset = axis.tickmarkOffset,
6763 xy = tick.getPosition(horiz, pos, tickmarkOffset, old),
6764 x = xy.x,
6765 y = xy.y,
6766 reverseCrisp = ((horiz && x === axis.pos + axis.len) || (!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687
6767
6768
6769 var gridPrefix = type ? type + 'Grid' : 'grid',
6770 gridLineWidth = options[gridPrefix + 'LineWidth'],
6771 gridLineColor = options[gridPrefix + 'LineColor'],
6772 dashStyle = options[gridPrefix + 'LineDashStyle'],
6773 tickWidth = pick(options[tickPrefix + 'Width'], !type && axis.isXAxis ? 1 : 0), // X axis defaults to 1
6774 tickColor = options[tickPrefix + 'Color'];
6775
6776
6777 opacity = pick(opacity, 1);
6778 this.isActive = true;
6779
6780 // Create the grid line
6781 if (!gridLine) {
6782
6783 attribs.stroke = gridLineColor;
6784 attribs['stroke-width'] = gridLineWidth;
6785 if (dashStyle) {
6786 attribs.dashstyle = dashStyle;
6787 }
6788
6789 if (!type) {
6790 attribs.zIndex = 1;
6791 }
6792 if (old) {
6793 attribs.opacity = 0;
6794 }
6795 tick.gridLine = gridLine = renderer.path()
6796 .attr(attribs)
6797 .addClass('highcharts-' + (type ? type + '-' : '') + 'grid-line')
6798 .add(axis.gridGroup);
6799 }
6800
6801 // If the parameter 'old' is set, the current call will be followed
6802 // by another call, therefore do not do any animations this time
6803 if (!old && gridLine) {
6804 gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLine.strokeWidth() * reverseCrisp, old, true);
6805 if (gridLinePath) {
6806 gridLine[tick.isNew ? 'attr' : 'animate']({
6807 d: gridLinePath,
6808 opacity: opacity
6809 });
6810 }
6811 }
6812
6813 // create the tick mark
6814 if (tickSize) {
6815
6816 // negate the length
6817 if (axis.opposite) {
6818 tickSize[0] = -tickSize[0];
6819 }
6820
6821 // First time, create it
6822 if (isNewMark) {
6823 tick.mark = mark = renderer.path()
6824 .addClass('highcharts-' + (type ? type + '-' : '') + 'tick')
6825 .add(axis.axisGroup);
6826
6827
6828 mark.attr({
6829 stroke: tickColor,
6830 'stroke-width': tickWidth
6831 });
6832
6833 }
6834 mark[isNewMark ? 'attr' : 'animate']({
6835 d: tick.getMarkPath(x, y, tickSize[0], mark.strokeWidth() * reverseCrisp, horiz, renderer),
6836 opacity: opacity
6837 });
6838
6839 }
6840
6841 // the label is created on init - now move it into place
6842 if (label && isNumber(x)) {
6843 label.xy = xy = tick.getLabelPosition(x, y, label, horiz, labelOptions, tickmarkOffset, index, step);
6844
6845 // Apply show first and show last. If the tick is both first and last, it is
6846 // a single centered tick, in which case we show the label anyway (#2100).
6847 if ((tick.isFirst && !tick.isLast && !pick(options.showFirstLabel, 1)) ||
6848 (tick.isLast && !tick.isFirst && !pick(options.showLastLabel, 1))) {
6849 show = false;
6850
6851 // Handle label overflow and show or hide accordingly
6852 } else if (horiz && !axis.isRadial && !labelOptions.step && !labelOptions.rotation && !old && opacity !== 0) {
6853 tick.handleOverflow(xy);
6854 }
6855
6856 // apply step
6857 if (step && index % step) {
6858 // show those indices dividable by step
6859 show = false;
6860 }
6861
6862 // Set the new position, and show or hide
6863 if (show && isNumber(xy.y)) {
6864 xy.opacity = opacity;
6865 label[tick.isNew ? 'attr' : 'animate'](xy);
6866 } else {
6867 stop(label); // #5332
6868 label.attr('y', -9999); // #1338
6869 }
6870 tick.isNew = false;
6871 }
6872 },
6873
6874 /**
6875 * Destructor for the tick prototype
6876 */
6877 destroy: function() {
6878 destroyObjectProperties(this, this.axis);
6879 }
6880 };
6881
6882 }(Highcharts));
6883 (function(H) {
6884 /**
6885 * (c) 2010-2016 Torstein Honsi
6886 *
6887 * License: www.highcharts.com/license
6888 */
6889 'use strict';
6890 var arrayMax = H.arrayMax,
6891 arrayMin = H.arrayMin,
6892 defined = H.defined,
6893 destroyObjectProperties = H.destroyObjectProperties,
6894 each = H.each,
6895 erase = H.erase,
6896 merge = H.merge,
6897 pick = H.pick;
6898 /*
6899 * The object wrapper for plot lines and plot bands
6900 * @param {Object} options
6901 */
6902 H.PlotLineOrBand = function(axis, options) {
6903 this.axis = axis;
6904
6905 if (options) {
6906 this.options = options;
6907 this.id = options.id;
6908 }
6909 };
6910
6911 H.PlotLineOrBand.prototype = {
6912
6913 /**
6914 * Render the plot line or plot band. If it is already existing,
6915 * move it.
6916 */
6917 render: function() {
6918 var plotLine = this,
6919 axis = plotLine.axis,
6920 horiz = axis.horiz,
6921 options = plotLine.options,
6922 optionsLabel = options.label,
6923 label = plotLine.label,
6924 to = options.to,
6925 from = options.from,
6926 value = options.value,
6927 isBand = defined(from) && defined(to),
6928 isLine = defined(value),
6929 svgElem = plotLine.svgElem,
6930 isNew = !svgElem,
6931 path = [],
6932 addEvent,
6933 eventType,
6934 color = options.color,
6935 zIndex = pick(options.zIndex, 0),
6936 events = options.events,
6937 attribs = {
6938 'class': 'highcharts-plot-' + (isBand ? 'band ' : 'line ') + (options.className || '')
6939 },
6940 groupAttribs = {},
6941 renderer = axis.chart.renderer,
6942 groupName = isBand ? 'bands' : 'lines',
6943 group,
6944 log2lin = axis.log2lin;
6945
6946 // logarithmic conversion
6947 if (axis.isLog) {
6948 from = log2lin(from);
6949 to = log2lin(to);
6950 value = log2lin(value);
6951 }
6952
6953
6954 // Set the presentational attributes
6955 if (isLine) {
6956 attribs = {
6957 stroke: color,
6958 'stroke-width': options.width
6959 };
6960 if (options.dashStyle) {
6961 attribs.dashstyle = options.dashStyle;
6962 }
6963
6964 } else if (isBand) { // plot band
6965 if (color) {
6966 attribs.fill = color;
6967 }
6968 if (options.borderWidth) {
6969 attribs.stroke = options.borderColor;
6970 attribs['stroke-width'] = options.borderWidth;
6971 }
6972 }
6973
6974
6975 // Grouping and zIndex
6976 groupAttribs.zIndex = zIndex;
6977 groupName += '-' + zIndex;
6978
6979 group = axis[groupName];
6980 if (!group) {
6981 axis[groupName] = group = renderer.g('plot-' + groupName)
6982 .attr(groupAttribs).add();
6983 }
6984
6985 // Create the path
6986 if (isNew) {
6987 plotLine.svgElem = svgElem =
6988 renderer
6989 .path()
6990 .attr(attribs).add(group);
6991 }
6992
6993
6994 // Set the path or return
6995 if (isLine) {
6996 path = axis.getPlotLinePath(value, svgElem.strokeWidth());
6997 } else if (isBand) { // plot band
6998 path = axis.getPlotBandPath(from, to, options);
6999 } else {
7000 return;
7001 }
7002
7003 // common for lines and bands
7004 if (isNew && path && path.length) {
7005 svgElem.attr({
7006 d: path
7007 });
7008
7009 // events
7010 if (events) {
7011 addEvent = function(eventType) {
7012 svgElem.on(eventType, function(e) {
7013 events[eventType].apply(plotLine, [e]);
7014 });
7015 };
7016 for (eventType in events) {
7017 addEvent(eventType);
7018 }
7019 }
7020 } else if (svgElem) {
7021 if (path) {
7022 svgElem.show();
7023 svgElem.animate({
7024 d: path
7025 });
7026 } else {
7027 svgElem.hide();
7028 if (label) {
7029 plotLine.label = label = label.destroy();
7030 }
7031 }
7032 }
7033
7034 // the plot band/line label
7035 if (optionsLabel && defined(optionsLabel.text) && path && path.length &&
7036 axis.width > 0 && axis.height > 0 && !path.flat) {
7037 // apply defaults
7038 optionsLabel = merge({
7039 align: horiz && isBand && 'center',
7040 x: horiz ? !isBand && 4 : 10,
7041 verticalAlign: !horiz && isBand && 'middle',
7042 y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4,
7043 rotation: horiz && !isBand && 90
7044 }, optionsLabel);
7045
7046 this.renderLabel(optionsLabel, path, isBand, zIndex);
7047
7048 } else if (label) { // move out of sight
7049 label.hide();
7050 }
7051
7052 // chainable
7053 return plotLine;
7054 },
7055
7056 /**
7057 * Render and align label for plot line or band.
7058 */
7059 renderLabel: function(optionsLabel, path, isBand, zIndex) {
7060 var plotLine = this,
7061 label = plotLine.label,
7062 renderer = plotLine.axis.chart.renderer,
7063 attribs,
7064 xs,
7065 ys,
7066 x,
7067 y;
7068
7069 // add the SVG element
7070 if (!label) {
7071 attribs = {
7072 align: optionsLabel.textAlign || optionsLabel.align,
7073 rotation: optionsLabel.rotation,
7074 'class': 'highcharts-plot-' + (isBand ? 'band' : 'line') + '-label ' + (optionsLabel.className || '')
7075 };
7076
7077 attribs.zIndex = zIndex;
7078
7079 plotLine.label = label = renderer.text(
7080 optionsLabel.text,
7081 0,
7082 0,
7083 optionsLabel.useHTML
7084 )
7085 .attr(attribs)
7086 .add();
7087
7088
7089 label.css(optionsLabel.style);
7090
7091 }
7092
7093 // get the bounding box and align the label
7094 // #3000 changed to better handle choice between plotband or plotline
7095 xs = [path[1], path[4], (isBand ? path[6] : path[1])];
7096 ys = [path[2], path[5], (isBand ? path[7] : path[2])];
7097 x = arrayMin(xs);
7098 y = arrayMin(ys);
7099
7100 label.align(optionsLabel, false, {
7101 x: x,
7102 y: y,
7103 width: arrayMax(xs) - x,
7104 height: arrayMax(ys) - y
7105 });
7106 label.show();
7107 },
7108
7109 /**
7110 * Remove the plot line or band
7111 */
7112 destroy: function() {
7113 // remove it from the lookup
7114 erase(this.axis.plotLinesAndBands, this);
7115
7116 delete this.axis;
7117 destroyObjectProperties(this);
7118 }
7119 };
7120
7121 /**
7122 * Object with members for extending the Axis prototype
7123 * @todo Extend directly instead of adding object to Highcharts first
7124 */
7125
7126 H.AxisPlotLineOrBandExtension = {
7127
7128 /**
7129 * Create the path for a plot band
7130 */
7131 getPlotBandPath: function(from, to) {
7132 var toPath = this.getPlotLinePath(to, null, null, true),
7133 path = this.getPlotLinePath(from, null, null, true);
7134
7135 if (path && toPath) {
7136
7137 // Flat paths don't need labels (#3836)
7138 path.flat = path.toString() === toPath.toString();
7139
7140 path.push(
7141 toPath[4],
7142 toPath[5],
7143 toPath[1],
7144 toPath[2]
7145 );
7146 } else { // outside the axis area
7147 path = null;
7148 }
7149
7150 return path;
7151 },
7152
7153 addPlotBand: function(options) {
7154 return this.addPlotBandOrLine(options, 'plotBands');
7155 },
7156
7157 addPlotLine: function(options) {
7158 return this.addPlotBandOrLine(options, 'plotLines');
7159 },
7160
7161 /**
7162 * Add a plot band or plot line after render time
7163 *
7164 * @param options {Object} The plotBand or plotLine configuration object
7165 */
7166 addPlotBandOrLine: function(options, coll) {
7167 var obj = new H.PlotLineOrBand(this, options).render(),
7168 userOptions = this.userOptions;
7169
7170 if (obj) { // #2189
7171 // Add it to the user options for exporting and Axis.update
7172 if (coll) {
7173 userOptions[coll] = userOptions[coll] || [];
7174 userOptions[coll].push(options);
7175 }
7176 this.plotLinesAndBands.push(obj);
7177 }
7178
7179 return obj;
7180 },
7181
7182 /**
7183 * Remove a plot band or plot line from the chart by id
7184 * @param {Object} id
7185 */
7186 removePlotBandOrLine: function(id) {
7187 var plotLinesAndBands = this.plotLinesAndBands,
7188 options = this.options,
7189 userOptions = this.userOptions,
7190 i = plotLinesAndBands.length;
7191 while (i--) {
7192 if (plotLinesAndBands[i].id === id) {
7193 plotLinesAndBands[i].destroy();
7194 }
7195 }
7196 each([options.plotLines || [], userOptions.plotLines || [], options.plotBands || [], userOptions.plotBands || []], function(arr) {
7197 i = arr.length;
7198 while (i--) {
7199 if (arr[i].id === id) {
7200 erase(arr, arr[i]);
7201 }
7202 }
7203 });
7204 }
7205 };
7206
7207 }(Highcharts));
7208 (function(H) {
7209 /**
7210 * (c) 2010-2016 Torstein Honsi
7211 *
7212 * License: www.highcharts.com/license
7213 */
7214 'use strict';
7215 var addEvent = H.addEvent,
7216 animObject = H.animObject,
7217 arrayMax = H.arrayMax,
7218 arrayMin = H.arrayMin,
7219 AxisPlotLineOrBandExtension = H.AxisPlotLineOrBandExtension,
7220 color = H.color,
7221 correctFloat = H.correctFloat,
7222 defaultOptions = H.defaultOptions,
7223 defined = H.defined,
7224 deg2rad = H.deg2rad,
7225 destroyObjectProperties = H.destroyObjectProperties,
7226 each = H.each,
7227 error = H.error,
7228 extend = H.extend,
7229 fireEvent = H.fireEvent,
7230 format = H.format,
7231 getMagnitude = H.getMagnitude,
7232 grep = H.grep,
7233 inArray = H.inArray,
7234 isArray = H.isArray,
7235 isNumber = H.isNumber,
7236 isString = H.isString,
7237 merge = H.merge,
7238 normalizeTickInterval = H.normalizeTickInterval,
7239 pick = H.pick,
7240 PlotLineOrBand = H.PlotLineOrBand,
7241 removeEvent = H.removeEvent,
7242 splat = H.splat,
7243 syncTimeout = H.syncTimeout,
7244 Tick = H.Tick;
7245 /**
7246 * Create a new axis object
7247 * @param {Object} chart
7248 * @param {Object} options
7249 */
7250 H.Axis = function() {
7251 this.init.apply(this, arguments);
7252 };
7253
7254 H.Axis.prototype = {
7255
7256 /**
7257 * Default options for the X axis - the Y axis has extended defaults
7258 */
7259 defaultOptions: {
7260 // allowDecimals: null,
7261 // alternateGridColor: null,
7262 // categories: [],
7263 dateTimeLabelFormats: {
7264 millisecond: '%H:%M:%S.%L',
7265 second: '%H:%M:%S',
7266 minute: '%H:%M',
7267 hour: '%H:%M',
7268 day: '%e. %b',
7269 week: '%e. %b',
7270 month: '%b \'%y',
7271 year: '%Y'
7272 },
7273 endOnTick: false,
7274 // reversed: false,
7275
7276 labels: {
7277 enabled: true,
7278 // rotation: 0,
7279 // align: 'center',
7280 // step: null,
7281
7282 style: {
7283 color: '#666666',
7284 cursor: 'default',
7285 fontSize: '11px'
7286 },
7287
7288 x: 0
7289 //y: undefined
7290 /*formatter: function () {
7291 return this.value;
7292 },*/
7293 },
7294 //linkedTo: null,
7295 //max: undefined,
7296 //min: undefined,
7297 minPadding: 0.01,
7298 maxPadding: 0.01,
7299 //minRange: null,
7300 //minorTickInterval: null,
7301 minorTickLength: 2,
7302 minorTickPosition: 'outside', // inside or outside
7303 //opposite: false,
7304 //offset: 0,
7305 //plotBands: [{
7306 // events: {},
7307 // zIndex: 1,
7308 // labels: { align, x, verticalAlign, y, style, rotation, textAlign }
7309 //}],
7310 //plotLines: [{
7311 // events: {}
7312 // dashStyle: {}
7313 // zIndex:
7314 // labels: { align, x, verticalAlign, y, style, rotation, textAlign }
7315 //}],
7316 //reversed: false,
7317 // showFirstLabel: true,
7318 // showLastLabel: true,
7319 startOfWeek: 1,
7320 startOnTick: false,
7321 //tickInterval: null,
7322 tickLength: 10,
7323 tickmarkPlacement: 'between', // on or between
7324 tickPixelInterval: 100,
7325 tickPosition: 'outside',
7326 title: {
7327 //text: null,
7328 align: 'middle', // low, middle or high
7329 //margin: 0 for horizontal, 10 for vertical axes,
7330 //rotation: 0,
7331 //side: 'outside',
7332
7333 style: {
7334 color: '#666666'
7335 }
7336
7337 //x: 0,
7338 //y: 0
7339 },
7340 type: 'linear', // linear, logarithmic or datetime
7341 //visible: true
7342
7343 minorGridLineColor: '#f2f2f2',
7344 // minorGridLineDashStyle: null,
7345 minorGridLineWidth: 1,
7346 minorTickColor: '#999999',
7347 //minorTickWidth: 0,
7348 lineColor: '#ccd6eb',
7349 lineWidth: 1,
7350 gridLineColor: '#e6e6e6',
7351 // gridLineDashStyle: 'solid',
7352 // gridLineWidth: 0,
7353 tickColor: '#ccd6eb'
7354 // tickWidth: 1
7355
7356 },
7357
7358 /**
7359 * This options set extends the defaultOptions for Y axes
7360 */
7361 defaultYAxisOptions: {
7362 endOnTick: true,
7363 tickPixelInterval: 72,
7364 showLastLabel: true,
7365 labels: {
7366 x: -8
7367 },
7368 maxPadding: 0.05,
7369 minPadding: 0.05,
7370 startOnTick: true,
7371 title: {
7372 rotation: 270,
7373 text: 'Values'
7374 },
7375 stackLabels: {
7376 enabled: false,
7377 //align: dynamic,
7378 //y: dynamic,
7379 //x: dynamic,
7380 //verticalAlign: dynamic,
7381 //textAlign: dynamic,
7382 //rotation: 0,
7383 formatter: function() {
7384 return H.numberFormat(this.total, -1);
7385 },
7386
7387 style: {
7388 fontSize: '11px',
7389 fontWeight: 'bold',
7390 color: '#000000',
7391 textShadow: '1px 1px contrast, -1px -1px contrast, -1px 1px contrast, 1px -1px contrast'
7392 }
7393
7394 },
7395
7396 gridLineWidth: 1,
7397 lineWidth: 0
7398 // tickWidth: 0
7399
7400 },
7401
7402 /**
7403 * These options extend the defaultOptions for left axes
7404 */
7405 defaultLeftAxisOptions: {
7406 labels: {
7407 x: -15
7408 },
7409 title: {
7410 rotation: 270
7411 }
7412 },
7413
7414 /**
7415 * These options extend the defaultOptions for right axes
7416 */
7417 defaultRightAxisOptions: {
7418 labels: {
7419 x: 15
7420 },
7421 title: {
7422 rotation: 90
7423 }
7424 },
7425
7426 /**
7427 * These options extend the defaultOptions for bottom axes
7428 */
7429 defaultBottomAxisOptions: {
7430 labels: {
7431 autoRotation: [-45],
7432 x: 0
7433 // overflow: undefined,
7434 // staggerLines: null
7435 },
7436 title: {
7437 rotation: 0
7438 }
7439 },
7440 /**
7441 * These options extend the defaultOptions for top axes
7442 */
7443 defaultTopAxisOptions: {
7444 labels: {
7445 autoRotation: [-45],
7446 x: 0
7447 // overflow: undefined
7448 // staggerLines: null
7449 },
7450 title: {
7451 rotation: 0
7452 }
7453 },
7454
7455 /**
7456 * Initialize the axis
7457 */
7458 init: function(chart, userOptions) {
7459
7460
7461 var isXAxis = userOptions.isX,
7462 axis = this;
7463
7464 axis.chart = chart;
7465
7466 // Flag, is the axis horizontal
7467 axis.horiz = chart.inverted ? !isXAxis : isXAxis;
7468
7469 // Flag, isXAxis
7470 axis.isXAxis = isXAxis;
7471 axis.coll = axis.coll || (isXAxis ? 'xAxis' : 'yAxis');
7472
7473 axis.opposite = userOptions.opposite; // needed in setOptions
7474 axis.side = userOptions.side || (axis.horiz ?
7475 (axis.opposite ? 0 : 2) : // top : bottom
7476 (axis.opposite ? 1 : 3)); // right : left
7477
7478 axis.setOptions(userOptions);
7479
7480
7481 var options = this.options,
7482 type = options.type,
7483 isDatetimeAxis = type === 'datetime';
7484
7485 axis.labelFormatter = options.labels.formatter || axis.defaultLabelFormatter; // can be overwritten by dynamic format
7486
7487
7488 // Flag, stagger lines or not
7489 axis.userOptions = userOptions;
7490
7491 //axis.axisTitleMargin = undefined,// = options.title.margin,
7492 axis.minPixelPadding = 0;
7493
7494 axis.reversed = options.reversed;
7495 axis.visible = options.visible !== false;
7496 axis.zoomEnabled = options.zoomEnabled !== false;
7497
7498 // Initial categories
7499 axis.hasNames = type === 'category' || options.categories === true;
7500 axis.categories = options.categories || axis.hasNames;
7501 axis.names = axis.names || []; // Preserve on update (#3830)
7502
7503 // Elements
7504 //axis.axisGroup = undefined;
7505 //axis.gridGroup = undefined;
7506 //axis.axisTitle = undefined;
7507 //axis.axisLine = undefined;
7508
7509 // Shorthand types
7510 axis.isLog = type === 'logarithmic';
7511 axis.isDatetimeAxis = isDatetimeAxis;
7512
7513 // Flag, if axis is linked to another axis
7514 axis.isLinked = defined(options.linkedTo);
7515 // Linked axis.
7516 //axis.linkedParent = undefined;
7517
7518 // Tick positions
7519 //axis.tickPositions = undefined; // array containing predefined positions
7520 // Tick intervals
7521 //axis.tickInterval = undefined;
7522 //axis.minorTickInterval = undefined;
7523
7524
7525 // Major ticks
7526 axis.ticks = {};
7527 axis.labelEdge = [];
7528 // Minor ticks
7529 axis.minorTicks = {};
7530
7531 // List of plotLines/Bands
7532 axis.plotLinesAndBands = [];
7533
7534 // Alternate bands
7535 axis.alternateBands = {};
7536
7537 // Axis metrics
7538 //axis.left = undefined;
7539 //axis.top = undefined;
7540 //axis.width = undefined;
7541 //axis.height = undefined;
7542 //axis.bottom = undefined;
7543 //axis.right = undefined;
7544 //axis.transA = undefined;
7545 //axis.transB = undefined;
7546 //axis.oldTransA = undefined;
7547 axis.len = 0;
7548 //axis.oldMin = undefined;
7549 //axis.oldMax = undefined;
7550 //axis.oldUserMin = undefined;
7551 //axis.oldUserMax = undefined;
7552 //axis.oldAxisLength = undefined;
7553 axis.minRange = axis.userMinRange = options.minRange || options.maxZoom;
7554 axis.range = options.range;
7555 axis.offset = options.offset || 0;
7556
7557
7558 // Dictionary for stacks
7559 axis.stacks = {};
7560 axis.oldStacks = {};
7561 axis.stacksTouched = 0;
7562
7563 // Min and max in the data
7564 //axis.dataMin = undefined,
7565 //axis.dataMax = undefined,
7566
7567 // The axis range
7568 axis.max = null;
7569 axis.min = null;
7570
7571 // User set min and max
7572 //axis.userMin = undefined,
7573 //axis.userMax = undefined,
7574
7575 // Crosshair options
7576 axis.crosshair = pick(options.crosshair, splat(chart.options.tooltip.crosshairs)[isXAxis ? 0 : 1], false);
7577 // Run Axis
7578
7579 var eventType,
7580 events = axis.options.events;
7581
7582 // Register
7583 if (inArray(axis, chart.axes) === -1) { // don't add it again on Axis.update()
7584 if (isXAxis) { // #2713
7585 chart.axes.splice(chart.xAxis.length, 0, axis);
7586 } else {
7587 chart.axes.push(axis);
7588 }
7589
7590 chart[axis.coll].push(axis);
7591 }
7592
7593 axis.series = axis.series || []; // populated by Series
7594
7595 // inverted charts have reversed xAxes as default
7596 if (chart.inverted && isXAxis && axis.reversed === undefined) {
7597 axis.reversed = true;
7598 }
7599
7600 axis.removePlotBand = axis.removePlotBandOrLine;
7601 axis.removePlotLine = axis.removePlotBandOrLine;
7602
7603
7604 // register event listeners
7605 for (eventType in events) {
7606 addEvent(axis, eventType, events[eventType]);
7607 }
7608
7609 // extend logarithmic axis
7610 if (axis.isLog) {
7611 axis.val2lin = axis.log2lin;
7612 axis.lin2val = axis.lin2log;
7613 }
7614 },
7615
7616 /**
7617 * Merge and set options
7618 */
7619 setOptions: function(userOptions) {
7620 this.options = merge(
7621 this.defaultOptions,
7622 this.coll === 'yAxis' && this.defaultYAxisOptions, [this.defaultTopAxisOptions, this.defaultRightAxisOptions,
7623 this.defaultBottomAxisOptions, this.defaultLeftAxisOptions
7624 ][this.side],
7625 merge(
7626 defaultOptions[this.coll], // if set in setOptions (#1053)
7627 userOptions
7628 )
7629 );
7630 },
7631
7632 /**
7633 * The default label formatter. The context is a special config object for the label.
7634 */
7635 defaultLabelFormatter: function() {
7636 var axis = this.axis,
7637 value = this.value,
7638 categories = axis.categories,
7639 dateTimeLabelFormat = this.dateTimeLabelFormat,
7640 numericSymbols = defaultOptions.lang.numericSymbols,
7641 i = numericSymbols && numericSymbols.length,
7642 multi,
7643 ret,
7644 formatOption = axis.options.labels.format,
7645
7646 // make sure the same symbol is added for all labels on a linear axis
7647 numericSymbolDetector = axis.isLog ? value : axis.tickInterval;
7648
7649 if (formatOption) {
7650 ret = format(formatOption, this);
7651
7652 } else if (categories) {
7653 ret = value;
7654
7655 } else if (dateTimeLabelFormat) { // datetime axis
7656 ret = H.dateFormat(dateTimeLabelFormat, value);
7657
7658 } else if (i && numericSymbolDetector >= 1000) {
7659 // Decide whether we should add a numeric symbol like k (thousands) or M (millions).
7660 // If we are to enable this in tooltip or other places as well, we can move this
7661 // logic to the numberFormatter and enable it by a parameter.
7662 while (i-- && ret === undefined) {
7663 multi = Math.pow(1000, i + 1);
7664 if (numericSymbolDetector >= multi && (value * 10) % multi === 0 && numericSymbols[i] !== null && value !== 0) { // #5480
7665 ret = H.numberFormat(value / multi, -1) + numericSymbols[i];
7666 }
7667 }
7668 }
7669
7670 if (ret === undefined) {
7671 if (Math.abs(value) >= 10000) { // add thousands separators
7672 ret = H.numberFormat(value, -1);
7673 } else { // small numbers
7674 ret = H.numberFormat(value, -1, undefined, ''); // #2466
7675 }
7676 }
7677
7678 return ret;
7679 },
7680
7681 /**
7682 * Get the minimum and maximum for the series of each axis
7683 */
7684 getSeriesExtremes: function() {
7685 var axis = this,
7686 chart = axis.chart;
7687 axis.hasVisibleSeries = false;
7688
7689 // Reset properties in case we're redrawing (#3353)
7690 axis.dataMin = axis.dataMax = axis.threshold = null;
7691 axis.softThreshold = !axis.isXAxis;
7692
7693 if (axis.buildStacks) {
7694 axis.buildStacks();
7695 }
7696
7697 // loop through this axis' series
7698 each(axis.series, function(series) {
7699
7700 if (series.visible || !chart.options.chart.ignoreHiddenSeries) {
7701
7702 var seriesOptions = series.options,
7703 xData,
7704 threshold = seriesOptions.threshold,
7705 seriesDataMin,
7706 seriesDataMax;
7707
7708 axis.hasVisibleSeries = true;
7709
7710 // Validate threshold in logarithmic axes
7711 if (axis.isLog && threshold <= 0) {
7712 threshold = null;
7713 }
7714
7715 // Get dataMin and dataMax for X axes
7716 if (axis.isXAxis) {
7717 xData = series.xData;
7718 if (xData.length) {
7719 // If xData contains values which is not numbers, then filter them out.
7720 // To prevent performance hit, we only do this after we have already
7721 // found seriesDataMin because in most cases all data is valid. #5234.
7722 seriesDataMin = arrayMin(xData);
7723 if (!isNumber(seriesDataMin) && !(seriesDataMin instanceof Date)) { // Date for #5010
7724 xData = grep(xData, function(x) {
7725 return isNumber(x);
7726 });
7727 seriesDataMin = arrayMin(xData); // Do it again with valid data
7728 }
7729
7730 axis.dataMin = Math.min(pick(axis.dataMin, xData[0]), seriesDataMin);
7731 axis.dataMax = Math.max(pick(axis.dataMax, xData[0]), arrayMax(xData));
7732
7733 }
7734
7735 // Get dataMin and dataMax for Y axes, as well as handle stacking and processed data
7736 } else {
7737
7738 // Get this particular series extremes
7739 series.getExtremes();
7740 seriesDataMax = series.dataMax;
7741 seriesDataMin = series.dataMin;
7742
7743 // Get the dataMin and dataMax so far. If percentage is used, the min and max are
7744 // always 0 and 100. If seriesDataMin and seriesDataMax is null, then series
7745 // doesn't have active y data, we continue with nulls
7746 if (defined(seriesDataMin) && defined(seriesDataMax)) {
7747 axis.dataMin = Math.min(pick(axis.dataMin, seriesDataMin), seriesDataMin);
7748 axis.dataMax = Math.max(pick(axis.dataMax, seriesDataMax), seriesDataMax);
7749 }
7750
7751 // Adjust to threshold
7752 if (defined(threshold)) {
7753 axis.threshold = threshold;
7754 }
7755 // If any series has a hard threshold, it takes precedence
7756 if (!seriesOptions.softThreshold || axis.isLog) {
7757 axis.softThreshold = false;
7758 }
7759 }
7760 }
7761 });
7762 },
7763
7764 /**
7765 * Translate from axis value to pixel position on the chart, or back
7766 *
7767 */
7768 translate: function(val, backwards, cvsCoord, old, handleLog, pointPlacement) {
7769 var axis = this.linkedParent || this, // #1417
7770 sign = 1,
7771 cvsOffset = 0,
7772 localA = old ? axis.oldTransA : axis.transA,
7773 localMin = old ? axis.oldMin : axis.min,
7774 returnValue,
7775 minPixelPadding = axis.minPixelPadding,
7776 doPostTranslate = (axis.isOrdinal || axis.isBroken || (axis.isLog && handleLog)) && axis.lin2val;
7777
7778 if (!localA) {
7779 localA = axis.transA;
7780 }
7781
7782 // In vertical axes, the canvas coordinates start from 0 at the top like in
7783 // SVG.
7784 if (cvsCoord) {
7785 sign *= -1; // canvas coordinates inverts the value
7786 cvsOffset = axis.len;
7787 }
7788
7789 // Handle reversed axis
7790 if (axis.reversed) {
7791 sign *= -1;
7792 cvsOffset -= sign * (axis.sector || axis.len);
7793 }
7794
7795 // From pixels to value
7796 if (backwards) { // reverse translation
7797
7798 val = val * sign + cvsOffset;
7799 val -= minPixelPadding;
7800 returnValue = val / localA + localMin; // from chart pixel to value
7801 if (doPostTranslate) { // log and ordinal axes
7802 returnValue = axis.lin2val(returnValue);
7803 }
7804
7805 // From value to pixels
7806 } else {
7807 if (doPostTranslate) { // log and ordinal axes
7808 val = axis.val2lin(val);
7809 }
7810 if (pointPlacement === 'between') {
7811 pointPlacement = 0.5;
7812 }
7813 returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * minPixelPadding) +
7814 (isNumber(pointPlacement) ? localA * pointPlacement * axis.pointRange : 0);
7815 }
7816
7817 return returnValue;
7818 },
7819
7820 /**
7821 * Utility method to translate an axis value to pixel position.
7822 * @param {Number} value A value in terms of axis units
7823 * @param {Boolean} paneCoordinates Whether to return the pixel coordinate relative to the chart
7824 * or just the axis/pane itself.
7825 */
7826 toPixels: function(value, paneCoordinates) {
7827 return this.translate(value, false, !this.horiz, null, true) + (paneCoordinates ? 0 : this.pos);
7828 },
7829
7830 /*
7831 * Utility method to translate a pixel position in to an axis value
7832 * @param {Number} pixel The pixel value coordinate
7833 * @param {Boolean} paneCoordiantes Whether the input pixel is relative to the chart or just the
7834 * axis/pane itself.
7835 */
7836 toValue: function(pixel, paneCoordinates) {
7837 return this.translate(pixel - (paneCoordinates ? 0 : this.pos), true, !this.horiz, null, true);
7838 },
7839
7840 /**
7841 * Create the path for a plot line that goes from the given value on
7842 * this axis, across the plot to the opposite side
7843 * @param {Number} value
7844 * @param {Number} lineWidth Used for calculation crisp line
7845 * @param {Number] old Use old coordinates (for resizing and rescaling)
7846 */
7847 getPlotLinePath: function(value, lineWidth, old, force, translatedValue) {
7848 var axis = this,
7849 chart = axis.chart,
7850 axisLeft = axis.left,
7851 axisTop = axis.top,
7852 x1,
7853 y1,
7854 x2,
7855 y2,
7856 cHeight = (old && chart.oldChartHeight) || chart.chartHeight,
7857 cWidth = (old && chart.oldChartWidth) || chart.chartWidth,
7858 skip,
7859 transB = axis.transB,
7860 /**
7861 * Check if x is between a and b. If not, either move to a/b or skip,
7862 * depending on the force parameter.
7863 */
7864 between = function(x, a, b) {
7865 if (x < a || x > b) {
7866 if (force) {
7867 x = Math.min(Math.max(a, x), b);
7868 } else {
7869 skip = true;
7870 }
7871 }
7872 return x;
7873 };
7874
7875 translatedValue = pick(translatedValue, axis.translate(value, null, null, old));
7876 x1 = x2 = Math.round(translatedValue + transB);
7877 y1 = y2 = Math.round(cHeight - translatedValue - transB);
7878 if (!isNumber(translatedValue)) { // no min or max
7879 skip = true;
7880
7881 } else if (axis.horiz) {
7882 y1 = axisTop;
7883 y2 = cHeight - axis.bottom;
7884 x1 = x2 = between(x1, axisLeft, axisLeft + axis.width);
7885 } else {
7886 x1 = axisLeft;
7887 x2 = cWidth - axis.right;
7888 y1 = y2 = between(y1, axisTop, axisTop + axis.height);
7889 }
7890 return skip && !force ?
7891 null :
7892 chart.renderer.crispLine(['M', x1, y1, 'L', x2, y2], lineWidth || 1);
7893 },
7894
7895 /**
7896 * Set the tick positions of a linear axis to round values like whole tens or every five.
7897 */
7898 getLinearTickPositions: function(tickInterval, min, max) {
7899 var pos,
7900 lastPos,
7901 roundedMin = correctFloat(Math.floor(min / tickInterval) * tickInterval),
7902 roundedMax = correctFloat(Math.ceil(max / tickInterval) * tickInterval),
7903 tickPositions = [];
7904
7905 // For single points, add a tick regardless of the relative position (#2662)
7906 if (min === max && isNumber(min)) {
7907 return [min];
7908 }
7909
7910 // Populate the intermediate values
7911 pos = roundedMin;
7912 while (pos <= roundedMax) {
7913
7914 // Place the tick on the rounded value
7915 tickPositions.push(pos);
7916
7917 // Always add the raw tickInterval, not the corrected one.
7918 pos = correctFloat(pos + tickInterval);
7919
7920 // If the interval is not big enough in the current min - max range to actually increase
7921 // the loop variable, we need to break out to prevent endless loop. Issue #619
7922 if (pos === lastPos) {
7923 break;
7924 }
7925
7926 // Record the last value
7927 lastPos = pos;
7928 }
7929 return tickPositions;
7930 },
7931
7932 /**
7933 * Return the minor tick positions. For logarithmic axes, reuse the same logic
7934 * as for major ticks.
7935 */
7936 getMinorTickPositions: function() {
7937 var axis = this,
7938 options = axis.options,
7939 tickPositions = axis.tickPositions,
7940 minorTickInterval = axis.minorTickInterval,
7941 minorTickPositions = [],
7942 pos,
7943 i,
7944 pointRangePadding = axis.pointRangePadding || 0,
7945 min = axis.min - pointRangePadding, // #1498
7946 max = axis.max + pointRangePadding, // #1498
7947 range = max - min,
7948 len;
7949
7950 // If minor ticks get too dense, they are hard to read, and may cause long running script. So we don't draw them.
7951 if (range && range / minorTickInterval < axis.len / 3) { // #3875
7952
7953 if (axis.isLog) {
7954 len = tickPositions.length;
7955 for (i = 1; i < len; i++) {
7956 minorTickPositions = minorTickPositions.concat(
7957 axis.getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true)
7958 );
7959 }
7960 } else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314
7961 minorTickPositions = minorTickPositions.concat(
7962 axis.getTimeTicks(
7963 axis.normalizeTimeTickInterval(minorTickInterval),
7964 min,
7965 max,
7966 options.startOfWeek
7967 )
7968 );
7969 } else {
7970 for (pos = min + (tickPositions[0] - min) % minorTickInterval; pos <= max; pos += minorTickInterval) {
7971 minorTickPositions.push(pos);
7972 }
7973 }
7974 }
7975
7976 if (minorTickPositions.length !== 0) { // don't change the extremes, when there is no minor ticks
7977 axis.trimTicks(minorTickPositions, options.startOnTick, options.endOnTick); // #3652 #3743 #1498
7978 }
7979 return minorTickPositions;
7980 },
7981
7982 /**
7983 * Adjust the min and max for the minimum range. Keep in mind that the series data is
7984 * not yet processed, so we don't have information on data cropping and grouping, or
7985 * updated axis.pointRange or series.pointRange. The data can't be processed until
7986 * we have finally established min and max.
7987 */
7988 adjustForMinRange: function() {
7989 var axis = this,
7990 options = axis.options,
7991 min = axis.min,
7992 max = axis.max,
7993 zoomOffset,
7994 spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange,
7995 closestDataRange,
7996 i,
7997 distance,
7998 xData,
7999 loopLength,
8000 minArgs,
8001 maxArgs,
8002 minRange;
8003
8004 // Set the automatic minimum range based on the closest point distance
8005 if (axis.isXAxis && axis.minRange === undefined && !axis.isLog) {
8006
8007 if (defined(options.min) || defined(options.max)) {
8008 axis.minRange = null; // don't do this again
8009
8010 } else {
8011
8012 // Find the closest distance between raw data points, as opposed to
8013 // closestPointRange that applies to processed points (cropped and grouped)
8014 each(axis.series, function(series) {
8015 xData = series.xData;
8016 loopLength = series.xIncrement ? 1 : xData.length - 1;
8017 for (i = loopLength; i > 0; i--) {
8018 distance = xData[i] - xData[i - 1];
8019 if (closestDataRange === undefined || distance < closestDataRange) {
8020 closestDataRange = distance;
8021 }
8022 }
8023 });
8024 axis.minRange = Math.min(closestDataRange * 5, axis.dataMax - axis.dataMin);
8025 }
8026 }
8027
8028 // if minRange is exceeded, adjust
8029 if (max - min < axis.minRange) {
8030 minRange = axis.minRange;
8031 zoomOffset = (minRange - max + min) / 2;
8032
8033 // if min and max options have been set, don't go beyond it
8034 minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)];
8035 if (spaceAvailable) { // if space is available, stay within the data range
8036 minArgs[2] = axis.isLog ? axis.log2lin(axis.dataMin) : axis.dataMin;
8037 }
8038 min = arrayMax(minArgs);
8039
8040 maxArgs = [min + minRange, pick(options.max, min + minRange)];
8041 if (spaceAvailable) { // if space is availabe, stay within the data range
8042 maxArgs[2] = axis.isLog ? axis.log2lin(axis.dataMax) : axis.dataMax;
8043 }
8044
8045 max = arrayMin(maxArgs);
8046
8047 // now if the max is adjusted, adjust the min back
8048 if (max - min < minRange) {
8049 minArgs[0] = max - minRange;
8050 minArgs[1] = pick(options.min, max - minRange);
8051 min = arrayMax(minArgs);
8052 }
8053 }
8054
8055 // Record modified extremes
8056 axis.min = min;
8057 axis.max = max;
8058 },
8059
8060 /**
8061 * Find the closestPointRange across all series
8062 */
8063 getClosest: function() {
8064 var ret;
8065
8066 if (this.categories) {
8067 ret = 1;
8068 } else {
8069 each(this.series, function(series) {
8070 var seriesClosest = series.closestPointRange;
8071 if (!series.noSharedTooltip && defined(seriesClosest)) {
8072 ret = defined(ret) ?
8073 Math.min(ret, seriesClosest) :
8074 seriesClosest;
8075 }
8076 });
8077 }
8078 return ret;
8079 },
8080
8081 /**
8082 * When a point name is given and no x, search for the name in the existing categories,
8083 * or if categories aren't provided, search names or create a new category (#2522).
8084 */
8085 nameToX: function(point) {
8086 var explicitCategories = isArray(this.categories),
8087 names = explicitCategories ? this.categories : this.names,
8088 nameX = point.options.x,
8089 x;
8090
8091 point.series.requireSorting = false;
8092
8093 if (!defined(nameX)) {
8094 nameX = this.options.nameToX === false ?
8095 point.series.autoIncrement() :
8096 inArray(point.name, names);
8097 }
8098 if (nameX === -1) { // The name is not found in currenct categories
8099 if (!explicitCategories) {
8100 x = names.length;
8101 }
8102 } else {
8103 x = nameX;
8104 }
8105
8106 // Write the last point's name to the names array
8107 this.names[x] = point.name;
8108
8109 return x;
8110 },
8111
8112 /**
8113 * When changes have been done to series data, update the axis.names.
8114 */
8115 updateNames: function() {
8116 var axis = this;
8117
8118 if (this.names.length > 0) {
8119 this.names.length = 0;
8120 this.minRange = undefined;
8121 each(this.series || [], function(series) {
8122
8123 // When adding a series, points are not yet generated
8124 if (!series.processedXData) {
8125 series.processData();
8126 series.generatePoints();
8127 }
8128
8129 each(series.points, function(point, i) {
8130 var x;
8131 if (point.options && point.options.x === undefined) {
8132 x = axis.nameToX(point);
8133 if (x !== point.x) {
8134 point.x = x;
8135 series.xData[i] = x;
8136 }
8137 }
8138 });
8139 });
8140 }
8141 },
8142
8143 /**
8144 * Update translation information
8145 */
8146 setAxisTranslation: function(saveOld) {
8147 var axis = this,
8148 range = axis.max - axis.min,
8149 pointRange = axis.axisPointRange || 0,
8150 closestPointRange,
8151 minPointOffset = 0,
8152 pointRangePadding = 0,
8153 linkedParent = axis.linkedParent,
8154 ordinalCorrection,
8155 hasCategories = !!axis.categories,
8156 transA = axis.transA,
8157 isXAxis = axis.isXAxis;
8158
8159 // Adjust translation for padding. Y axis with categories need to go through the same (#1784).
8160 if (isXAxis || hasCategories || pointRange) {
8161 if (linkedParent) {
8162 minPointOffset = linkedParent.minPointOffset;
8163 pointRangePadding = linkedParent.pointRangePadding;
8164
8165 } else {
8166
8167 // Get the closest points
8168 closestPointRange = axis.getClosest();
8169
8170 each(axis.series, function(series) {
8171 var seriesPointRange = hasCategories ?
8172 1 :
8173 (isXAxis ?
8174 pick(series.options.pointRange, closestPointRange, 0) :
8175 (axis.axisPointRange || 0)), // #2806
8176 pointPlacement = series.options.pointPlacement;
8177
8178 pointRange = Math.max(pointRange, seriesPointRange);
8179
8180 if (!axis.single) {
8181 // minPointOffset is the value padding to the left of the axis in order to make
8182 // room for points with a pointRange, typically columns. When the pointPlacement option
8183 // is 'between' or 'on', this padding does not apply.
8184 minPointOffset = Math.max(
8185 minPointOffset,
8186 isString(pointPlacement) ? 0 : seriesPointRange / 2
8187 );
8188
8189 // Determine the total padding needed to the length of the axis to make room for the
8190 // pointRange. If the series' pointPlacement is 'on', no padding is added.
8191 pointRangePadding = Math.max(
8192 pointRangePadding,
8193 pointPlacement === 'on' ? 0 : seriesPointRange
8194 );
8195 }
8196 });
8197 }
8198
8199 // Record minPointOffset and pointRangePadding
8200 ordinalCorrection = axis.ordinalSlope && closestPointRange ? axis.ordinalSlope / closestPointRange : 1; // #988, #1853
8201 axis.minPointOffset = minPointOffset = minPointOffset * ordinalCorrection;
8202 axis.pointRangePadding = pointRangePadding = pointRangePadding * ordinalCorrection;
8203
8204 // pointRange means the width reserved for each point, like in a column chart
8205 axis.pointRange = Math.min(pointRange, range);
8206
8207 // closestPointRange means the closest distance between points. In columns
8208 // it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange
8209 // is some other value
8210 if (isXAxis) {
8211 axis.closestPointRange = closestPointRange;
8212 }
8213 }
8214
8215 // Secondary values
8216 if (saveOld) {
8217 axis.oldTransA = transA;
8218 }
8219 axis.translationSlope = axis.transA = transA = axis.len / ((range + pointRangePadding) || 1);
8220 axis.transB = axis.horiz ? axis.left : axis.bottom; // translation addend
8221 axis.minPixelPadding = transA * minPointOffset;
8222 },
8223
8224 minFromRange: function() {
8225 return this.max - this.range;
8226 },
8227
8228 /**
8229 * Set the tick positions to round values and optionally extend the extremes
8230 * to the nearest tick
8231 */
8232 setTickInterval: function(secondPass) {
8233 var axis = this,
8234 chart = axis.chart,
8235 options = axis.options,
8236 isLog = axis.isLog,
8237 log2lin = axis.log2lin,
8238 isDatetimeAxis = axis.isDatetimeAxis,
8239 isXAxis = axis.isXAxis,
8240 isLinked = axis.isLinked,
8241 maxPadding = options.maxPadding,
8242 minPadding = options.minPadding,
8243 length,
8244 linkedParentExtremes,
8245 tickIntervalOption = options.tickInterval,
8246 minTickInterval,
8247 tickPixelIntervalOption = options.tickPixelInterval,
8248 categories = axis.categories,
8249 threshold = axis.threshold,
8250 softThreshold = axis.softThreshold,
8251 thresholdMin,
8252 thresholdMax,
8253 hardMin,
8254 hardMax;
8255
8256 if (!isDatetimeAxis && !categories && !isLinked) {
8257 this.getTickAmount();
8258 }
8259
8260 // Min or max set either by zooming/setExtremes or initial options
8261 hardMin = pick(axis.userMin, options.min);
8262 hardMax = pick(axis.userMax, options.max);
8263
8264 // Linked axis gets the extremes from the parent axis
8265 if (isLinked) {
8266 axis.linkedParent = chart[axis.coll][options.linkedTo];
8267 linkedParentExtremes = axis.linkedParent.getExtremes();
8268 axis.min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin);
8269 axis.max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax);
8270 if (options.type !== axis.linkedParent.options.type) {
8271 error(11, 1); // Can't link axes of different type
8272 }
8273
8274 // Initial min and max from the extreme data values
8275 } else {
8276
8277 // Adjust to hard threshold
8278 if (!softThreshold && defined(threshold)) {
8279 if (axis.dataMin >= threshold) {
8280 thresholdMin = threshold;
8281 minPadding = 0;
8282 } else if (axis.dataMax <= threshold) {
8283 thresholdMax = threshold;
8284 maxPadding = 0;
8285 }
8286 }
8287
8288 axis.min = pick(hardMin, thresholdMin, axis.dataMin);
8289 axis.max = pick(hardMax, thresholdMax, axis.dataMax);
8290
8291 }
8292
8293 if (isLog) {
8294 if (!secondPass && Math.min(axis.min, pick(axis.dataMin, axis.min)) <= 0) { // #978
8295 error(10, 1); // Can't plot negative values on log axis
8296 }
8297 // The correctFloat cures #934, float errors on full tens. But it
8298 // was too aggressive for #4360 because of conversion back to lin,
8299 // therefore use precision 15.
8300 axis.min = correctFloat(log2lin(axis.min), 15);
8301 axis.max = correctFloat(log2lin(axis.max), 15);
8302 }
8303
8304 // handle zoomed range
8305 if (axis.range && defined(axis.max)) {
8306 axis.userMin = axis.min = hardMin = Math.max(axis.min, axis.minFromRange()); // #618
8307 axis.userMax = hardMax = axis.max;
8308
8309 axis.range = null; // don't use it when running setExtremes
8310 }
8311
8312 // Hook for Highstock Scroller. Consider combining with beforePadding.
8313 fireEvent(axis, 'foundExtremes');
8314
8315 // Hook for adjusting this.min and this.max. Used by bubble series.
8316 if (axis.beforePadding) {
8317 axis.beforePadding();
8318 }
8319
8320 // adjust min and max for the minimum range
8321 axis.adjustForMinRange();
8322
8323 // Pad the values to get clear of the chart's edges. To avoid tickInterval taking the padding
8324 // into account, we do this after computing tick interval (#1337).
8325 if (!categories && !axis.axisPointRange && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) {
8326 length = axis.max - axis.min;
8327 if (length) {
8328 if (!defined(hardMin) && minPadding) {
8329 axis.min -= length * minPadding;
8330 }
8331 if (!defined(hardMax) && maxPadding) {
8332 axis.max += length * maxPadding;
8333 }
8334 }
8335 }
8336
8337 // Stay within floor and ceiling
8338 if (isNumber(options.floor)) {
8339 axis.min = Math.max(axis.min, options.floor);
8340 }
8341 if (isNumber(options.ceiling)) {
8342 axis.max = Math.min(axis.max, options.ceiling);
8343 }
8344
8345 // When the threshold is soft, adjust the extreme value only if
8346 // the data extreme and the padded extreme land on either side of the threshold. For example,
8347 // a series of [0, 1, 2, 3] would make the yAxis add a tick for -1 because of the
8348 // default minPadding and startOnTick options. This is prevented by the softThreshold
8349 // option.
8350 if (softThreshold && defined(axis.dataMin)) {
8351 threshold = threshold || 0;
8352 if (!defined(hardMin) && axis.min < threshold && axis.dataMin >= threshold) {
8353 axis.min = threshold;
8354 } else if (!defined(hardMax) && axis.max > threshold && axis.dataMax <= threshold) {
8355 axis.max = threshold;
8356 }
8357 }
8358
8359
8360 // get tickInterval
8361 if (axis.min === axis.max || axis.min === undefined || axis.max === undefined) {
8362 axis.tickInterval = 1;
8363 } else if (isLinked && !tickIntervalOption &&
8364 tickPixelIntervalOption === axis.linkedParent.options.tickPixelInterval) {
8365 axis.tickInterval = tickIntervalOption = axis.linkedParent.tickInterval;
8366 } else {
8367 axis.tickInterval = pick(
8368 tickIntervalOption,
8369 this.tickAmount ? ((axis.max - axis.min) / Math.max(this.tickAmount - 1, 1)) : undefined,
8370 categories ? // for categoried axis, 1 is default, for linear axis use tickPix
8371 1 :
8372 // don't let it be more than the data range
8373 (axis.max - axis.min) * tickPixelIntervalOption / Math.max(axis.len, tickPixelIntervalOption)
8374 );
8375 }
8376
8377 // Now we're finished detecting min and max, crop and group series data. This
8378 // is in turn needed in order to find tick positions in ordinal axes.
8379 if (isXAxis && !secondPass) {
8380 each(axis.series, function(series) {
8381 series.processData(axis.min !== axis.oldMin || axis.max !== axis.oldMax);
8382 });
8383 }
8384
8385 // set the translation factor used in translate function
8386 axis.setAxisTranslation(true);
8387
8388 // hook for ordinal axes and radial axes
8389 if (axis.beforeSetTickPositions) {
8390 axis.beforeSetTickPositions();
8391 }
8392
8393 // hook for extensions, used in Highstock ordinal axes
8394 if (axis.postProcessTickInterval) {
8395 axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval);
8396 }
8397
8398 // In column-like charts, don't cramp in more ticks than there are points (#1943, #4184)
8399 if (axis.pointRange && !tickIntervalOption) {
8400 axis.tickInterval = Math.max(axis.pointRange, axis.tickInterval);
8401 }
8402
8403 // Before normalizing the tick interval, handle minimum tick interval. This applies only if tickInterval is not defined.
8404 minTickInterval = pick(options.minTickInterval, axis.isDatetimeAxis && axis.closestPointRange);
8405 if (!tickIntervalOption && axis.tickInterval < minTickInterval) {
8406 axis.tickInterval = minTickInterval;
8407 }
8408
8409 // for linear axes, get magnitude and normalize the interval
8410 if (!isDatetimeAxis && !isLog && !tickIntervalOption) {
8411 axis.tickInterval = normalizeTickInterval(
8412 axis.tickInterval,
8413 null,
8414 getMagnitude(axis.tickInterval),
8415 // If the tick interval is between 0.5 and 5 and the axis max is in the order of
8416 // thousands, chances are we are dealing with years. Don't allow decimals. #3363.
8417 pick(options.allowDecimals, !(axis.tickInterval > 0.5 && axis.tickInterval < 5 && axis.max > 1000 && axis.max < 9999)), !!this.tickAmount
8418 );
8419 }
8420
8421 // Prevent ticks from getting so close that we can't draw the labels
8422 if (!this.tickAmount) {
8423 axis.tickInterval = axis.unsquish();
8424 }
8425
8426 this.setTickPositions();
8427 },
8428
8429 /**
8430 * Now we have computed the normalized tickInterval, get the tick positions
8431 */
8432 setTickPositions: function() {
8433
8434 var options = this.options,
8435 tickPositions,
8436 tickPositionsOption = options.tickPositions,
8437 tickPositioner = options.tickPositioner,
8438 startOnTick = options.startOnTick,
8439 endOnTick = options.endOnTick,
8440 single;
8441
8442 // Set the tickmarkOffset
8443 this.tickmarkOffset = (this.categories && options.tickmarkPlacement === 'between' &&
8444 this.tickInterval === 1) ? 0.5 : 0; // #3202
8445
8446
8447 // get minorTickInterval
8448 this.minorTickInterval = options.minorTickInterval === 'auto' && this.tickInterval ?
8449 this.tickInterval / 5 : options.minorTickInterval;
8450
8451 // Find the tick positions
8452 this.tickPositions = tickPositions = tickPositionsOption && tickPositionsOption.slice(); // Work on a copy (#1565)
8453 if (!tickPositions) {
8454
8455 if (this.isDatetimeAxis) {
8456 tickPositions = this.getTimeTicks(
8457 this.normalizeTimeTickInterval(this.tickInterval, options.units),
8458 this.min,
8459 this.max,
8460 options.startOfWeek,
8461 this.ordinalPositions,
8462 this.closestPointRange,
8463 true
8464 );
8465 } else if (this.isLog) {
8466 tickPositions = this.getLogTickPositions(this.tickInterval, this.min, this.max);
8467 } else {
8468 tickPositions = this.getLinearTickPositions(this.tickInterval, this.min, this.max);
8469 }
8470
8471 // Too dense ticks, keep only the first and last (#4477)
8472 if (tickPositions.length > this.len) {
8473 tickPositions = [tickPositions[0], tickPositions.pop()];
8474 }
8475
8476 this.tickPositions = tickPositions;
8477
8478 // Run the tick positioner callback, that allows modifying auto tick positions.
8479 if (tickPositioner) {
8480 tickPositioner = tickPositioner.apply(this, [this.min, this.max]);
8481 if (tickPositioner) {
8482 this.tickPositions = tickPositions = tickPositioner;
8483 }
8484 }
8485
8486 }
8487
8488 if (!this.isLinked) {
8489
8490 // reset min/max or remove extremes based on start/end on tick
8491 this.trimTicks(tickPositions, startOnTick, endOnTick);
8492
8493 // When there is only one point, or all points have the same value on this axis, then min
8494 // and max are equal and tickPositions.length is 0 or 1. In this case, add some padding
8495 // in order to center the point, but leave it with one tick. #1337.
8496 if (this.min === this.max && defined(this.min) && !this.tickAmount) {
8497 // Substract half a unit (#2619, #2846, #2515, #3390)
8498 single = true;
8499 this.min -= 0.5;
8500 this.max += 0.5;
8501 }
8502 this.single = single;
8503
8504 if (!tickPositionsOption && !tickPositioner) {
8505 this.adjustTickAmount();
8506 }
8507 }
8508 },
8509
8510 /**
8511 * Handle startOnTick and endOnTick by either adapting to padding min/max or rounded min/max
8512 */
8513 trimTicks: function(tickPositions, startOnTick, endOnTick) {
8514 var roundedMin = tickPositions[0],
8515 roundedMax = tickPositions[tickPositions.length - 1],
8516 minPointOffset = this.minPointOffset || 0;
8517
8518 if (startOnTick) {
8519 this.min = roundedMin;
8520 } else {
8521 while (this.min - minPointOffset > tickPositions[0]) {
8522 tickPositions.shift();
8523 }
8524 }
8525
8526 if (endOnTick) {
8527 this.max = roundedMax;
8528 } else {
8529 while (this.max + minPointOffset < tickPositions[tickPositions.length - 1]) {
8530 tickPositions.pop();
8531 }
8532 }
8533
8534 // If no tick are left, set one tick in the middle (#3195)
8535 if (tickPositions.length === 0 && defined(roundedMin)) {
8536 tickPositions.push((roundedMax + roundedMin) / 2);
8537 }
8538 },
8539
8540 /**
8541 * Check if there are multiple axes in the same pane
8542 * @returns {Boolean} There are other axes
8543 */
8544 alignToOthers: function() {
8545 var others = {}, // Whether there is another axis to pair with this one
8546 hasOther,
8547 options = this.options;
8548
8549 if (this.chart.options.chart.alignTicks !== false && options.alignTicks !== false) {
8550 each(this.chart[this.coll], function(axis) {
8551 var otherOptions = axis.options,
8552 horiz = axis.horiz,
8553 key = [
8554 horiz ? otherOptions.left : otherOptions.top,
8555 otherOptions.width,
8556 otherOptions.height,
8557 otherOptions.pane
8558 ].join(',');
8559
8560
8561 if (axis.series.length) { // #4442
8562 if (others[key]) {
8563 hasOther = true; // #4201
8564 } else {
8565 others[key] = 1;
8566 }
8567 }
8568 });
8569 }
8570 return hasOther;
8571 },
8572
8573 /**
8574 * Set the max ticks of either the x and y axis collection
8575 */
8576 getTickAmount: function() {
8577 var options = this.options,
8578 tickAmount = options.tickAmount,
8579 tickPixelInterval = options.tickPixelInterval;
8580
8581 if (!defined(options.tickInterval) && this.len < tickPixelInterval && !this.isRadial &&
8582 !this.isLog && options.startOnTick && options.endOnTick) {
8583 tickAmount = 2;
8584 }
8585
8586 if (!tickAmount && this.alignToOthers()) {
8587 // Add 1 because 4 tick intervals require 5 ticks (including first and last)
8588 tickAmount = Math.ceil(this.len / tickPixelInterval) + 1;
8589 }
8590
8591 // For tick amounts of 2 and 3, compute five ticks and remove the intermediate ones. This
8592 // prevents the axis from adding ticks that are too far away from the data extremes.
8593 if (tickAmount < 4) {
8594 this.finalTickAmt = tickAmount;
8595 tickAmount = 5;
8596 }
8597
8598 this.tickAmount = tickAmount;
8599 },
8600
8601 /**
8602 * When using multiple axes, adjust the number of ticks to match the highest
8603 * number of ticks in that group
8604 */
8605 adjustTickAmount: function() {
8606 var tickInterval = this.tickInterval,
8607 tickPositions = this.tickPositions,
8608 tickAmount = this.tickAmount,
8609 finalTickAmt = this.finalTickAmt,
8610 currentTickAmount = tickPositions && tickPositions.length,
8611 i,
8612 len;
8613
8614 if (currentTickAmount < tickAmount) {
8615 while (tickPositions.length < tickAmount) {
8616 tickPositions.push(correctFloat(
8617 tickPositions[tickPositions.length - 1] + tickInterval
8618 ));
8619 }
8620 this.transA *= (currentTickAmount - 1) / (tickAmount - 1);
8621 this.max = tickPositions[tickPositions.length - 1];
8622
8623 // We have too many ticks, run second pass to try to reduce ticks
8624 } else if (currentTickAmount > tickAmount) {
8625 this.tickInterval *= 2;
8626 this.setTickPositions();
8627 }
8628
8629 // The finalTickAmt property is set in getTickAmount
8630 if (defined(finalTickAmt)) {
8631 i = len = tickPositions.length;
8632 while (i--) {
8633 if (
8634 (finalTickAmt === 3 && i % 2 === 1) || // Remove every other tick
8635 (finalTickAmt <= 2 && i > 0 && i < len - 1) // Remove all but first and last
8636 ) {
8637 tickPositions.splice(i, 1);
8638 }
8639 }
8640 this.finalTickAmt = undefined;
8641 }
8642 },
8643
8644 /**
8645 * Set the scale based on data min and max, user set min and max or options
8646 *
8647 */
8648 setScale: function() {
8649 var axis = this,
8650 isDirtyData,
8651 isDirtyAxisLength;
8652
8653 axis.oldMin = axis.min;
8654 axis.oldMax = axis.max;
8655 axis.oldAxisLength = axis.len;
8656
8657 // set the new axisLength
8658 axis.setAxisSize();
8659 //axisLength = horiz ? axisWidth : axisHeight;
8660 isDirtyAxisLength = axis.len !== axis.oldAxisLength;
8661
8662 // is there new data?
8663 each(axis.series, function(series) {
8664 if (series.isDirtyData || series.isDirty ||
8665 series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well
8666 isDirtyData = true;
8667 }
8668 });
8669
8670 // do we really need to go through all this?
8671 if (isDirtyAxisLength || isDirtyData || axis.isLinked || axis.forceRedraw ||
8672 axis.userMin !== axis.oldUserMin || axis.userMax !== axis.oldUserMax || axis.alignToOthers()) {
8673
8674 if (axis.resetStacks) {
8675 axis.resetStacks();
8676 }
8677
8678 axis.forceRedraw = false;
8679
8680 // get data extremes if needed
8681 axis.getSeriesExtremes();
8682
8683 // get fixed positions based on tickInterval
8684 axis.setTickInterval();
8685
8686 // record old values to decide whether a rescale is necessary later on (#540)
8687 axis.oldUserMin = axis.userMin;
8688 axis.oldUserMax = axis.userMax;
8689
8690 // Mark as dirty if it is not already set to dirty and extremes have changed. #595.
8691 if (!axis.isDirty) {
8692 axis.isDirty = isDirtyAxisLength || axis.min !== axis.oldMin || axis.max !== axis.oldMax;
8693 }
8694 } else if (axis.cleanStacks) {
8695 axis.cleanStacks();
8696 }
8697 },
8698
8699 /**
8700 * Set the extremes and optionally redraw
8701 * @param {Number} newMin
8702 * @param {Number} newMax
8703 * @param {Boolean} redraw
8704 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
8705 * configuration
8706 * @param {Object} eventArguments
8707 *
8708 */
8709 setExtremes: function(newMin, newMax, redraw, animation, eventArguments) {
8710 var axis = this,
8711 chart = axis.chart;
8712
8713 redraw = pick(redraw, true); // defaults to true
8714
8715 each(axis.series, function(serie) {
8716 delete serie.kdTree;
8717 });
8718
8719 // Extend the arguments with min and max
8720 eventArguments = extend(eventArguments, {
8721 min: newMin,
8722 max: newMax
8723 });
8724
8725 // Fire the event
8726 fireEvent(axis, 'setExtremes', eventArguments, function() { // the default event handler
8727
8728 axis.userMin = newMin;
8729 axis.userMax = newMax;
8730 axis.eventArgs = eventArguments;
8731
8732 if (redraw) {
8733 chart.redraw(animation);
8734 }
8735 });
8736 },
8737
8738 /**
8739 * Overridable method for zooming chart. Pulled out in a separate method to allow overriding
8740 * in stock charts.
8741 */
8742 zoom: function(newMin, newMax) {
8743 var dataMin = this.dataMin,
8744 dataMax = this.dataMax,
8745 options = this.options,
8746 min = Math.min(dataMin, pick(options.min, dataMin)),
8747 max = Math.max(dataMax, pick(options.max, dataMax));
8748
8749 // Prevent pinch zooming out of range. Check for defined is for #1946. #1734.
8750 if (!this.allowZoomOutside) {
8751 if (defined(dataMin) && newMin <= min) {
8752 newMin = min;
8753 }
8754 if (defined(dataMax) && newMax >= max) {
8755 newMax = max;
8756 }
8757 }
8758
8759 // In full view, displaying the reset zoom button is not required
8760 this.displayBtn = newMin !== undefined || newMax !== undefined;
8761
8762 // Do it
8763 this.setExtremes(
8764 newMin,
8765 newMax,
8766 false,
8767 undefined, {
8768 trigger: 'zoom'
8769 }
8770 );
8771 return true;
8772 },
8773
8774 /**
8775 * Update the axis metrics
8776 */
8777 setAxisSize: function() {
8778 var chart = this.chart,
8779 options = this.options,
8780 offsetLeft = options.offsetLeft || 0,
8781 offsetRight = options.offsetRight || 0,
8782 horiz = this.horiz,
8783 width = pick(options.width, chart.plotWidth - offsetLeft + offsetRight),
8784 height = pick(options.height, chart.plotHeight),
8785 top = pick(options.top, chart.plotTop),
8786 left = pick(options.left, chart.plotLeft + offsetLeft),
8787 percentRegex = /%$/;
8788
8789 // Check for percentage based input values. Rounding fixes problems with
8790 // column overflow and plot line filtering (#4898, #4899)
8791 if (percentRegex.test(height)) {
8792 height = Math.round(parseFloat(height) / 100 * chart.plotHeight);
8793 }
8794 if (percentRegex.test(top)) {
8795 top = Math.round(parseFloat(top) / 100 * chart.plotHeight + chart.plotTop);
8796 }
8797
8798 // Expose basic values to use in Series object and navigator
8799 this.left = left;
8800 this.top = top;
8801 this.width = width;
8802 this.height = height;
8803 this.bottom = chart.chartHeight - height - top;
8804 this.right = chart.chartWidth - width - left;
8805
8806 // Direction agnostic properties
8807 this.len = Math.max(horiz ? width : height, 0); // Math.max fixes #905
8808 this.pos = horiz ? left : top; // distance from SVG origin
8809 },
8810
8811 /**
8812 * Get the actual axis extremes
8813 */
8814 getExtremes: function() {
8815 var axis = this,
8816 isLog = axis.isLog,
8817 lin2log = axis.lin2log;
8818
8819 return {
8820 min: isLog ? correctFloat(lin2log(axis.min)) : axis.min,
8821 max: isLog ? correctFloat(lin2log(axis.max)) : axis.max,
8822 dataMin: axis.dataMin,
8823 dataMax: axis.dataMax,
8824 userMin: axis.userMin,
8825 userMax: axis.userMax
8826 };
8827 },
8828
8829 /**
8830 * Get the zero plane either based on zero or on the min or max value.
8831 * Used in bar and area plots
8832 */
8833 getThreshold: function(threshold) {
8834 var axis = this,
8835 isLog = axis.isLog,
8836 lin2log = axis.lin2log,
8837 realMin = isLog ? lin2log(axis.min) : axis.min,
8838 realMax = isLog ? lin2log(axis.max) : axis.max;
8839
8840 if (threshold === null) {
8841 threshold = realMin;
8842 } else if (realMin > threshold) {
8843 threshold = realMin;
8844 } else if (realMax < threshold) {
8845 threshold = realMax;
8846 }
8847
8848 return axis.translate(threshold, 0, 1, 0, 1);
8849 },
8850
8851 /**
8852 * Compute auto alignment for the axis label based on which side the axis is on
8853 * and the given rotation for the label
8854 */
8855 autoLabelAlign: function(rotation) {
8856 var ret,
8857 angle = (pick(rotation, 0) - (this.side * 90) + 720) % 360;
8858
8859 if (angle > 15 && angle < 165) {
8860 ret = 'right';
8861 } else if (angle > 195 && angle < 345) {
8862 ret = 'left';
8863 } else {
8864 ret = 'center';
8865 }
8866 return ret;
8867 },
8868
8869 /**
8870 * Get the tick length and width for the axis.
8871 * @param {String} prefix 'tick' or 'minorTick'
8872 * @returns {Array} An array of tickLength and tickWidth
8873 */
8874 tickSize: function(prefix) {
8875 var options = this.options,
8876 tickLength = options[prefix + 'Length'],
8877 tickWidth = pick(options[prefix + 'Width'], prefix === 'tick' && this.isXAxis ? 1 : 0); // X axis defaults to 1
8878
8879 if (tickWidth && tickLength) {
8880 // Negate the length
8881 if (options[prefix + 'Position'] === 'inside') {
8882 tickLength = -tickLength;
8883 }
8884 return [tickLength, tickWidth];
8885 }
8886
8887 },
8888
8889 /**
8890 * Return the size of the labels
8891 */
8892 labelMetrics: function() {
8893 return this.chart.renderer.fontMetrics(
8894 this.options.labels.style && this.options.labels.style.fontSize,
8895 this.ticks[0] && this.ticks[0].label
8896 );
8897 },
8898
8899 /**
8900 * Prevent the ticks from getting so close we can't draw the labels. On a horizontal
8901 * axis, this is handled by rotating the labels, removing ticks and adding ellipsis.
8902 * On a vertical axis remove ticks and add ellipsis.
8903 */
8904 unsquish: function() {
8905 var labelOptions = this.options.labels,
8906 horiz = this.horiz,
8907 tickInterval = this.tickInterval,
8908 newTickInterval = tickInterval,
8909 slotSize = this.len / (((this.categories ? 1 : 0) + this.max - this.min) / tickInterval),
8910 rotation,
8911 rotationOption = labelOptions.rotation,
8912 labelMetrics = this.labelMetrics(),
8913 step,
8914 bestScore = Number.MAX_VALUE,
8915 autoRotation,
8916 // Return the multiple of tickInterval that is needed to avoid collision
8917 getStep = function(spaceNeeded) {
8918 var step = spaceNeeded / (slotSize || 1);
8919 step = step > 1 ? Math.ceil(step) : 1;
8920 return step * tickInterval;
8921 };
8922
8923 if (horiz) {
8924 autoRotation = !labelOptions.staggerLines && !labelOptions.step && ( // #3971
8925 defined(rotationOption) ? [rotationOption] :
8926 slotSize < pick(labelOptions.autoRotationLimit, 80) && labelOptions.autoRotation
8927 );
8928
8929 if (autoRotation) {
8930
8931 // Loop over the given autoRotation options, and determine which gives the best score. The
8932 // best score is that with the lowest number of steps and a rotation closest to horizontal.
8933 each(autoRotation, function(rot) {
8934 var score;
8935
8936 if (rot === rotationOption || (rot && rot >= -90 && rot <= 90)) { // #3891
8937
8938 step = getStep(Math.abs(labelMetrics.h / Math.sin(deg2rad * rot)));
8939
8940 score = step + Math.abs(rot / 360);
8941
8942 if (score < bestScore) {
8943 bestScore = score;
8944 rotation = rot;
8945 newTickInterval = step;
8946 }
8947 }
8948 });
8949 }
8950
8951 } else if (!labelOptions.step) { // #4411
8952 newTickInterval = getStep(labelMetrics.h);
8953 }
8954
8955 this.autoRotation = autoRotation;
8956 this.labelRotation = pick(rotation, rotationOption);
8957
8958 return newTickInterval;
8959 },
8960
8961 /**
8962 * Get the general slot width for this axis. This may change between the pre-render (from Axis.getOffset)
8963 * and the final tick rendering and placement (#5086).
8964 */
8965 getSlotWidth: function() {
8966 var chart = this.chart,
8967 horiz = this.horiz,
8968 labelOptions = this.options.labels,
8969 slotCount = Math.max(this.tickPositions.length - (this.categories ? 0 : 1), 1),
8970 marginLeft = chart.margin[3];
8971
8972 return (horiz && (labelOptions.step || 0) < 2 && !labelOptions.rotation && // #4415
8973 ((this.staggerLines || 1) * chart.plotWidth) / slotCount) ||
8974 (!horiz && ((marginLeft && (marginLeft - chart.spacing[3])) || chart.chartWidth * 0.33)); // #1580, #1931
8975
8976 },
8977
8978 /**
8979 * Render the axis labels and determine whether ellipsis or rotation need to be applied
8980 */
8981 renderUnsquish: function() {
8982 var chart = this.chart,
8983 renderer = chart.renderer,
8984 tickPositions = this.tickPositions,
8985 ticks = this.ticks,
8986 labelOptions = this.options.labels,
8987 horiz = this.horiz,
8988 slotWidth = this.getSlotWidth(),
8989 innerWidth = Math.max(1, Math.round(slotWidth - 2 * (labelOptions.padding || 5))),
8990 attr = {},
8991 labelMetrics = this.labelMetrics(),
8992 textOverflowOption = labelOptions.style && labelOptions.style.textOverflow,
8993 css,
8994 maxLabelLength = 0,
8995 label,
8996 i,
8997 pos;
8998
8999 // Set rotation option unless it is "auto", like in gauges
9000 if (!isString(labelOptions.rotation)) {
9001 attr.rotation = labelOptions.rotation || 0; // #4443
9002 }
9003
9004 // Get the longest label length
9005 each(tickPositions, function(tick) {
9006 tick = ticks[tick];
9007 if (tick && tick.labelLength > maxLabelLength) {
9008 maxLabelLength = tick.labelLength;
9009 }
9010 });
9011 this.maxLabelLength = maxLabelLength;
9012
9013
9014 // Handle auto rotation on horizontal axis
9015 if (this.autoRotation) {
9016
9017 // Apply rotation only if the label is too wide for the slot, and
9018 // the label is wider than its height.
9019 if (maxLabelLength > innerWidth && maxLabelLength > labelMetrics.h) {
9020 attr.rotation = this.labelRotation;
9021 } else {
9022 this.labelRotation = 0;
9023 }
9024
9025 // Handle word-wrap or ellipsis on vertical axis
9026 } else if (slotWidth) {
9027 // For word-wrap or ellipsis
9028 css = {
9029 width: innerWidth + 'px'
9030 };
9031
9032 if (!textOverflowOption) {
9033 css.textOverflow = 'clip';
9034
9035 // On vertical axis, only allow word wrap if there is room for more lines.
9036 i = tickPositions.length;
9037 while (!horiz && i--) {
9038 pos = tickPositions[i];
9039 label = ticks[pos].label;
9040 if (label) {
9041 // Reset ellipsis in order to get the correct bounding box (#4070)
9042 if (label.styles && label.styles.textOverflow === 'ellipsis') {
9043 label.css({
9044 textOverflow: 'clip'
9045 });
9046
9047 // Set the correct width in order to read the bounding box height (#4678, #5034)
9048 } else if (ticks[pos].labelLength > slotWidth) {
9049 label.css({
9050 width: slotWidth + 'px'
9051 });
9052 }
9053
9054 if (label.getBBox().height > this.len / tickPositions.length - (labelMetrics.h - labelMetrics.f)) {
9055 label.specCss = {
9056 textOverflow: 'ellipsis'
9057 };
9058 }
9059 }
9060 }
9061 }
9062 }
9063
9064
9065 // Add ellipsis if the label length is significantly longer than ideal
9066 if (attr.rotation) {
9067 css = {
9068 width: (maxLabelLength > chart.chartHeight * 0.5 ? chart.chartHeight * 0.33 : chart.chartHeight) + 'px'
9069 };
9070 if (!textOverflowOption) {
9071 css.textOverflow = 'ellipsis';
9072 }
9073 }
9074
9075 // Set the explicit or automatic label alignment
9076 this.labelAlign = labelOptions.align || this.autoLabelAlign(this.labelRotation);
9077 if (this.labelAlign) {
9078 attr.align = this.labelAlign;
9079 }
9080
9081 // Apply general and specific CSS
9082 each(tickPositions, function(pos) {
9083 var tick = ticks[pos],
9084 label = tick && tick.label;
9085 if (label) {
9086 label.attr(attr); // This needs to go before the CSS in old IE (#4502)
9087 if (css) {
9088 label.css(merge(css, label.specCss));
9089 }
9090 delete label.specCss;
9091 tick.rotation = attr.rotation;
9092 }
9093 });
9094
9095 // Note: Why is this not part of getLabelPosition?
9096 this.tickRotCorr = renderer.rotCorr(labelMetrics.b, this.labelRotation || 0, this.side !== 0);
9097 },
9098
9099 /**
9100 * Return true if the axis has associated data
9101 */
9102 hasData: function() {
9103 return this.hasVisibleSeries || (defined(this.min) && defined(this.max) && !!this.tickPositions);
9104 },
9105
9106 /**
9107 * Render the tick labels to a preliminary position to get their sizes
9108 */
9109 getOffset: function() {
9110 var axis = this,
9111 chart = axis.chart,
9112 renderer = chart.renderer,
9113 options = axis.options,
9114 tickPositions = axis.tickPositions,
9115 ticks = axis.ticks,
9116 horiz = axis.horiz,
9117 side = axis.side,
9118 invertedSide = chart.inverted ? [1, 0, 3, 2][side] : side,
9119 hasData,
9120 showAxis,
9121 titleOffset = 0,
9122 titleOffsetOption,
9123 titleMargin = 0,
9124 axisTitleOptions = options.title,
9125 labelOptions = options.labels,
9126 labelOffset = 0, // reset
9127 labelOffsetPadded,
9128 opposite = axis.opposite,
9129 axisOffset = chart.axisOffset,
9130 clipOffset = chart.clipOffset,
9131 clip,
9132 directionFactor = [-1, 1, 1, -1][side],
9133 n,
9134 className = options.className,
9135 textAlign,
9136 axisParent = axis.axisParent, // Used in color axis
9137 lineHeightCorrection,
9138 tickSize = this.tickSize('tick');
9139
9140 // For reuse in Axis.render
9141 hasData = axis.hasData();
9142 axis.showAxis = showAxis = hasData || pick(options.showEmpty, true);
9143
9144 // Set/reset staggerLines
9145 axis.staggerLines = axis.horiz && labelOptions.staggerLines;
9146
9147 // Create the axisGroup and gridGroup elements on first iteration
9148 if (!axis.axisGroup) {
9149 axis.gridGroup = renderer.g('grid')
9150 .attr({
9151 zIndex: options.gridZIndex || 1
9152 })
9153 .addClass('highcharts-' + this.coll.toLowerCase() + '-grid ' + (className || ''))
9154 .add(axisParent);
9155 axis.axisGroup = renderer.g('axis')
9156 .attr({
9157 zIndex: options.zIndex || 2
9158 })
9159 .addClass('highcharts-' + this.coll.toLowerCase() + ' ' + (className || ''))
9160 .add(axisParent);
9161 axis.labelGroup = renderer.g('axis-labels')
9162 .attr({
9163 zIndex: labelOptions.zIndex || 7
9164 })
9165 .addClass('highcharts-' + axis.coll.toLowerCase() + '-labels ' + (className || ''))
9166 .add(axisParent);
9167 }
9168
9169 if (hasData || axis.isLinked) {
9170
9171 // Generate ticks
9172 each(tickPositions, function(pos) {
9173 if (!ticks[pos]) {
9174 ticks[pos] = new Tick(axis, pos);
9175 } else {
9176 ticks[pos].addLabel(); // update labels depending on tick interval
9177 }
9178 });
9179
9180 axis.renderUnsquish();
9181
9182
9183 // Left side must be align: right and right side must have align: left for labels
9184 if (labelOptions.reserveSpace !== false && (side === 0 || side === 2 || {
9185 1: 'left',
9186 3: 'right'
9187 }[side] === axis.labelAlign || axis.labelAlign === 'center')) {
9188 each(tickPositions, function(pos) {
9189
9190 // get the highest offset
9191 labelOffset = Math.max(
9192 ticks[pos].getLabelSize(),
9193 labelOffset
9194 );
9195 });
9196 }
9197
9198 if (axis.staggerLines) {
9199 labelOffset *= axis.staggerLines;
9200 axis.labelOffset = labelOffset * (axis.opposite ? -1 : 1);
9201 }
9202
9203
9204 } else { // doesn't have data
9205 for (n in ticks) {
9206 ticks[n].destroy();
9207 delete ticks[n];
9208 }
9209 }
9210
9211 if (axisTitleOptions && axisTitleOptions.text && axisTitleOptions.enabled !== false) {
9212 if (!axis.axisTitle) {
9213 textAlign = axisTitleOptions.textAlign;
9214 if (!textAlign) {
9215 textAlign = (horiz ? {
9216 low: 'left',
9217 middle: 'center',
9218 high: 'right'
9219 } : {
9220 low: opposite ? 'right' : 'left',
9221 middle: 'center',
9222 high: opposite ? 'left' : 'right'
9223 })[axisTitleOptions.align];
9224 }
9225 axis.axisTitle = renderer.text(
9226 axisTitleOptions.text,
9227 0,
9228 0,
9229 axisTitleOptions.useHTML
9230 )
9231 .attr({
9232 zIndex: 7,
9233 rotation: axisTitleOptions.rotation || 0,
9234 align: textAlign
9235 })
9236 .addClass('highcharts-axis-title')
9237
9238 .css(axisTitleOptions.style)
9239
9240 .add(axis.axisGroup);
9241 axis.axisTitle.isNew = true;
9242 }
9243
9244 if (showAxis) {
9245 titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width'];
9246 titleOffsetOption = axisTitleOptions.offset;
9247 titleMargin = defined(titleOffsetOption) ? 0 : pick(axisTitleOptions.margin, horiz ? 5 : 10);
9248 }
9249
9250 // hide or show the title depending on whether showEmpty is set
9251 axis.axisTitle[showAxis ? 'show' : 'hide'](true);
9252 }
9253
9254 // Render the axis line
9255 axis.renderLine();
9256
9257 // handle automatic or user set offset
9258 axis.offset = directionFactor * pick(options.offset, axisOffset[side]);
9259
9260 axis.tickRotCorr = axis.tickRotCorr || {
9261 x: 0,
9262 y: 0
9263 }; // polar
9264 if (side === 0) {
9265 lineHeightCorrection = -axis.labelMetrics().h;
9266 } else if (side === 2) {
9267 lineHeightCorrection = axis.tickRotCorr.y;
9268 } else {
9269 lineHeightCorrection = 0;
9270 }
9271
9272 // Find the padded label offset
9273 labelOffsetPadded = Math.abs(labelOffset) + titleMargin;
9274 if (labelOffset) {
9275 labelOffsetPadded -= lineHeightCorrection;
9276 labelOffsetPadded += directionFactor * (horiz ? pick(labelOptions.y, axis.tickRotCorr.y + directionFactor * 8) : labelOptions.x);
9277 }
9278 axis.axisTitleMargin = pick(titleOffsetOption, labelOffsetPadded);
9279
9280 axisOffset[side] = Math.max(
9281 axisOffset[side],
9282 axis.axisTitleMargin + titleOffset + directionFactor * axis.offset,
9283 labelOffsetPadded, // #3027
9284 hasData && tickPositions.length && tickSize ? tickSize[0] : 0 // #4866
9285 );
9286
9287 // Decide the clipping needed to keep the graph inside the plot area and axis lines
9288 clip = options.offset ? 0 : Math.floor(axis.axisLine.strokeWidth() / 2) * 2; // #4308, #4371
9289 clipOffset[invertedSide] = Math.max(clipOffset[invertedSide], clip);
9290 },
9291
9292 /**
9293 * Get the path for the axis line
9294 */
9295 getLinePath: function(lineWidth) {
9296 var chart = this.chart,
9297 opposite = this.opposite,
9298 offset = this.offset,
9299 horiz = this.horiz,
9300 lineLeft = this.left + (opposite ? this.width : 0) + offset,
9301 lineTop = chart.chartHeight - this.bottom - (opposite ? this.height : 0) + offset;
9302
9303 if (opposite) {
9304 lineWidth *= -1; // crispify the other way - #1480, #1687
9305 }
9306
9307 return chart.renderer
9308 .crispLine([
9309 'M',
9310 horiz ?
9311 this.left :
9312 lineLeft,
9313 horiz ?
9314 lineTop :
9315 this.top,
9316 'L',
9317 horiz ?
9318 chart.chartWidth - this.right :
9319 lineLeft,
9320 horiz ?
9321 lineTop :
9322 chart.chartHeight - this.bottom
9323 ], lineWidth);
9324 },
9325
9326 /**
9327 * Render the axis line
9328 * @returns {[type]} [description]
9329 */
9330 renderLine: function() {
9331 if (!this.axisLine) {
9332 this.axisLine = this.chart.renderer.path()
9333 .addClass('highcharts-axis-line')
9334 .add(this.axisGroup);
9335
9336
9337 this.axisLine.attr({
9338 stroke: this.options.lineColor,
9339 'stroke-width': this.options.lineWidth,
9340 zIndex: 7
9341 });
9342
9343 }
9344 },
9345
9346 /**
9347 * Position the title
9348 */
9349 getTitlePosition: function() {
9350 // compute anchor points for each of the title align options
9351 var horiz = this.horiz,
9352 axisLeft = this.left,
9353 axisTop = this.top,
9354 axisLength = this.len,
9355 axisTitleOptions = this.options.title,
9356 margin = horiz ? axisLeft : axisTop,
9357 opposite = this.opposite,
9358 offset = this.offset,
9359 xOption = axisTitleOptions.x || 0,
9360 yOption = axisTitleOptions.y || 0,
9361 fontSize = this.chart.renderer.fontMetrics(axisTitleOptions.style && axisTitleOptions.style.fontSize, this.axisTitle).f,
9362
9363 // the position in the length direction of the axis
9364 alongAxis = {
9365 low: margin + (horiz ? 0 : axisLength),
9366 middle: margin + axisLength / 2,
9367 high: margin + (horiz ? axisLength : 0)
9368 }[axisTitleOptions.align],
9369
9370 // the position in the perpendicular direction of the axis
9371 offAxis = (horiz ? axisTop + this.height : axisLeft) +
9372 (horiz ? 1 : -1) * // horizontal axis reverses the margin
9373 (opposite ? -1 : 1) * // so does opposite axes
9374 this.axisTitleMargin +
9375 (this.side === 2 ? fontSize : 0);
9376
9377 return {
9378 x: horiz ?
9379 alongAxis + xOption : offAxis + (opposite ? this.width : 0) + offset + xOption,
9380 y: horiz ?
9381 offAxis + yOption - (opposite ? this.height : 0) + offset : alongAxis + yOption
9382 };
9383 },
9384
9385 /**
9386 * Render the axis
9387 */
9388 render: function() {
9389 var axis = this,
9390 chart = axis.chart,
9391 renderer = chart.renderer,
9392 options = axis.options,
9393 isLog = axis.isLog,
9394 lin2log = axis.lin2log,
9395 isLinked = axis.isLinked,
9396 tickPositions = axis.tickPositions,
9397 axisTitle = axis.axisTitle,
9398 ticks = axis.ticks,
9399 minorTicks = axis.minorTicks,
9400 alternateBands = axis.alternateBands,
9401 stackLabelOptions = options.stackLabels,
9402 alternateGridColor = options.alternateGridColor,
9403 tickmarkOffset = axis.tickmarkOffset,
9404 axisLine = axis.axisLine,
9405 hasRendered = chart.hasRendered,
9406 slideInTicks = hasRendered && isNumber(axis.oldMin),
9407 showAxis = axis.showAxis,
9408 animation = animObject(renderer.globalAnimation),
9409 from,
9410 to;
9411
9412 // Reset
9413 axis.labelEdge.length = 0;
9414 //axis.justifyToPlot = overflow === 'justify';
9415 axis.overlap = false;
9416
9417 // Mark all elements inActive before we go over and mark the active ones
9418 each([ticks, minorTicks, alternateBands], function(coll) {
9419 var pos;
9420 for (pos in coll) {
9421 coll[pos].isActive = false;
9422 }
9423 });
9424
9425 // If the series has data draw the ticks. Else only the line and title
9426 if (axis.hasData() || isLinked) {
9427
9428 // minor ticks
9429 if (axis.minorTickInterval && !axis.categories) {
9430 each(axis.getMinorTickPositions(), function(pos) {
9431 if (!minorTicks[pos]) {
9432 minorTicks[pos] = new Tick(axis, pos, 'minor');
9433 }
9434
9435 // render new ticks in old position
9436 if (slideInTicks && minorTicks[pos].isNew) {
9437 minorTicks[pos].render(null, true);
9438 }
9439
9440 minorTicks[pos].render(null, false, 1);
9441 });
9442 }
9443
9444 // Major ticks. Pull out the first item and render it last so that
9445 // we can get the position of the neighbour label. #808.
9446 if (tickPositions.length) { // #1300
9447 each(tickPositions, function(pos, i) {
9448
9449 // linked axes need an extra check to find out if
9450 if (!isLinked || (pos >= axis.min && pos <= axis.max)) {
9451
9452 if (!ticks[pos]) {
9453 ticks[pos] = new Tick(axis, pos);
9454 }
9455
9456 // render new ticks in old position
9457 if (slideInTicks && ticks[pos].isNew) {
9458 ticks[pos].render(i, true, 0.1);
9459 }
9460
9461 ticks[pos].render(i);
9462 }
9463
9464 });
9465 // In a categorized axis, the tick marks are displayed between labels. So
9466 // we need to add a tick mark and grid line at the left edge of the X axis.
9467 if (tickmarkOffset && (axis.min === 0 || axis.single)) {
9468 if (!ticks[-1]) {
9469 ticks[-1] = new Tick(axis, -1, null, true);
9470 }
9471 ticks[-1].render(-1);
9472 }
9473
9474 }
9475
9476 // alternate grid color
9477 if (alternateGridColor) {
9478 each(tickPositions, function(pos, i) {
9479 to = tickPositions[i + 1] !== undefined ? tickPositions[i + 1] + tickmarkOffset : axis.max - tickmarkOffset;
9480 if (i % 2 === 0 && pos < axis.max && to <= axis.max + (chart.polar ? -tickmarkOffset : tickmarkOffset)) { // #2248, #4660
9481 if (!alternateBands[pos]) {
9482 alternateBands[pos] = new PlotLineOrBand(axis);
9483 }
9484 from = pos + tickmarkOffset; // #949
9485 alternateBands[pos].options = {
9486 from: isLog ? lin2log(from) : from,
9487 to: isLog ? lin2log(to) : to,
9488 color: alternateGridColor
9489 };
9490 alternateBands[pos].render();
9491 alternateBands[pos].isActive = true;
9492 }
9493 });
9494 }
9495
9496 // custom plot lines and bands
9497 if (!axis._addedPlotLB) { // only first time
9498 each((options.plotLines || []).concat(options.plotBands || []), function(plotLineOptions) {
9499 axis.addPlotBandOrLine(plotLineOptions);
9500 });
9501 axis._addedPlotLB = true;
9502 }
9503
9504 } // end if hasData
9505
9506 // Remove inactive ticks
9507 each([ticks, minorTicks, alternateBands], function(coll) {
9508 var pos,
9509 i,
9510 forDestruction = [],
9511 delay = animation.duration,
9512 destroyInactiveItems = function() {
9513 i = forDestruction.length;
9514 while (i--) {
9515 // When resizing rapidly, the same items may be destroyed in different timeouts,
9516 // or the may be reactivated
9517 if (coll[forDestruction[i]] && !coll[forDestruction[i]].isActive) {
9518 coll[forDestruction[i]].destroy();
9519 delete coll[forDestruction[i]];
9520 }
9521 }
9522
9523 };
9524
9525 for (pos in coll) {
9526
9527 if (!coll[pos].isActive) {
9528 // Render to zero opacity
9529 coll[pos].render(pos, false, 0);
9530 coll[pos].isActive = false;
9531 forDestruction.push(pos);
9532 }
9533 }
9534
9535 // When the objects are finished fading out, destroy them
9536 syncTimeout(
9537 destroyInactiveItems,
9538 coll === alternateBands || !chart.hasRendered || !delay ? 0 : delay
9539 );
9540 });
9541
9542 // Set the axis line path
9543 if (axisLine) {
9544 axisLine[axisLine.isPlaced ? 'animate' : 'attr']({
9545 d: this.getLinePath(axisLine.strokeWidth())
9546 });
9547 axisLine.isPlaced = true;
9548
9549 // Show or hide the line depending on options.showEmpty
9550 axisLine[showAxis ? 'show' : 'hide'](true);
9551 }
9552
9553 if (axisTitle && showAxis) {
9554
9555 axisTitle[axisTitle.isNew ? 'attr' : 'animate'](
9556 axis.getTitlePosition()
9557 );
9558 axisTitle.isNew = false;
9559 }
9560
9561 // Stacked totals:
9562 if (stackLabelOptions && stackLabelOptions.enabled) {
9563 axis.renderStackTotals();
9564 }
9565 // End stacked totals
9566
9567 axis.isDirty = false;
9568 },
9569
9570 /**
9571 * Redraw the axis to reflect changes in the data or axis extremes
9572 */
9573 redraw: function() {
9574
9575 if (this.visible) {
9576 // render the axis
9577 this.render();
9578
9579 // move plot lines and bands
9580 each(this.plotLinesAndBands, function(plotLine) {
9581 plotLine.render();
9582 });
9583 }
9584
9585 // mark associated series as dirty and ready for redraw
9586 each(this.series, function(series) {
9587 series.isDirty = true;
9588 });
9589
9590 },
9591
9592 /**
9593 * Destroys an Axis instance.
9594 */
9595 destroy: function(keepEvents) {
9596 var axis = this,
9597 stacks = axis.stacks,
9598 stackKey,
9599 plotLinesAndBands = axis.plotLinesAndBands,
9600 i,
9601 n,
9602 keepProps;
9603
9604 // Remove the events
9605 if (!keepEvents) {
9606 removeEvent(axis);
9607 }
9608
9609 // Destroy each stack total
9610 for (stackKey in stacks) {
9611 destroyObjectProperties(stacks[stackKey]);
9612
9613 stacks[stackKey] = null;
9614 }
9615
9616 // Destroy collections
9617 each([axis.ticks, axis.minorTicks, axis.alternateBands], function(coll) {
9618 destroyObjectProperties(coll);
9619 });
9620 if (plotLinesAndBands) {
9621 i = plotLinesAndBands.length;
9622 while (i--) { // #1975
9623 plotLinesAndBands[i].destroy();
9624 }
9625 }
9626
9627 // Destroy local variables
9628 each(['stackTotalGroup', 'axisLine', 'axisTitle', 'axisGroup', 'gridGroup', 'labelGroup', 'cross'], function(prop) {
9629 if (axis[prop]) {
9630 axis[prop] = axis[prop].destroy();
9631 }
9632 });
9633
9634
9635 // Delete all properties and fall back to the prototype.
9636 // Preserve some properties, needed for Axis.update (#4317).
9637 keepProps = ['names', 'series', 'userMax', 'userMin'];
9638 for (n in axis) {
9639 if (axis.hasOwnProperty(n) && inArray(n, keepProps) === -1) {
9640 delete axis[n];
9641 }
9642 }
9643 },
9644
9645 /**
9646 * Draw the crosshair
9647 *
9648 * @param {Object} e The event arguments from the modified pointer event
9649 * @param {Object} point The Point object
9650 */
9651 drawCrosshair: function(e, point) {
9652
9653 var path,
9654 options = this.crosshair,
9655 pos,
9656 categorized,
9657 graphic = this.cross;
9658
9659 // Use last available event when updating non-snapped crosshairs without
9660 // mouse interaction (#5287)
9661 if (!e) {
9662 e = this.cross && this.cross.e;
9663 }
9664
9665 if (
9666 // Disabled in options
9667 !this.crosshair ||
9668 // Snap
9669 ((defined(point) || !pick(options.snap, true)) === false)
9670 ) {
9671 this.hideCrosshair();
9672 } else {
9673
9674 // Get the path
9675 if (!pick(options.snap, true)) {
9676 pos = (this.horiz ? e.chartX - this.pos : this.len - e.chartY + this.pos);
9677 } else if (defined(point)) {
9678 pos = this.isXAxis ? point.plotX : this.len - point.plotY; // #3834
9679 }
9680
9681 if (this.isRadial) {
9682 path = this.getPlotLinePath(this.isXAxis ? point.x : pick(point.stackY, point.y)) || null; // #3189
9683 } else {
9684 path = this.getPlotLinePath(null, null, null, null, pos) || null; // #3189
9685 }
9686
9687 if (path === null) {
9688 this.hideCrosshair();
9689 return;
9690 }
9691
9692 categorized = this.categories && !this.isRadial;
9693
9694 // Draw the cross
9695 if (!graphic) {
9696 this.cross = graphic = this.chart.renderer
9697 .path()
9698 .addClass('highcharts-crosshair highcharts-crosshair-' +
9699 (categorized ? 'category ' : 'thin ') + options.className)
9700 .attr({
9701 zIndex: pick(options.zIndex, 2)
9702 })
9703 .add();
9704
9705
9706 // Presentational attributes
9707 graphic.attr({
9708 'stroke': options.color || (categorized ? color('#ccd6eb').setOpacity(0.25).get() : '#cccccc'),
9709 'stroke-width': pick(options.width, 1)
9710 });
9711 if (options.dashStyle) {
9712 graphic.attr({
9713 dashstyle: options.dashStyle
9714 });
9715 }
9716
9717
9718 }
9719
9720 graphic.show().attr({
9721 d: path
9722 });
9723
9724 if (categorized) {
9725 graphic.attr({
9726 'stroke-width': this.transA
9727 });
9728 }
9729 this.cross.e = e;
9730 }
9731
9732 },
9733
9734 /**
9735 * Hide the crosshair.
9736 */
9737 hideCrosshair: function() {
9738 if (this.cross) {
9739 this.cross.hide();
9740 }
9741 }
9742 }; // end Axis
9743
9744 extend(H.Axis.prototype, AxisPlotLineOrBandExtension);
9745
9746 }(Highcharts));
9747 (function(H) {
9748 /**
9749 * (c) 2010-2016 Torstein Honsi
9750 *
9751 * License: www.highcharts.com/license
9752 */
9753 'use strict';
9754 var Axis = H.Axis,
9755 getMagnitude = H.getMagnitude,
9756 map = H.map,
9757 normalizeTickInterval = H.normalizeTickInterval,
9758 pick = H.pick;
9759 /**
9760 * Methods defined on the Axis prototype
9761 */
9762
9763 /**
9764 * Set the tick positions of a logarithmic axis
9765 */
9766 Axis.prototype.getLogTickPositions = function(interval, min, max, minor) {
9767 var axis = this,
9768 options = axis.options,
9769 axisLength = axis.len,
9770 lin2log = axis.lin2log,
9771 log2lin = axis.log2lin,
9772 // Since we use this method for both major and minor ticks,
9773 // use a local variable and return the result
9774 positions = [];
9775
9776 // Reset
9777 if (!minor) {
9778 axis._minorAutoInterval = null;
9779 }
9780
9781 // First case: All ticks fall on whole logarithms: 1, 10, 100 etc.
9782 if (interval >= 0.5) {
9783 interval = Math.round(interval);
9784 positions = axis.getLinearTickPositions(interval, min, max);
9785
9786 // Second case: We need intermediary ticks. For example
9787 // 1, 2, 4, 6, 8, 10, 20, 40 etc.
9788 } else if (interval >= 0.08) {
9789 var roundedMin = Math.floor(min),
9790 intermediate,
9791 i,
9792 j,
9793 len,
9794 pos,
9795 lastPos,
9796 break2;
9797
9798 if (interval > 0.3) {
9799 intermediate = [1, 2, 4];
9800 } else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc
9801 intermediate = [1, 2, 4, 6, 8];
9802 } else { // 0.1 equals ten minor ticks per 1, 10, 100 etc
9803 intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
9804 }
9805
9806 for (i = roundedMin; i < max + 1 && !break2; i++) {
9807 len = intermediate.length;
9808 for (j = 0; j < len && !break2; j++) {
9809 pos = log2lin(lin2log(i) * intermediate[j]);
9810 if (pos > min && (!minor || lastPos <= max) && lastPos !== undefined) { // #1670, lastPos is #3113
9811 positions.push(lastPos);
9812 }
9813
9814 if (lastPos > max) {
9815 break2 = true;
9816 }
9817 lastPos = pos;
9818 }
9819 }
9820
9821 // Third case: We are so deep in between whole logarithmic values that
9822 // we might as well handle the tick positions like a linear axis. For
9823 // example 1.01, 1.02, 1.03, 1.04.
9824 } else {
9825 var realMin = lin2log(min),
9826 realMax = lin2log(max),
9827 tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'],
9828 filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption,
9829 tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1),
9830 totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength;
9831
9832 interval = pick(
9833 filteredTickIntervalOption,
9834 axis._minorAutoInterval,
9835 (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1)
9836 );
9837
9838 interval = normalizeTickInterval(
9839 interval,
9840 null,
9841 getMagnitude(interval)
9842 );
9843
9844 positions = map(axis.getLinearTickPositions(
9845 interval,
9846 realMin,
9847 realMax
9848 ), log2lin);
9849
9850 if (!minor) {
9851 axis._minorAutoInterval = interval / 5;
9852 }
9853 }
9854
9855 // Set the axis-level tickInterval variable
9856 if (!minor) {
9857 axis.tickInterval = interval;
9858 }
9859 return positions;
9860 };
9861
9862 Axis.prototype.log2lin = function(num) {
9863 return Math.log(num) / Math.LN10;
9864 };
9865
9866 Axis.prototype.lin2log = function(num) {
9867 return Math.pow(10, num);
9868 };
9869
9870 }(Highcharts));
9871 (function(H) {
9872 /**
9873 * (c) 2010-2016 Torstein Honsi
9874 *
9875 * License: www.highcharts.com/license
9876 */
9877 'use strict';
9878 var addEvent = H.addEvent,
9879 dateFormat = H.dateFormat,
9880 each = H.each,
9881 extend = H.extend,
9882 format = H.format,
9883 isNumber = H.isNumber,
9884 map = H.map,
9885 merge = H.merge,
9886 pick = H.pick,
9887 splat = H.splat,
9888 stop = H.stop,
9889 syncTimeout = H.syncTimeout,
9890 timeUnits = H.timeUnits;
9891 /**
9892 * The tooltip object
9893 * @param {Object} chart The chart instance
9894 * @param {Object} options Tooltip options
9895 */
9896 H.Tooltip = function() {
9897 this.init.apply(this, arguments);
9898 };
9899
9900 H.Tooltip.prototype = {
9901
9902 init: function(chart, options) {
9903
9904 // Save the chart and options
9905 this.chart = chart;
9906 this.options = options;
9907
9908 // Keep track of the current series
9909 //this.currentSeries = undefined;
9910
9911 // List of crosshairs
9912 this.crosshairs = [];
9913
9914 // Current values of x and y when animating
9915 this.now = {
9916 x: 0,
9917 y: 0
9918 };
9919
9920 // The tooltip is initially hidden
9921 this.isHidden = true;
9922
9923
9924
9925 // Public property for getting the shared state.
9926 this.split = options.split && !chart.inverted;
9927 this.shared = options.shared || this.split;
9928
9929
9930 // Create the label
9931 if (this.split) {
9932 this.label = this.chart.renderer.g('tooltip');
9933 } else {
9934 this.label = chart.renderer.label(
9935 '',
9936 0,
9937 0,
9938 options.shape || 'callout',
9939 null,
9940 null,
9941 options.useHTML,
9942 null,
9943 'tooltip'
9944 )
9945 .attr({
9946 padding: options.padding,
9947 r: options.borderRadius,
9948 display: 'none' // #2301, #2657, #3532, #5570
9949 });
9950
9951
9952 this.label
9953 .attr({
9954 'fill': options.backgroundColor,
9955 'stroke-width': options.borderWidth
9956 })
9957 // #2301, #2657
9958 .css(options.style)
9959 .shadow(options.shadow);
9960
9961 }
9962 this.label.attr({
9963 zIndex: 8
9964 })
9965 .add();
9966 },
9967
9968 update: function(options) {
9969 this.destroy();
9970 this.init(this.chart, merge(true, this.options, options));
9971 },
9972
9973 /**
9974 * Destroy the tooltip and its elements.
9975 */
9976 destroy: function() {
9977 // Destroy and clear local variables
9978 if (this.label) {
9979 this.label = this.label.destroy();
9980 }
9981 clearTimeout(this.hideTimer);
9982 clearTimeout(this.tooltipTimeout);
9983 },
9984
9985 /**
9986 * Provide a soft movement for the tooltip
9987 *
9988 * @param {Number} x
9989 * @param {Number} y
9990 * @private
9991 */
9992 move: function(x, y, anchorX, anchorY) {
9993 var tooltip = this,
9994 now = tooltip.now,
9995 animate = tooltip.options.animation !== false && !tooltip.isHidden &&
9996 // When we get close to the target position, abort animation and land on the right place (#3056)
9997 (Math.abs(x - now.x) > 1 || Math.abs(y - now.y) > 1),
9998 skipAnchor = tooltip.followPointer || tooltip.len > 1;
9999
10000 // Get intermediate values for animation
10001 extend(now, {
10002 x: animate ? (2 * now.x + x) / 3 : x,
10003 y: animate ? (now.y + y) / 2 : y,
10004 anchorX: skipAnchor ? undefined : animate ? (2 * now.anchorX + anchorX) / 3 : anchorX,
10005 anchorY: skipAnchor ? undefined : animate ? (now.anchorY + anchorY) / 2 : anchorY
10006 });
10007
10008 // Move to the intermediate value
10009 tooltip.label.attr(now);
10010
10011
10012 // Run on next tick of the mouse tracker
10013 if (animate) {
10014
10015 // Never allow two timeouts
10016 clearTimeout(this.tooltipTimeout);
10017
10018 // Set the fixed interval ticking for the smooth tooltip
10019 this.tooltipTimeout = setTimeout(function() {
10020 // The interval function may still be running during destroy, so check that the chart is really there before calling.
10021 if (tooltip) {
10022 tooltip.move(x, y, anchorX, anchorY);
10023 }
10024 }, 32);
10025
10026 }
10027 },
10028
10029 /**
10030 * Hide the tooltip
10031 */
10032 hide: function(delay) {
10033 var tooltip = this;
10034 clearTimeout(this.hideTimer); // disallow duplicate timers (#1728, #1766)
10035 delay = pick(delay, this.options.hideDelay, 500);
10036 if (!this.isHidden) {
10037 this.hideTimer = syncTimeout(function() {
10038 tooltip.label[delay ? 'fadeOut' : 'hide']();
10039 tooltip.isHidden = true;
10040 }, delay);
10041 }
10042 },
10043
10044 /**
10045 * Extendable method to get the anchor position of the tooltip
10046 * from a point or set of points
10047 */
10048 getAnchor: function(points, mouseEvent) {
10049 var ret,
10050 chart = this.chart,
10051 inverted = chart.inverted,
10052 plotTop = chart.plotTop,
10053 plotLeft = chart.plotLeft,
10054 plotX = 0,
10055 plotY = 0,
10056 yAxis,
10057 xAxis;
10058
10059 points = splat(points);
10060
10061 // Pie uses a special tooltipPos
10062 ret = points[0].tooltipPos;
10063
10064 // When tooltip follows mouse, relate the position to the mouse
10065 if (this.followPointer && mouseEvent) {
10066 if (mouseEvent.chartX === undefined) {
10067 mouseEvent = chart.pointer.normalize(mouseEvent);
10068 }
10069 ret = [
10070 mouseEvent.chartX - chart.plotLeft,
10071 mouseEvent.chartY - plotTop
10072 ];
10073 }
10074 // When shared, use the average position
10075 if (!ret) {
10076 each(points, function(point) {
10077 yAxis = point.series.yAxis;
10078 xAxis = point.series.xAxis;
10079 plotX += point.plotX + (!inverted && xAxis ? xAxis.left - plotLeft : 0);
10080 plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) +
10081 (!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151
10082 });
10083
10084 plotX /= points.length;
10085 plotY /= points.length;
10086
10087 ret = [
10088 inverted ? chart.plotWidth - plotY : plotX,
10089 this.shared && !inverted && points.length > 1 && mouseEvent ?
10090 mouseEvent.chartY - plotTop : // place shared tooltip next to the mouse (#424)
10091 inverted ? chart.plotHeight - plotX : plotY
10092 ];
10093 }
10094
10095 return map(ret, Math.round);
10096 },
10097
10098 /**
10099 * Place the tooltip in a chart without spilling over
10100 * and not covering the point it self.
10101 */
10102 getPosition: function(boxWidth, boxHeight, point) {
10103
10104 var chart = this.chart,
10105 distance = this.distance,
10106 ret = {},
10107 h = point.h || 0, // #4117
10108 swapped,
10109 first = ['y', chart.chartHeight, boxHeight, point.plotY + chart.plotTop, chart.plotTop, chart.plotTop + chart.plotHeight],
10110 second = ['x', chart.chartWidth, boxWidth, point.plotX + chart.plotLeft, chart.plotLeft, chart.plotLeft + chart.plotWidth],
10111 // The far side is right or bottom
10112 preferFarSide = !this.followPointer && pick(point.ttBelow, !chart.inverted === !!point.negative), // #4984
10113 /**
10114 * Handle the preferred dimension. When the preferred dimension is tooltip
10115 * on top or bottom of the point, it will look for space there.
10116 */
10117 firstDimension = function(dim, outerSize, innerSize, point, min, max) {
10118 var roomLeft = innerSize < point - distance,
10119 roomRight = point + distance + innerSize < outerSize,
10120 alignedLeft = point - distance - innerSize,
10121 alignedRight = point + distance;
10122
10123 if (preferFarSide && roomRight) {
10124 ret[dim] = alignedRight;
10125 } else if (!preferFarSide && roomLeft) {
10126 ret[dim] = alignedLeft;
10127 } else if (roomLeft) {
10128 ret[dim] = Math.min(max - innerSize, alignedLeft - h < 0 ? alignedLeft : alignedLeft - h);
10129 } else if (roomRight) {
10130 ret[dim] = Math.max(min, alignedRight + h + innerSize > outerSize ? alignedRight : alignedRight + h);
10131 } else {
10132 return false;
10133 }
10134 },
10135 /**
10136 * Handle the secondary dimension. If the preferred dimension is tooltip
10137 * on top or bottom of the point, the second dimension is to align the tooltip
10138 * above the point, trying to align center but allowing left or right
10139 * align within the chart box.
10140 */
10141 secondDimension = function(dim, outerSize, innerSize, point) {
10142 var retVal;
10143
10144 // Too close to the edge, return false and swap dimensions
10145 if (point < distance || point > outerSize - distance) {
10146 retVal = false;
10147 // Align left/top
10148 } else if (point < innerSize / 2) {
10149 ret[dim] = 1;
10150 // Align right/bottom
10151 } else if (point > outerSize - innerSize / 2) {
10152 ret[dim] = outerSize - innerSize - 2;
10153 // Align center
10154 } else {
10155 ret[dim] = point - innerSize / 2;
10156 }
10157 return retVal;
10158 },
10159 /**
10160 * Swap the dimensions
10161 */
10162 swap = function(count) {
10163 var temp = first;
10164 first = second;
10165 second = temp;
10166 swapped = count;
10167 },
10168 run = function() {
10169 if (firstDimension.apply(0, first) !== false) {
10170 if (secondDimension.apply(0, second) === false && !swapped) {
10171 swap(true);
10172 run();
10173 }
10174 } else if (!swapped) {
10175 swap(true);
10176 run();
10177 } else {
10178 ret.x = ret.y = 0;
10179 }
10180 };
10181
10182 // Under these conditions, prefer the tooltip on the side of the point
10183 if (chart.inverted || this.len > 1) {
10184 swap();
10185 }
10186 run();
10187
10188 return ret;
10189
10190 },
10191
10192 /**
10193 * In case no user defined formatter is given, this will be used. Note that the context
10194 * here is an object holding point, series, x, y etc.
10195 *
10196 * @returns {String|Array<String>}
10197 */
10198 defaultFormatter: function(tooltip) {
10199 var items = this.points || splat(this),
10200 s;
10201
10202 // build the header
10203 s = [tooltip.tooltipFooterHeaderFormatter(items[0])]; //#3397: abstraction to enable formatting of footer and header
10204
10205 // build the values
10206 s = s.concat(tooltip.bodyFormatter(items));
10207
10208 // footer
10209 s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true)); //#3397: abstraction to enable formatting of footer and header
10210
10211 return s;
10212 },
10213
10214 /**
10215 * Refresh the tooltip's text and position.
10216 * @param {Object} point
10217 */
10218 refresh: function(point, mouseEvent) {
10219 var tooltip = this,
10220 chart = tooltip.chart,
10221 label = tooltip.label,
10222 options = tooltip.options,
10223 x,
10224 y,
10225 anchor,
10226 textConfig = {},
10227 text,
10228 pointConfig = [],
10229 formatter = options.formatter || tooltip.defaultFormatter,
10230 hoverPoints = chart.hoverPoints,
10231 shared = tooltip.shared,
10232 currentSeries;
10233
10234 clearTimeout(this.hideTimer);
10235
10236 // get the reference point coordinates (pie charts use tooltipPos)
10237 tooltip.followPointer = splat(point)[0].series.tooltipOptions.followPointer;
10238 anchor = tooltip.getAnchor(point, mouseEvent);
10239 x = anchor[0];
10240 y = anchor[1];
10241
10242 // shared tooltip, array is sent over
10243 if (shared && !(point.series && point.series.noSharedTooltip)) {
10244
10245 // hide previous hoverPoints and set new
10246
10247 chart.hoverPoints = point;
10248 if (hoverPoints) {
10249 each(hoverPoints, function(point) {
10250 point.setState();
10251 });
10252 }
10253
10254 each(point, function(item) {
10255 item.setState('hover');
10256
10257 pointConfig.push(item.getLabelConfig());
10258 });
10259
10260 textConfig = {
10261 x: point[0].category,
10262 y: point[0].y
10263 };
10264 textConfig.points = pointConfig;
10265 this.len = pointConfig.length;
10266 point = point[0];
10267
10268 // single point tooltip
10269 } else {
10270 textConfig = point.getLabelConfig();
10271 }
10272 text = formatter.call(textConfig, tooltip);
10273
10274 // register the current series
10275 currentSeries = point.series;
10276 this.distance = pick(currentSeries.tooltipOptions.distance, 16);
10277
10278 // update the inner HTML
10279 if (text === false) {
10280 this.hide();
10281 } else {
10282
10283 // show it
10284 if (tooltip.isHidden) {
10285 stop(label);
10286 label.attr({
10287 opacity: 1,
10288 display: 'block'
10289 }).show();
10290 }
10291
10292 // update text
10293 if (tooltip.split) {
10294 this.renderSplit(text, chart.hoverPoints);
10295 } else {
10296 label.attr({
10297 text: text.join ? text.join('') : text
10298 });
10299
10300 // Set the stroke color of the box to reflect the point
10301 label.removeClass(/highcharts-color-[\d]+/g)
10302 .addClass('highcharts-color-' + pick(point.colorIndex, currentSeries.colorIndex));
10303
10304
10305 label.attr({
10306 stroke: options.borderColor || point.color || currentSeries.color || '#666666'
10307 });
10308
10309
10310 tooltip.updatePosition({
10311 plotX: x,
10312 plotY: y,
10313 negative: point.negative,
10314 ttBelow: point.ttBelow,
10315 h: anchor[2] || 0
10316 });
10317 }
10318
10319 this.isHidden = false;
10320 }
10321 },
10322
10323 /**
10324 * Render the split tooltip. Loops over each point's text and adds
10325 * a label next to the point, then uses the distribute function to
10326 * find best non-overlapping positions.
10327 */
10328 renderSplit: function(labels, points) {
10329 var tooltip = this,
10330 boxes = [],
10331 chart = this.chart,
10332 ren = chart.renderer,
10333 rightAligned = true,
10334 options = this.options,
10335 headerHeight;
10336
10337 /**
10338 * Destroy a single-series tooltip
10339 */
10340 function destroy(tt) {
10341 tt.connector = tt.connector.destroy();
10342 tt.destroy();
10343 }
10344
10345 // Create the individual labels
10346 each(labels.slice(0, labels.length - 1), function(str, i) {
10347 var point = points[i - 1] ||
10348 // Item 0 is the header. Instead of this, we could also use the crosshair label
10349 {
10350 isHeader: true,
10351 plotX: points[0].plotX
10352 },
10353 owner = point.series || tooltip,
10354 tt = owner.tt,
10355 series = point.series || {},
10356 colorClass = 'highcharts-color-' + pick(point.colorIndex, series.colorIndex, 'none'),
10357 target,
10358 x,
10359 bBox;
10360
10361 // Store the tooltip referance on the series
10362 if (!tt) {
10363 owner.tt = tt = ren.label(null, null, null, point.isHeader && 'callout')
10364 .addClass('highcharts-tooltip-box ' + colorClass)
10365 .attr({
10366 'padding': options.padding,
10367 'r': options.borderRadius,
10368
10369 'fill': options.backgroundColor,
10370 'stroke': point.color || series.color || '#333333',
10371 'stroke-width': options.borderWidth
10372
10373 })
10374 .add(tooltip.label);
10375
10376 // Add a connector back to the point
10377 if (point.series) {
10378 tt.connector = ren.path()
10379 .addClass('highcharts-tooltip-connector ' + colorClass)
10380
10381 .attr({
10382 'stroke-width': series.options.lineWidth || 2,
10383 'stroke': point.color || series.color || '#666666'
10384 })
10385
10386 .add(tooltip.label);
10387
10388 addEvent(point.series, 'hide', function() {
10389 this.tt = destroy(this.tt);
10390 });
10391 }
10392 }
10393 tt.isActive = true;
10394 tt.attr({
10395 text: str
10396 });
10397
10398 // Get X position now, so we can move all to the other side in case of overflow
10399 bBox = tt.getBBox();
10400 if (point.isHeader) {
10401 headerHeight = bBox.height;
10402 x = point.plotX + chart.plotLeft - bBox.width / 2;
10403 } else {
10404 x = point.plotX + chart.plotLeft - pick(options.distance, 16) -
10405 bBox.width;
10406 }
10407
10408
10409 // If overflow left, we don't use this x in the next loop
10410 if (x < 0) {
10411 rightAligned = false;
10412 }
10413
10414 // Prepare for distribution
10415 target = (point.series && point.series.yAxis && point.series.yAxis.pos) + (point.plotY || 0);
10416 target -= chart.plotTop;
10417 boxes.push({
10418 target: point.isHeader ? chart.plotHeight + headerHeight : target,
10419 rank: point.isHeader ? 1 : 0,
10420 size: owner.tt.getBBox().height + 1,
10421 point: point,
10422 x: x,
10423 tt: tt
10424 });
10425 });
10426
10427 // Clean previous run (for missing points)
10428 each(chart.series, function(series) {
10429 var tt = series.tt;
10430 if (tt) {
10431 if (!tt.isActive) {
10432 series.tt = destroy(tt);
10433 } else {
10434 tt.isActive = false;
10435 }
10436 }
10437 });
10438
10439 // Distribute and put in place
10440 H.distribute(boxes, chart.plotHeight + headerHeight);
10441 each(boxes, function(box) {
10442 var point = box.point,
10443 tt = box.tt,
10444 attr;
10445
10446 // Put the label in place
10447 attr = {
10448 display: box.pos === undefined ? 'none' : '',
10449 x: (rightAligned || point.isHeader ? box.x : point.plotX + chart.plotLeft + pick(options.distance, 16)),
10450 y: box.pos + chart.plotTop
10451 };
10452 if (point.isHeader) {
10453 attr.anchorX = point.plotX + chart.plotLeft;
10454 attr.anchorY = attr.y - 100;
10455 }
10456 tt.attr(attr);
10457
10458 // Draw the connector to the point
10459 if (!point.isHeader) {
10460 tt.connector.attr({
10461 d: [
10462 'M',
10463 point.plotX + chart.plotLeft,
10464 point.plotY + point.series.yAxis.pos,
10465 'L',
10466 rightAligned ?
10467 point.plotX + chart.plotLeft - pick(options.distance, 16) :
10468 point.plotX + chart.plotLeft + pick(options.distance, 16),
10469 box.pos + chart.plotTop + tt.getBBox().height / 2
10470 ]
10471 });
10472 }
10473 });
10474 },
10475
10476 /**
10477 * Find the new position and perform the move
10478 */
10479 updatePosition: function(point) {
10480 var chart = this.chart,
10481 label = this.label,
10482 pos = (this.options.positioner || this.getPosition).call(
10483 this,
10484 label.width,
10485 label.height,
10486 point
10487 );
10488
10489 // do the move
10490 this.move(
10491 Math.round(pos.x),
10492 Math.round(pos.y || 0), // can be undefined (#3977)
10493 point.plotX + chart.plotLeft,
10494 point.plotY + chart.plotTop
10495 );
10496 },
10497
10498 /**
10499 * Get the best X date format based on the closest point range on the axis.
10500 */
10501 getXDateFormat: function(point, options, xAxis) {
10502 var xDateFormat,
10503 dateTimeLabelFormats = options.dateTimeLabelFormats,
10504 closestPointRange = xAxis && xAxis.closestPointRange,
10505 n,
10506 blank = '01-01 00:00:00.000',
10507 strpos = {
10508 millisecond: 15,
10509 second: 12,
10510 minute: 9,
10511 hour: 6,
10512 day: 3
10513 },
10514 date,
10515 lastN = 'millisecond'; // for sub-millisecond data, #4223
10516
10517 if (closestPointRange) {
10518 date = dateFormat('%m-%d %H:%M:%S.%L', point.x);
10519 for (n in timeUnits) {
10520
10521 // If the range is exactly one week and we're looking at a Sunday/Monday, go for the week format
10522 if (closestPointRange === timeUnits.week && +dateFormat('%w', point.x) === xAxis.options.startOfWeek &&
10523 date.substr(6) === blank.substr(6)) {
10524 n = 'week';
10525 break;
10526 }
10527
10528 // The first format that is too great for the range
10529 if (timeUnits[n] > closestPointRange) {
10530 n = lastN;
10531 break;
10532 }
10533
10534 // If the point is placed every day at 23:59, we need to show
10535 // the minutes as well. #2637.
10536 if (strpos[n] && date.substr(strpos[n]) !== blank.substr(strpos[n])) {
10537 break;
10538 }
10539
10540 // Weeks are outside the hierarchy, only apply them on Mondays/Sundays like in the first condition
10541 if (n !== 'week') {
10542 lastN = n;
10543 }
10544 }
10545
10546 if (n) {
10547 xDateFormat = dateTimeLabelFormats[n];
10548 }
10549 } else {
10550 xDateFormat = dateTimeLabelFormats.day;
10551 }
10552
10553 return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581
10554 },
10555
10556 /**
10557 * Format the footer/header of the tooltip
10558 * #3397: abstraction to enable formatting of footer and header
10559 */
10560 tooltipFooterHeaderFormatter: function(labelConfig, isFooter) {
10561 var footOrHead = isFooter ? 'footer' : 'header',
10562 series = labelConfig.series,
10563 tooltipOptions = series.tooltipOptions,
10564 xDateFormat = tooltipOptions.xDateFormat,
10565 xAxis = series.xAxis,
10566 isDateTime = xAxis && xAxis.options.type === 'datetime' && isNumber(labelConfig.key),
10567 formatString = tooltipOptions[footOrHead + 'Format'];
10568
10569 // Guess the best date format based on the closest point distance (#568, #3418)
10570 if (isDateTime && !xDateFormat) {
10571 xDateFormat = this.getXDateFormat(labelConfig, tooltipOptions, xAxis);
10572 }
10573
10574 // Insert the footer date format if any
10575 if (isDateTime && xDateFormat) {
10576 formatString = formatString.replace('{point.key}', '{point.key:' + xDateFormat + '}');
10577 }
10578
10579 return format(formatString, {
10580 point: labelConfig,
10581 series: series
10582 });
10583 },
10584
10585 /**
10586 * Build the body (lines) of the tooltip by iterating over the items and returning one entry for each item,
10587 * abstracting this functionality allows to easily overwrite and extend it.
10588 */
10589 bodyFormatter: function(items) {
10590 return map(items, function(item) {
10591 var tooltipOptions = item.series.tooltipOptions;
10592 return (tooltipOptions.pointFormatter || item.point.tooltipFormatter).call(item.point, tooltipOptions.pointFormat);
10593 });
10594 }
10595
10596 };
10597
10598 }(Highcharts));
10599 (function(H) {
10600 /**
10601 * (c) 2010-2016 Torstein Honsi
10602 *
10603 * License: www.highcharts.com/license
10604 */
10605 'use strict';
10606 var addEvent = H.addEvent,
10607 attr = H.attr,
10608 charts = H.charts,
10609 color = H.color,
10610 css = H.css,
10611 defined = H.defined,
10612 doc = H.doc,
10613 each = H.each,
10614 extend = H.extend,
10615 fireEvent = H.fireEvent,
10616 offset = H.offset,
10617 pick = H.pick,
10618 removeEvent = H.removeEvent,
10619 splat = H.splat,
10620 Tooltip = H.Tooltip,
10621 win = H.win;
10622
10623 // Global flag for touch support
10624 H.hasTouch = doc && doc.documentElement.ontouchstart !== undefined;
10625
10626 /**
10627 * The mouse tracker object. All methods starting with "on" are primary DOM event handlers.
10628 * Subsequent methods should be named differently from what they are doing.
10629 * @param {Object} chart The Chart instance
10630 * @param {Object} options The root options object
10631 */
10632 H.Pointer = function(chart, options) {
10633 this.init(chart, options);
10634 };
10635
10636 H.Pointer.prototype = {
10637 /**
10638 * Initialize Pointer
10639 */
10640 init: function(chart, options) {
10641
10642 // Store references
10643 this.options = options;
10644 this.chart = chart;
10645
10646 // Do we need to handle click on a touch device?
10647 this.runChartClick = options.chart.events && !!options.chart.events.click;
10648
10649 this.pinchDown = [];
10650 this.lastValidTouch = {};
10651
10652 if (Tooltip && options.tooltip.enabled) {
10653 chart.tooltip = new Tooltip(chart, options.tooltip);
10654 this.followTouchMove = pick(options.tooltip.followTouchMove, true);
10655 }
10656
10657 this.setDOMEvents();
10658 },
10659
10660 /**
10661 * Resolve the zoomType option
10662 */
10663 zoomOption: function() {
10664 var chart = this.chart,
10665 zoomType = chart.options.chart.zoomType,
10666 zoomX = /x/.test(zoomType),
10667 zoomY = /y/.test(zoomType),
10668 inverted = chart.inverted;
10669
10670 this.zoomX = zoomX;
10671 this.zoomY = zoomY;
10672 this.zoomHor = (zoomX && !inverted) || (zoomY && inverted);
10673 this.zoomVert = (zoomY && !inverted) || (zoomX && inverted);
10674 this.hasZoom = zoomX || zoomY;
10675 },
10676
10677 /**
10678 * Add crossbrowser support for chartX and chartY
10679 * @param {Object} e The event object in standard browsers
10680 */
10681 normalize: function(e, chartPosition) {
10682 var chartX,
10683 chartY,
10684 ePos;
10685
10686 // IE normalizing
10687 e = e || win.event;
10688 if (!e.target) {
10689 e.target = e.srcElement;
10690 }
10691
10692 // iOS (#2757)
10693 ePos = e.touches ? (e.touches.length ? e.touches.item(0) : e.changedTouches[0]) : e;
10694
10695 // Get mouse position
10696 if (!chartPosition) {
10697 this.chartPosition = chartPosition = offset(this.chart.container);
10698 }
10699
10700 // chartX and chartY
10701 if (ePos.pageX === undefined) { // IE < 9. #886.
10702 chartX = Math.max(e.x, e.clientX - chartPosition.left); // #2005, #2129: the second case is
10703 // for IE10 quirks mode within framesets
10704 chartY = e.y;
10705 } else {
10706 chartX = ePos.pageX - chartPosition.left;
10707 chartY = ePos.pageY - chartPosition.top;
10708 }
10709
10710 return extend(e, {
10711 chartX: Math.round(chartX),
10712 chartY: Math.round(chartY)
10713 });
10714 },
10715
10716 /**
10717 * Get the click position in terms of axis values.
10718 *
10719 * @param {Object} e A pointer event
10720 */
10721 getCoordinates: function(e) {
10722 var coordinates = {
10723 xAxis: [],
10724 yAxis: []
10725 };
10726
10727 each(this.chart.axes, function(axis) {
10728 coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({
10729 axis: axis,
10730 value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY'])
10731 });
10732 });
10733 return coordinates;
10734 },
10735
10736 /**
10737 * With line type charts with a single tracker, get the point closest to the mouse.
10738 * Run Point.onMouseOver and display tooltip for the point or points.
10739 */
10740 runPointActions: function(e) {
10741
10742 var pointer = this,
10743 chart = pointer.chart,
10744 series = chart.series,
10745 tooltip = chart.tooltip,
10746 shared = tooltip ? tooltip.shared : false,
10747 followPointer,
10748 updatePosition = true,
10749 hoverPoint = chart.hoverPoint,
10750 hoverSeries = chart.hoverSeries,
10751 i,
10752 anchor,
10753 noSharedTooltip,
10754 stickToHoverSeries,
10755 directTouch,
10756 kdpoints = [],
10757 kdpointT;
10758
10759 // For hovering over the empty parts of the plot area (hoverSeries is undefined).
10760 // If there is one series with point tracking (combo chart), don't go to nearest neighbour.
10761 if (!shared && !hoverSeries) {
10762 for (i = 0; i < series.length; i++) {
10763 if (series[i].directTouch || !series[i].options.stickyTracking) {
10764 series = [];
10765 }
10766 }
10767 }
10768
10769 // If it has a hoverPoint and that series requires direct touch (like columns, #3899), or we're on
10770 // a noSharedTooltip series among shared tooltip series (#4546), use the hoverPoint . Otherwise,
10771 // search the k-d tree.
10772 stickToHoverSeries = hoverSeries && (shared ? hoverSeries.noSharedTooltip : hoverSeries.directTouch);
10773 if (stickToHoverSeries && hoverPoint) {
10774 kdpoints = [hoverPoint];
10775
10776 // Handle shared tooltip or cases where a series is not yet hovered
10777 } else {
10778 // When we have non-shared tooltip and sticky tracking is disabled,
10779 // search for the closest point only on hovered series: #5533, #5476
10780 if (!shared && hoverSeries && !hoverSeries.options.stickyTracking) {
10781 series = [hoverSeries];
10782 }
10783 // Find nearest points on all series
10784 each(series, function(s) {
10785 // Skip hidden series
10786 noSharedTooltip = s.noSharedTooltip && shared;
10787 directTouch = !shared && s.directTouch;
10788 if (s.visible && !noSharedTooltip && !directTouch && pick(s.options.enableMouseTracking, true)) { // #3821
10789 kdpointT = s.searchPoint(e, !noSharedTooltip && s.kdDimensions === 1); // #3828
10790 if (kdpointT && kdpointT.series) { // Point.series becomes null when reset and before redraw (#5197)
10791 kdpoints.push(kdpointT);
10792 }
10793 }
10794 });
10795
10796 // Sort kdpoints by distance to mouse pointer
10797 kdpoints.sort(function(p1, p2) {
10798 var isCloserX = p1.distX - p2.distX,
10799 isCloser = p1.dist - p2.dist,
10800 isAbove = p1.series.group.zIndex > p2.series.group.zIndex ? -1 : 1;
10801 // We have two points which are not in the same place on xAxis and shared tooltip:
10802 if (isCloserX !== 0) {
10803 return isCloserX;
10804 }
10805 // Points are not exactly in the same place on x/yAxis:
10806 if (isCloser !== 0) {
10807 return isCloser;
10808 }
10809 // The same xAxis and yAxis position, sort by z-index:
10810 return isAbove;
10811 });
10812 }
10813
10814 // Remove points with different x-positions, required for shared tooltip and crosshairs (#4645):
10815 if (shared) {
10816 i = kdpoints.length;
10817 while (i--) {
10818 if (kdpoints[i].clientX !== kdpoints[0].clientX || kdpoints[i].series.noSharedTooltip) {
10819 kdpoints.splice(i, 1);
10820 }
10821 }
10822 }
10823
10824 // Refresh tooltip for kdpoint if new hover point or tooltip was hidden // #3926, #4200
10825 if (kdpoints[0] && (kdpoints[0] !== pointer.hoverPoint || (tooltip && tooltip.isHidden))) {
10826 // Draw tooltip if necessary
10827 if (shared && !kdpoints[0].series.noSharedTooltip) {
10828 // Do mouseover on all points (#3919, #3985, #4410)
10829 for (i = 0; i >= 0; i--) {
10830 kdpoints[i].onMouseOver(e, kdpoints[i] !== ((hoverSeries && hoverSeries.directTouch && hoverPoint) || kdpoints[0]));
10831 }
10832 // Make sure that the hoverPoint and hoverSeries are stored for events (e.g. click), #5622
10833 if (hoverSeries && hoverSeries.directTouch && hoverPoint && hoverPoint !== kdpoints[0]) {
10834 hoverPoint.onMouseOver(e, false);
10835 }
10836 if (kdpoints.length && tooltip) {
10837 // Keep the order of series in tooltip:
10838 tooltip.refresh(kdpoints.sort(function(p1, p2) {
10839 return p1.series.index - p2.series.index;
10840 }), e);
10841 }
10842 } else {
10843 if (tooltip) {
10844 tooltip.refresh(kdpoints[0], e);
10845 }
10846 if (!hoverSeries || !hoverSeries.directTouch) { // #4448
10847 kdpoints[0].onMouseOver(e);
10848 }
10849 }
10850 pointer.prevKDPoint = kdpoints[0];
10851 updatePosition = false;
10852 }
10853 // Update positions (regardless of kdpoint or hoverPoint)
10854 if (updatePosition) {
10855 followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer;
10856 if (tooltip && followPointer && !tooltip.isHidden) {
10857 anchor = tooltip.getAnchor([{}], e);
10858 tooltip.updatePosition({
10859 plotX: anchor[0],
10860 plotY: anchor[1]
10861 });
10862 }
10863 }
10864
10865 // Start the event listener to pick up the tooltip and crosshairs
10866 if (!pointer._onDocumentMouseMove) {
10867 pointer._onDocumentMouseMove = function(e) {
10868 if (charts[H.hoverChartIndex]) {
10869 charts[H.hoverChartIndex].pointer.onDocumentMouseMove(e);
10870 }
10871 };
10872 addEvent(doc, 'mousemove', pointer._onDocumentMouseMove);
10873 }
10874
10875 // Crosshair. For each hover point, loop over axes and draw cross if that point
10876 // belongs to the axis (#4927).
10877 each(shared ? kdpoints : [pick(hoverPoint, kdpoints[0])], function drawPointCrosshair(point) { // #5269
10878 each(chart.axes, function drawAxisCrosshair(axis) {
10879 // In case of snap = false, point is undefined, and we draw the crosshair anyway (#5066)
10880 if (!point || point.series && point.series[axis.coll] === axis) { // #5658
10881 axis.drawCrosshair(e, point);
10882 }
10883 });
10884 });
10885 },
10886
10887 /**
10888 * Reset the tracking by hiding the tooltip, the hover series state and the hover point
10889 *
10890 * @param allowMove {Boolean} Instead of destroying the tooltip altogether, allow moving it if possible
10891 */
10892 reset: function(allowMove, delay) {
10893 var pointer = this,
10894 chart = pointer.chart,
10895 hoverSeries = chart.hoverSeries,
10896 hoverPoint = chart.hoverPoint,
10897 hoverPoints = chart.hoverPoints,
10898 tooltip = chart.tooltip,
10899 tooltipPoints = tooltip && tooltip.shared ? hoverPoints : hoverPoint;
10900
10901 // Check if the points have moved outside the plot area (#1003, #4736, #5101)
10902 if (allowMove && tooltipPoints) {
10903 each(splat(tooltipPoints), function(point) {
10904 if (point.series.isCartesian && point.plotX === undefined) {
10905 allowMove = false;
10906 }
10907 });
10908 }
10909
10910 // Just move the tooltip, #349
10911 if (allowMove) {
10912 if (tooltip && tooltipPoints) {
10913 tooltip.refresh(tooltipPoints);
10914 if (hoverPoint) { // #2500
10915 hoverPoint.setState(hoverPoint.state, true);
10916 each(chart.axes, function(axis) {
10917 if (axis.crosshair) {
10918 axis.drawCrosshair(null, hoverPoint);
10919 }
10920 });
10921 }
10922 }
10923
10924 // Full reset
10925 } else {
10926
10927 if (hoverPoint) {
10928 hoverPoint.onMouseOut();
10929 }
10930
10931 if (hoverPoints) {
10932 each(hoverPoints, function(point) {
10933 point.setState();
10934 });
10935 }
10936
10937 if (hoverSeries) {
10938 hoverSeries.onMouseOut();
10939 }
10940
10941 if (tooltip) {
10942 tooltip.hide(delay);
10943 }
10944
10945 if (pointer._onDocumentMouseMove) {
10946 removeEvent(doc, 'mousemove', pointer._onDocumentMouseMove);
10947 pointer._onDocumentMouseMove = null;
10948 }
10949
10950 // Remove crosshairs
10951 each(chart.axes, function(axis) {
10952 axis.hideCrosshair();
10953 });
10954
10955 pointer.hoverX = pointer.prevKDPoint = chart.hoverPoints = chart.hoverPoint = null;
10956 }
10957 },
10958
10959 /**
10960 * Scale series groups to a certain scale and translation
10961 */
10962 scaleGroups: function(attribs, clip) {
10963
10964 var chart = this.chart,
10965 seriesAttribs;
10966
10967 // Scale each series
10968 each(chart.series, function(series) {
10969 seriesAttribs = attribs || series.getPlotBox(); // #1701
10970 if (series.xAxis && series.xAxis.zoomEnabled) {
10971 series.group.attr(seriesAttribs);
10972 if (series.markerGroup) {
10973 series.markerGroup.attr(seriesAttribs);
10974 series.markerGroup.clip(clip ? chart.clipRect : null);
10975 }
10976 if (series.dataLabelsGroup) {
10977 series.dataLabelsGroup.attr(seriesAttribs);
10978 }
10979 }
10980 });
10981
10982 // Clip
10983 chart.clipRect.attr(clip || chart.clipBox);
10984 },
10985
10986 /**
10987 * Start a drag operation
10988 */
10989 dragStart: function(e) {
10990 var chart = this.chart;
10991
10992 // Record the start position
10993 chart.mouseIsDown = e.type;
10994 chart.cancelClick = false;
10995 chart.mouseDownX = this.mouseDownX = e.chartX;
10996 chart.mouseDownY = this.mouseDownY = e.chartY;
10997 },
10998
10999 /**
11000 * Perform a drag operation in response to a mousemove event while the mouse is down
11001 */
11002 drag: function(e) {
11003
11004 var chart = this.chart,
11005 chartOptions = chart.options.chart,
11006 chartX = e.chartX,
11007 chartY = e.chartY,
11008 zoomHor = this.zoomHor,
11009 zoomVert = this.zoomVert,
11010 plotLeft = chart.plotLeft,
11011 plotTop = chart.plotTop,
11012 plotWidth = chart.plotWidth,
11013 plotHeight = chart.plotHeight,
11014 clickedInside,
11015 size,
11016 selectionMarker = this.selectionMarker,
11017 mouseDownX = this.mouseDownX,
11018 mouseDownY = this.mouseDownY,
11019 panKey = chartOptions.panKey && e[chartOptions.panKey + 'Key'];
11020
11021 // If the device supports both touch and mouse (like IE11), and we are touch-dragging
11022 // inside the plot area, don't handle the mouse event. #4339.
11023 if (selectionMarker && selectionMarker.touch) {
11024 return;
11025 }
11026
11027 // If the mouse is outside the plot area, adjust to cooordinates
11028 // inside to prevent the selection marker from going outside
11029 if (chartX < plotLeft) {
11030 chartX = plotLeft;
11031 } else if (chartX > plotLeft + plotWidth) {
11032 chartX = plotLeft + plotWidth;
11033 }
11034
11035 if (chartY < plotTop) {
11036 chartY = plotTop;
11037 } else if (chartY > plotTop + plotHeight) {
11038 chartY = plotTop + plotHeight;
11039 }
11040
11041 // determine if the mouse has moved more than 10px
11042 this.hasDragged = Math.sqrt(
11043 Math.pow(mouseDownX - chartX, 2) +
11044 Math.pow(mouseDownY - chartY, 2)
11045 );
11046
11047 if (this.hasDragged > 10) {
11048 clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop);
11049
11050 // make a selection
11051 if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside && !panKey) {
11052 if (!selectionMarker) {
11053 this.selectionMarker = selectionMarker = chart.renderer.rect(
11054 plotLeft,
11055 plotTop,
11056 zoomHor ? 1 : plotWidth,
11057 zoomVert ? 1 : plotHeight,
11058 0
11059 )
11060 .attr({
11061
11062 fill: chartOptions.selectionMarkerFill || color('#335cad').setOpacity(0.25).get(),
11063
11064 'class': 'highcharts-selection-marker',
11065 'zIndex': 7
11066 })
11067 .add();
11068 }
11069 }
11070
11071 // adjust the width of the selection marker
11072 if (selectionMarker && zoomHor) {
11073 size = chartX - mouseDownX;
11074 selectionMarker.attr({
11075 width: Math.abs(size),
11076 x: (size > 0 ? 0 : size) + mouseDownX
11077 });
11078 }
11079 // adjust the height of the selection marker
11080 if (selectionMarker && zoomVert) {
11081 size = chartY - mouseDownY;
11082 selectionMarker.attr({
11083 height: Math.abs(size),
11084 y: (size > 0 ? 0 : size) + mouseDownY
11085 });
11086 }
11087
11088 // panning
11089 if (clickedInside && !selectionMarker && chartOptions.panning) {
11090 chart.pan(e, chartOptions.panning);
11091 }
11092 }
11093 },
11094
11095 /**
11096 * On mouse up or touch end across the entire document, drop the selection.
11097 */
11098 drop: function(e) {
11099 var pointer = this,
11100 chart = this.chart,
11101 hasPinched = this.hasPinched;
11102
11103 if (this.selectionMarker) {
11104 var selectionData = {
11105 originalEvent: e, // #4890
11106 xAxis: [],
11107 yAxis: []
11108 },
11109 selectionBox = this.selectionMarker,
11110 selectionLeft = selectionBox.attr ? selectionBox.attr('x') : selectionBox.x,
11111 selectionTop = selectionBox.attr ? selectionBox.attr('y') : selectionBox.y,
11112 selectionWidth = selectionBox.attr ? selectionBox.attr('width') : selectionBox.width,
11113 selectionHeight = selectionBox.attr ? selectionBox.attr('height') : selectionBox.height,
11114 runZoom;
11115
11116 // a selection has been made
11117 if (this.hasDragged || hasPinched) {
11118
11119 // record each axis' min and max
11120 each(chart.axes, function(axis) {
11121 if (axis.zoomEnabled && defined(axis.min) && (hasPinched || pointer[{
11122 xAxis: 'zoomX',
11123 yAxis: 'zoomY'
11124 }[axis.coll]])) { // #859, #3569
11125 var horiz = axis.horiz,
11126 minPixelPadding = e.type === 'touchend' ? axis.minPixelPadding : 0, // #1207, #3075
11127 selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop) + minPixelPadding),
11128 selectionMax = axis.toValue((horiz ? selectionLeft + selectionWidth : selectionTop + selectionHeight) - minPixelPadding);
11129
11130 selectionData[axis.coll].push({
11131 axis: axis,
11132 min: Math.min(selectionMin, selectionMax), // for reversed axes
11133 max: Math.max(selectionMin, selectionMax)
11134 });
11135 runZoom = true;
11136 }
11137 });
11138 if (runZoom) {
11139 fireEvent(chart, 'selection', selectionData, function(args) {
11140 chart.zoom(extend(args, hasPinched ? {
11141 animation: false
11142 } : null));
11143 });
11144 }
11145
11146 }
11147 this.selectionMarker = this.selectionMarker.destroy();
11148
11149 // Reset scaling preview
11150 if (hasPinched) {
11151 this.scaleGroups();
11152 }
11153 }
11154
11155 // Reset all
11156 if (chart) { // it may be destroyed on mouse up - #877
11157 css(chart.container, {
11158 cursor: chart._cursor
11159 });
11160 chart.cancelClick = this.hasDragged > 10; // #370
11161 chart.mouseIsDown = this.hasDragged = this.hasPinched = false;
11162 this.pinchDown = [];
11163 }
11164 },
11165
11166 onContainerMouseDown: function(e) {
11167
11168 e = this.normalize(e);
11169
11170 this.zoomOption();
11171
11172 // issue #295, dragging not always working in Firefox
11173 if (e.preventDefault) {
11174 e.preventDefault();
11175 }
11176
11177 this.dragStart(e);
11178 },
11179
11180
11181
11182 onDocumentMouseUp: function(e) {
11183 if (charts[H.hoverChartIndex]) {
11184 charts[H.hoverChartIndex].pointer.drop(e);
11185 }
11186 },
11187
11188 /**
11189 * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea.
11190 * Issue #149 workaround. The mouseleave event does not always fire.
11191 */
11192 onDocumentMouseMove: function(e) {
11193 var chart = this.chart,
11194 chartPosition = this.chartPosition;
11195
11196 e = this.normalize(e, chartPosition);
11197
11198 // If we're outside, hide the tooltip
11199 if (chartPosition && !this.inClass(e.target, 'highcharts-tracker') &&
11200 !chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
11201 this.reset();
11202 }
11203 },
11204
11205 /**
11206 * When mouse leaves the container, hide the tooltip.
11207 */
11208 onContainerMouseLeave: function(e) {
11209 var chart = charts[H.hoverChartIndex];
11210 if (chart && (e.relatedTarget || e.toElement)) { // #4886, MS Touch end fires mouseleave but with no related target
11211 chart.pointer.reset();
11212 chart.pointer.chartPosition = null; // also reset the chart position, used in #149 fix
11213 }
11214 },
11215
11216 // The mousemove, touchmove and touchstart event handler
11217 onContainerMouseMove: function(e) {
11218
11219 var chart = this.chart;
11220
11221 if (!defined(H.hoverChartIndex) || !charts[H.hoverChartIndex] || !charts[H.hoverChartIndex].mouseIsDown) {
11222 H.hoverChartIndex = chart.index;
11223 }
11224
11225 e = this.normalize(e);
11226 e.returnValue = false; // #2251, #3224
11227
11228 if (chart.mouseIsDown === 'mousedown') {
11229 this.drag(e);
11230 }
11231
11232 // Show the tooltip and run mouse over events (#977)
11233 if ((this.inClass(e.target, 'highcharts-tracker') ||
11234 chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) && !chart.openMenu) {
11235 this.runPointActions(e);
11236 }
11237 },
11238
11239 /**
11240 * Utility to detect whether an element has, or has a parent with, a specific
11241 * class name. Used on detection of tracker objects and on deciding whether
11242 * hovering the tooltip should cause the active series to mouse out.
11243 */
11244 inClass: function(element, className) {
11245 var elemClassName;
11246 while (element) {
11247 elemClassName = attr(element, 'class');
11248 if (elemClassName) {
11249 if (elemClassName.indexOf(className) !== -1) {
11250 return true;
11251 }
11252 if (elemClassName.indexOf('highcharts-container') !== -1) {
11253 return false;
11254 }
11255 }
11256 element = element.parentNode;
11257 }
11258 },
11259
11260 onTrackerMouseOut: function(e) {
11261 var series = this.chart.hoverSeries,
11262 relatedTarget = e.relatedTarget || e.toElement;
11263
11264 if (series && relatedTarget && !series.options.stickyTracking &&
11265 !this.inClass(relatedTarget, 'highcharts-tooltip') &&
11266 !this.inClass(relatedTarget, 'highcharts-series-' + series.index)) { // #2499, #4465
11267 series.onMouseOut();
11268 }
11269 },
11270
11271 onContainerClick: function(e) {
11272 var chart = this.chart,
11273 hoverPoint = chart.hoverPoint,
11274 plotLeft = chart.plotLeft,
11275 plotTop = chart.plotTop;
11276
11277 e = this.normalize(e);
11278
11279 if (!chart.cancelClick) {
11280
11281 // On tracker click, fire the series and point events. #783, #1583
11282 if (hoverPoint && this.inClass(e.target, 'highcharts-tracker')) {
11283
11284 // the series click event
11285 fireEvent(hoverPoint.series, 'click', extend(e, {
11286 point: hoverPoint
11287 }));
11288
11289 // the point click event
11290 if (chart.hoverPoint) { // it may be destroyed (#1844)
11291 hoverPoint.firePointEvent('click', e);
11292 }
11293
11294 // When clicking outside a tracker, fire a chart event
11295 } else {
11296 extend(e, this.getCoordinates(e));
11297
11298 // fire a click event in the chart
11299 if (chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) {
11300 fireEvent(chart, 'click', e);
11301 }
11302 }
11303
11304
11305 }
11306 },
11307
11308 /**
11309 * Set the JS DOM events on the container and document. This method should contain
11310 * a one-to-one assignment between methods and their handlers. Any advanced logic should
11311 * be moved to the handler reflecting the event's name.
11312 */
11313 setDOMEvents: function() {
11314
11315 var pointer = this,
11316 container = pointer.chart.container;
11317
11318 container.onmousedown = function(e) {
11319 pointer.onContainerMouseDown(e);
11320 };
11321 container.onmousemove = function(e) {
11322 pointer.onContainerMouseMove(e);
11323 };
11324 container.onclick = function(e) {
11325 pointer.onContainerClick(e);
11326 };
11327 addEvent(container, 'mouseleave', pointer.onContainerMouseLeave);
11328 if (H.chartCount === 1) {
11329 addEvent(doc, 'mouseup', pointer.onDocumentMouseUp);
11330 }
11331 if (H.hasTouch) {
11332 container.ontouchstart = function(e) {
11333 pointer.onContainerTouchStart(e);
11334 };
11335 container.ontouchmove = function(e) {
11336 pointer.onContainerTouchMove(e);
11337 };
11338 if (H.chartCount === 1) {
11339 addEvent(doc, 'touchend', pointer.onDocumentTouchEnd);
11340 }
11341 }
11342
11343 },
11344
11345 /**
11346 * Destroys the Pointer object and disconnects DOM events.
11347 */
11348 destroy: function() {
11349 var prop;
11350
11351 removeEvent(this.chart.container, 'mouseleave', this.onContainerMouseLeave);
11352 if (!H.chartCount) {
11353 removeEvent(doc, 'mouseup', this.onDocumentMouseUp);
11354 removeEvent(doc, 'touchend', this.onDocumentTouchEnd);
11355 }
11356
11357 // memory and CPU leak
11358 clearInterval(this.tooltipTimeout);
11359
11360 for (prop in this) {
11361 this[prop] = null;
11362 }
11363 }
11364 };
11365
11366 }(Highcharts));
11367 (function(H) {
11368 /**
11369 * (c) 2010-2016 Torstein Honsi
11370 *
11371 * License: www.highcharts.com/license
11372 */
11373 'use strict';
11374 var charts = H.charts,
11375 each = H.each,
11376 extend = H.extend,
11377 map = H.map,
11378 noop = H.noop,
11379 pick = H.pick,
11380 Pointer = H.Pointer;
11381
11382 /* Support for touch devices */
11383 extend(Pointer.prototype, {
11384
11385 /**
11386 * Run translation operations
11387 */
11388 pinchTranslate: function(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) {
11389 if (this.zoomHor || this.pinchHor) {
11390 this.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
11391 }
11392 if (this.zoomVert || this.pinchVert) {
11393 this.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
11394 }
11395 },
11396
11397 /**
11398 * Run translation operations for each direction (horizontal and vertical) independently
11399 */
11400 pinchTranslateDirection: function(horiz, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch, forcedScale) {
11401 var chart = this.chart,
11402 xy = horiz ? 'x' : 'y',
11403 XY = horiz ? 'X' : 'Y',
11404 sChartXY = 'chart' + XY,
11405 wh = horiz ? 'width' : 'height',
11406 plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')],
11407 selectionWH,
11408 selectionXY,
11409 clipXY,
11410 scale = forcedScale || 1,
11411 inverted = chart.inverted,
11412 bounds = chart.bounds[horiz ? 'h' : 'v'],
11413 singleTouch = pinchDown.length === 1,
11414 touch0Start = pinchDown[0][sChartXY],
11415 touch0Now = touches[0][sChartXY],
11416 touch1Start = !singleTouch && pinchDown[1][sChartXY],
11417 touch1Now = !singleTouch && touches[1][sChartXY],
11418 outOfBounds,
11419 transformScale,
11420 scaleKey,
11421 setScale = function() {
11422 if (!singleTouch && Math.abs(touch0Start - touch1Start) > 20) { // Don't zoom if fingers are too close on this axis
11423 scale = forcedScale || Math.abs(touch0Now - touch1Now) / Math.abs(touch0Start - touch1Start);
11424 }
11425
11426 clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start;
11427 selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale;
11428 };
11429
11430 // Set the scale, first pass
11431 setScale();
11432
11433 selectionXY = clipXY; // the clip position (x or y) is altered if out of bounds, the selection position is not
11434
11435 // Out of bounds
11436 if (selectionXY < bounds.min) {
11437 selectionXY = bounds.min;
11438 outOfBounds = true;
11439 } else if (selectionXY + selectionWH > bounds.max) {
11440 selectionXY = bounds.max - selectionWH;
11441 outOfBounds = true;
11442 }
11443
11444 // Is the chart dragged off its bounds, determined by dataMin and dataMax?
11445 if (outOfBounds) {
11446
11447 // Modify the touchNow position in order to create an elastic drag movement. This indicates
11448 // to the user that the chart is responsive but can't be dragged further.
11449 touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]);
11450 if (!singleTouch) {
11451 touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]);
11452 }
11453
11454 // Set the scale, second pass to adapt to the modified touchNow positions
11455 setScale();
11456
11457 } else {
11458 lastValidTouch[xy] = [touch0Now, touch1Now];
11459 }
11460
11461 // Set geometry for clipping, selection and transformation
11462 if (!inverted) {
11463 clip[xy] = clipXY - plotLeftTop;
11464 clip[wh] = selectionWH;
11465 }
11466 scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY;
11467 transformScale = inverted ? 1 / scale : scale;
11468
11469 selectionMarker[wh] = selectionWH;
11470 selectionMarker[xy] = selectionXY;
11471 transform[scaleKey] = scale;
11472 transform['translate' + XY] = (transformScale * plotLeftTop) + (touch0Now - (transformScale * touch0Start));
11473 },
11474
11475 /**
11476 * Handle touch events with two touches
11477 */
11478 pinch: function(e) {
11479
11480 var self = this,
11481 chart = self.chart,
11482 pinchDown = self.pinchDown,
11483 touches = e.touches,
11484 touchesLength = touches.length,
11485 lastValidTouch = self.lastValidTouch,
11486 hasZoom = self.hasZoom,
11487 selectionMarker = self.selectionMarker,
11488 transform = {},
11489 fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, 'highcharts-tracker') &&
11490 chart.runTrackerClick) || self.runChartClick),
11491 clip = {};
11492
11493 // Don't initiate panning until the user has pinched. This prevents us from
11494 // blocking page scrolling as users scroll down a long page (#4210).
11495 if (touchesLength > 1) {
11496 self.initiated = true;
11497 }
11498
11499 // On touch devices, only proceed to trigger click if a handler is defined
11500 if (hasZoom && self.initiated && !fireClickEvent) {
11501 e.preventDefault();
11502 }
11503
11504 // Normalize each touch
11505 map(touches, function(e) {
11506 return self.normalize(e);
11507 });
11508
11509 // Register the touch start position
11510 if (e.type === 'touchstart') {
11511 each(touches, function(e, i) {
11512 pinchDown[i] = {
11513 chartX: e.chartX,
11514 chartY: e.chartY
11515 };
11516 });
11517 lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && pinchDown[1].chartX];
11518 lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && pinchDown[1].chartY];
11519
11520 // Identify the data bounds in pixels
11521 each(chart.axes, function(axis) {
11522 if (axis.zoomEnabled) {
11523 var bounds = chart.bounds[axis.horiz ? 'h' : 'v'],
11524 minPixelPadding = axis.minPixelPadding,
11525 min = axis.toPixels(pick(axis.options.min, axis.dataMin)),
11526 max = axis.toPixels(pick(axis.options.max, axis.dataMax)),
11527 absMin = Math.min(min, max),
11528 absMax = Math.max(min, max);
11529
11530 // Store the bounds for use in the touchmove handler
11531 bounds.min = Math.min(axis.pos, absMin - minPixelPadding);
11532 bounds.max = Math.max(axis.pos + axis.len, absMax + minPixelPadding);
11533 }
11534 });
11535 self.res = true; // reset on next move
11536
11537 // Event type is touchmove, handle panning and pinching
11538 } else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first
11539
11540
11541 // Set the marker
11542 if (!selectionMarker) {
11543 self.selectionMarker = selectionMarker = extend({
11544 destroy: noop,
11545 touch: true
11546 }, chart.plotBox);
11547 }
11548
11549 self.pinchTranslate(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
11550
11551 self.hasPinched = hasZoom;
11552
11553 // Scale and translate the groups to provide visual feedback during pinching
11554 self.scaleGroups(transform, clip);
11555
11556 // Optionally move the tooltip on touchmove
11557 if (!hasZoom && self.followTouchMove && touchesLength === 1) {
11558 this.runPointActions(self.normalize(e));
11559 } else if (self.res) {
11560 self.res = false;
11561 this.reset(false, 0);
11562 }
11563 }
11564 },
11565
11566 /**
11567 * General touch handler shared by touchstart and touchmove.
11568 */
11569 touch: function(e, start) {
11570 var chart = this.chart,
11571 hasMoved,
11572 pinchDown;
11573
11574 H.hoverChartIndex = chart.index;
11575
11576 if (e.touches.length === 1) {
11577
11578 e = this.normalize(e);
11579
11580 if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop) && !chart.openMenu) {
11581
11582 // Run mouse events and display tooltip etc
11583 if (start) {
11584 this.runPointActions(e);
11585 }
11586
11587 // Android fires touchmove events after the touchstart even if the
11588 // finger hasn't moved, or moved only a pixel or two. In iOS however,
11589 // the touchmove doesn't fire unless the finger moves more than ~4px.
11590 // So we emulate this behaviour in Android by checking how much it
11591 // moved, and cancelling on small distances. #3450.
11592 if (e.type === 'touchmove') {
11593 pinchDown = this.pinchDown;
11594 hasMoved = pinchDown[0] ? Math.sqrt( // #5266
11595 Math.pow(pinchDown[0].chartX - e.chartX, 2) +
11596 Math.pow(pinchDown[0].chartY - e.chartY, 2)
11597 ) >= 4 : false;
11598 }
11599
11600 if (pick(hasMoved, true)) {
11601 this.pinch(e);
11602 }
11603
11604 } else if (start) {
11605 // Hide the tooltip on touching outside the plot area (#1203)
11606 this.reset();
11607 }
11608
11609 } else if (e.touches.length === 2) {
11610 this.pinch(e);
11611 }
11612 },
11613
11614 onContainerTouchStart: function(e) {
11615 this.zoomOption();
11616 this.touch(e, true);
11617 },
11618
11619 onContainerTouchMove: function(e) {
11620 this.touch(e);
11621 },
11622
11623 onDocumentTouchEnd: function(e) {
11624 if (charts[H.hoverChartIndex]) {
11625 charts[H.hoverChartIndex].pointer.drop(e);
11626 }
11627 }
11628
11629 });
11630
11631 }(Highcharts));
11632 (function(H) {
11633 /**
11634 * (c) 2010-2016 Torstein Honsi
11635 *
11636 * License: www.highcharts.com/license
11637 */
11638 'use strict';
11639 var addEvent = H.addEvent,
11640 charts = H.charts,
11641 css = H.css,
11642 doc = H.doc,
11643 extend = H.extend,
11644 noop = H.noop,
11645 Pointer = H.Pointer,
11646 removeEvent = H.removeEvent,
11647 win = H.win,
11648 wrap = H.wrap;
11649
11650 if (win.PointerEvent || win.MSPointerEvent) {
11651
11652 // The touches object keeps track of the points being touched at all times
11653 var touches = {},
11654 hasPointerEvent = !!win.PointerEvent,
11655 getWebkitTouches = function() {
11656 var key,
11657 fake = [];
11658 fake.item = function(i) {
11659 return this[i];
11660 };
11661 for (key in touches) {
11662 if (touches.hasOwnProperty(key)) {
11663 fake.push({
11664 pageX: touches[key].pageX,
11665 pageY: touches[key].pageY,
11666 target: touches[key].target
11667 });
11668 }
11669 }
11670 return fake;
11671 },
11672 translateMSPointer = function(e, method, wktype, func) {
11673 var p;
11674 if ((e.pointerType === 'touch' || e.pointerType === e.MSPOINTER_TYPE_TOUCH) && charts[H.hoverChartIndex]) {
11675 func(e);
11676 p = charts[H.hoverChartIndex].pointer;
11677 p[method]({
11678 type: wktype,
11679 target: e.currentTarget,
11680 preventDefault: noop,
11681 touches: getWebkitTouches()
11682 });
11683 }
11684 };
11685
11686 /**
11687 * Extend the Pointer prototype with methods for each event handler and more
11688 */
11689 extend(Pointer.prototype, {
11690 onContainerPointerDown: function(e) {
11691 translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function(e) {
11692 touches[e.pointerId] = {
11693 pageX: e.pageX,
11694 pageY: e.pageY,
11695 target: e.currentTarget
11696 };
11697 });
11698 },
11699 onContainerPointerMove: function(e) {
11700 translateMSPointer(e, 'onContainerTouchMove', 'touchmove', function(e) {
11701 touches[e.pointerId] = {
11702 pageX: e.pageX,
11703 pageY: e.pageY
11704 };
11705 if (!touches[e.pointerId].target) {
11706 touches[e.pointerId].target = e.currentTarget;
11707 }
11708 });
11709 },
11710 onDocumentPointerUp: function(e) {
11711 translateMSPointer(e, 'onDocumentTouchEnd', 'touchend', function(e) {
11712 delete touches[e.pointerId];
11713 });
11714 },
11715
11716 /**
11717 * Add or remove the MS Pointer specific events
11718 */
11719 batchMSEvents: function(fn) {
11720 fn(this.chart.container, hasPointerEvent ? 'pointerdown' : 'MSPointerDown', this.onContainerPointerDown);
11721 fn(this.chart.container, hasPointerEvent ? 'pointermove' : 'MSPointerMove', this.onContainerPointerMove);
11722 fn(doc, hasPointerEvent ? 'pointerup' : 'MSPointerUp', this.onDocumentPointerUp);
11723 }
11724 });
11725
11726 // Disable default IE actions for pinch and such on chart element
11727 wrap(Pointer.prototype, 'init', function(proceed, chart, options) {
11728 proceed.call(this, chart, options);
11729 if (this.hasZoom) { // #4014
11730 css(chart.container, {
11731 '-ms-touch-action': 'none',
11732 'touch-action': 'none'
11733 });
11734 }
11735 });
11736
11737 // Add IE specific touch events to chart
11738 wrap(Pointer.prototype, 'setDOMEvents', function(proceed) {
11739 proceed.apply(this);
11740 if (this.hasZoom || this.followTouchMove) {
11741 this.batchMSEvents(addEvent);
11742 }
11743 });
11744 // Destroy MS events also
11745 wrap(Pointer.prototype, 'destroy', function(proceed) {
11746 this.batchMSEvents(removeEvent);
11747 proceed.call(this);
11748 });
11749 }
11750
11751 }(Highcharts));
11752 (function(H) {
11753 /**
11754 * (c) 2010-2016 Torstein Honsi
11755 *
11756 * License: www.highcharts.com/license
11757 */
11758 'use strict';
11759 var Legend,
11760
11761 addEvent = H.addEvent,
11762 css = H.css,
11763 discardElement = H.discardElement,
11764 defined = H.defined,
11765 each = H.each,
11766 extend = H.extend,
11767 isFirefox = H.isFirefox,
11768 marginNames = H.marginNames,
11769 merge = H.merge,
11770 pick = H.pick,
11771 setAnimation = H.setAnimation,
11772 stableSort = H.stableSort,
11773 win = H.win,
11774 wrap = H.wrap;
11775 /**
11776 * The overview of the chart's series
11777 */
11778 Legend = H.Legend = function(chart, options) {
11779 this.init(chart, options);
11780 };
11781
11782 Legend.prototype = {
11783
11784 /**
11785 * Initialize the legend
11786 */
11787 init: function(chart, options) {
11788
11789 this.chart = chart;
11790
11791 this.setOptions(options);
11792
11793 if (options.enabled) {
11794
11795 // Render it
11796 this.render();
11797
11798 // move checkboxes
11799 addEvent(this.chart, 'endResize', function() {
11800 this.legend.positionCheckboxes();
11801 });
11802 }
11803 },
11804
11805 setOptions: function(options) {
11806
11807 var padding = pick(options.padding, 8);
11808
11809 this.options = options;
11810
11811
11812 this.itemStyle = options.itemStyle;
11813 this.itemHiddenStyle = merge(this.itemStyle, options.itemHiddenStyle);
11814
11815 this.itemMarginTop = options.itemMarginTop || 0;
11816 this.padding = padding;
11817 this.initialItemX = padding;
11818 this.initialItemY = padding - 5; // 5 is the number of pixels above the text
11819 this.maxItemWidth = 0;
11820 this.itemHeight = 0;
11821 this.symbolWidth = pick(options.symbolWidth, 16);
11822 this.pages = [];
11823
11824 },
11825
11826 /**
11827 * Update the legend with new options. Equivalent to running chart.update with a legend
11828 * configuration option.
11829 * @param {Object} options Legend options
11830 * @param {Boolean} redraw Whether to redraw the chart, defaults to true.
11831 */
11832 update: function(options, redraw) {
11833 var chart = this.chart;
11834
11835 this.setOptions(merge(true, this.options, options));
11836 this.destroy();
11837 chart.isDirtyLegend = chart.isDirtyBox = true;
11838 if (pick(redraw, true)) {
11839 chart.redraw();
11840 }
11841 },
11842
11843 /**
11844 * Set the colors for the legend item
11845 * @param {Object} item A Series or Point instance
11846 * @param {Object} visible Dimmed or colored
11847 */
11848 colorizeItem: function(item, visible) {
11849 item.legendGroup[visible ? 'removeClass' : 'addClass']('highcharts-legend-item-hidden');
11850
11851
11852 var legend = this,
11853 options = legend.options,
11854 legendItem = item.legendItem,
11855 legendLine = item.legendLine,
11856 legendSymbol = item.legendSymbol,
11857 hiddenColor = legend.itemHiddenStyle.color,
11858 textColor = visible ? options.itemStyle.color : hiddenColor,
11859 symbolColor = visible ? (item.color || hiddenColor) : hiddenColor,
11860 markerOptions = item.options && item.options.marker,
11861 symbolAttr = {
11862 fill: symbolColor
11863 },
11864 key;
11865
11866 if (legendItem) {
11867 legendItem.css({
11868 fill: textColor,
11869 color: textColor
11870 }); // color for #1553, oldIE
11871 }
11872 if (legendLine) {
11873 legendLine.attr({
11874 stroke: symbolColor
11875 });
11876 }
11877
11878 if (legendSymbol) {
11879
11880 // Apply marker options
11881 if (markerOptions && legendSymbol.isMarker) { // #585
11882 //symbolAttr.stroke = symbolColor;
11883 symbolAttr = item.pointAttribs();
11884 if (!visible) {
11885 for (key in symbolAttr) {
11886 symbolAttr[key] = hiddenColor;
11887 }
11888 }
11889 }
11890
11891 legendSymbol.attr(symbolAttr);
11892 }
11893
11894 },
11895
11896 /**
11897 * Position the legend item
11898 * @param {Object} item A Series or Point instance
11899 */
11900 positionItem: function(item) {
11901 var legend = this,
11902 options = legend.options,
11903 symbolPadding = options.symbolPadding,
11904 ltr = !options.rtl,
11905 legendItemPos = item._legendItemPos,
11906 itemX = legendItemPos[0],
11907 itemY = legendItemPos[1],
11908 checkbox = item.checkbox,
11909 legendGroup = item.legendGroup;
11910
11911 if (legendGroup && legendGroup.element) {
11912 legendGroup.translate(
11913 ltr ? itemX : legend.legendWidth - itemX - 2 * symbolPadding - 4,
11914 itemY
11915 );
11916 }
11917
11918 if (checkbox) {
11919 checkbox.x = itemX;
11920 checkbox.y = itemY;
11921 }
11922 },
11923
11924 /**
11925 * Destroy a single legend item
11926 * @param {Object} item The series or point
11927 */
11928 destroyItem: function(item) {
11929 var checkbox = item.checkbox;
11930
11931 // destroy SVG elements
11932 each(['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], function(key) {
11933 if (item[key]) {
11934 item[key] = item[key].destroy();
11935 }
11936 });
11937
11938 if (checkbox) {
11939 discardElement(item.checkbox);
11940 }
11941 },
11942
11943 /**
11944 * Destroys the legend.
11945 */
11946 destroy: function() {
11947 var legend = this,
11948 legendGroup = legend.group,
11949 box = legend.box;
11950
11951 if (box) {
11952 legend.box = box.destroy();
11953 }
11954
11955 // Destroy items
11956 each(this.getAllItems(), function(item) {
11957 each(['legendItem', 'legendGroup'], function(key) {
11958 if (item[key]) {
11959 item[key] = item[key].destroy();
11960 }
11961 });
11962 });
11963
11964 if (legendGroup) {
11965 legend.group = legendGroup.destroy();
11966 }
11967 },
11968
11969 /**
11970 * Position the checkboxes after the width is determined
11971 */
11972 positionCheckboxes: function(scrollOffset) {
11973 var alignAttr = this.group.alignAttr,
11974 translateY,
11975 clipHeight = this.clipHeight || this.legendHeight,
11976 titleHeight = this.titleHeight;
11977
11978 if (alignAttr) {
11979 translateY = alignAttr.translateY;
11980 each(this.allItems, function(item) {
11981 var checkbox = item.checkbox,
11982 top;
11983
11984 if (checkbox) {
11985 top = translateY + titleHeight + checkbox.y + (scrollOffset || 0) + 3;
11986 css(checkbox, {
11987 left: (alignAttr.translateX + item.checkboxOffset + checkbox.x - 20) + 'px',
11988 top: top + 'px',
11989 display: top > translateY - 6 && top < translateY + clipHeight - 6 ? '' : 'none'
11990 });
11991 }
11992 });
11993 }
11994 },
11995
11996 /**
11997 * Render the legend title on top of the legend
11998 */
11999 renderTitle: function() {
12000 var options = this.options,
12001 padding = this.padding,
12002 titleOptions = options.title,
12003 titleHeight = 0,
12004 bBox;
12005
12006 if (titleOptions.text) {
12007 if (!this.title) {
12008 this.title = this.chart.renderer.label(titleOptions.text, padding - 3, padding - 4, null, null, null, null, null, 'legend-title')
12009 .attr({
12010 zIndex: 1
12011 })
12012
12013 .css(titleOptions.style)
12014
12015 .add(this.group);
12016 }
12017 bBox = this.title.getBBox();
12018 titleHeight = bBox.height;
12019 this.offsetWidth = bBox.width; // #1717
12020 this.contentGroup.attr({
12021 translateY: titleHeight
12022 });
12023 }
12024 this.titleHeight = titleHeight;
12025 },
12026
12027 /**
12028 * Set the legend item text
12029 */
12030 setText: function(item) {
12031 var options = this.options;
12032 item.legendItem.attr({
12033 text: options.labelFormat ? H.format(options.labelFormat, item) : options.labelFormatter.call(item)
12034 });
12035 },
12036
12037 /**
12038 * Render a single specific legend item
12039 * @param {Object} item A series or point
12040 */
12041 renderItem: function(item) {
12042 var legend = this,
12043 chart = legend.chart,
12044 renderer = chart.renderer,
12045 options = legend.options,
12046 horizontal = options.layout === 'horizontal',
12047 symbolWidth = legend.symbolWidth,
12048 symbolPadding = options.symbolPadding,
12049
12050 itemStyle = legend.itemStyle,
12051 itemHiddenStyle = legend.itemHiddenStyle,
12052
12053 padding = legend.padding,
12054 itemDistance = horizontal ? pick(options.itemDistance, 20) : 0,
12055 ltr = !options.rtl,
12056 itemHeight,
12057 widthOption = options.width,
12058 itemMarginBottom = options.itemMarginBottom || 0,
12059 itemMarginTop = legend.itemMarginTop,
12060 initialItemX = legend.initialItemX,
12061 bBox,
12062 itemWidth,
12063 li = item.legendItem,
12064 isSeries = !item.series,
12065 series = !isSeries && item.series.drawLegendSymbol ? item.series : item,
12066 seriesOptions = series.options,
12067 showCheckbox = legend.createCheckboxForItem && seriesOptions && seriesOptions.showCheckbox,
12068 useHTML = options.useHTML,
12069 fontSize = 12;
12070
12071 if (!li) { // generate it once, later move it
12072
12073 // Generate the group box
12074 // A group to hold the symbol and text. Text is to be appended in Legend class.
12075 item.legendGroup = renderer.g('legend-item')
12076 .addClass('highcharts-' + series.type + '-series highcharts-color-' + item.colorIndex + ' ' +
12077 (item.options.className || '') +
12078 (isSeries ? 'highcharts-series-' + item.index : '')
12079 )
12080 .attr({
12081 zIndex: 1
12082 })
12083 .add(legend.scrollGroup);
12084
12085 // Generate the list item text and add it to the group
12086 item.legendItem = li = renderer.text(
12087 '',
12088 ltr ? symbolWidth + symbolPadding : -symbolPadding,
12089 legend.baseline || 0,
12090 useHTML
12091 )
12092
12093 .css(merge(item.visible ? itemStyle : itemHiddenStyle)) // merge to prevent modifying original (#1021)
12094
12095 .attr({
12096 align: ltr ? 'left' : 'right',
12097 zIndex: 2
12098 })
12099 .add(item.legendGroup);
12100
12101 // Get the baseline for the first item - the font size is equal for all
12102 if (!legend.baseline) {
12103
12104 fontSize = itemStyle.fontSize;
12105
12106 legend.fontMetrics = renderer.fontMetrics(
12107 fontSize,
12108 li
12109 );
12110 legend.baseline = legend.fontMetrics.f + 3 + itemMarginTop;
12111 li.attr('y', legend.baseline);
12112 }
12113
12114 // Draw the legend symbol inside the group box
12115 series.drawLegendSymbol(legend, item);
12116
12117 if (legend.setItemEvents) {
12118 legend.setItemEvents(item, li, useHTML);
12119 }
12120
12121 // add the HTML checkbox on top
12122 if (showCheckbox) {
12123 legend.createCheckboxForItem(item);
12124 }
12125 }
12126
12127 // Colorize the items
12128 legend.colorizeItem(item, item.visible);
12129
12130 // Always update the text
12131 legend.setText(item);
12132
12133 // calculate the positions for the next line
12134 bBox = li.getBBox();
12135
12136 itemWidth = item.checkboxOffset =
12137 options.itemWidth ||
12138 item.legendItemWidth ||
12139 symbolWidth + symbolPadding + bBox.width + itemDistance + (showCheckbox ? 20 : 0);
12140 legend.itemHeight = itemHeight = Math.round(item.legendItemHeight || bBox.height);
12141
12142 // if the item exceeds the width, start a new line
12143 if (horizontal && legend.itemX - initialItemX + itemWidth >
12144 (widthOption || (chart.chartWidth - 2 * padding - initialItemX - options.x))) {
12145 legend.itemX = initialItemX;
12146 legend.itemY += itemMarginTop + legend.lastLineHeight + itemMarginBottom;
12147 legend.lastLineHeight = 0; // reset for next line (#915, #3976)
12148 }
12149
12150 // If the item exceeds the height, start a new column
12151 /*if (!horizontal && legend.itemY + options.y + itemHeight > chart.chartHeight - spacingTop - spacingBottom) {
12152 legend.itemY = legend.initialItemY;
12153 legend.itemX += legend.maxItemWidth;
12154 legend.maxItemWidth = 0;
12155 }*/
12156
12157 // Set the edge positions
12158 legend.maxItemWidth = Math.max(legend.maxItemWidth, itemWidth);
12159 legend.lastItemY = itemMarginTop + legend.itemY + itemMarginBottom;
12160 legend.lastLineHeight = Math.max(itemHeight, legend.lastLineHeight); // #915
12161
12162 // cache the position of the newly generated or reordered items
12163 item._legendItemPos = [legend.itemX, legend.itemY];
12164
12165 // advance
12166 if (horizontal) {
12167 legend.itemX += itemWidth;
12168
12169 } else {
12170 legend.itemY += itemMarginTop + itemHeight + itemMarginBottom;
12171 legend.lastLineHeight = itemHeight;
12172 }
12173
12174 // the width of the widest item
12175 legend.offsetWidth = widthOption || Math.max(
12176 (horizontal ? legend.itemX - initialItemX - itemDistance : itemWidth) + padding,
12177 legend.offsetWidth
12178 );
12179 },
12180
12181 /**
12182 * Get all items, which is one item per series for normal series and one item per point
12183 * for pie series.
12184 */
12185 getAllItems: function() {
12186 var allItems = [];
12187 each(this.chart.series, function(series) {
12188 var seriesOptions = series && series.options;
12189
12190 // Handle showInLegend. If the series is linked to another series, defaults to false.
12191 if (series && pick(seriesOptions.showInLegend, !defined(seriesOptions.linkedTo) ? undefined : false, true)) {
12192
12193 // Use points or series for the legend item depending on legendType
12194 allItems = allItems.concat(
12195 series.legendItems ||
12196 (seriesOptions.legendType === 'point' ?
12197 series.data :
12198 series)
12199 );
12200 }
12201 });
12202 return allItems;
12203 },
12204
12205 /**
12206 * Adjust the chart margins by reserving space for the legend on only one side
12207 * of the chart. If the position is set to a corner, top or bottom is reserved
12208 * for horizontal legends and left or right for vertical ones.
12209 */
12210 adjustMargins: function(margin, spacing) {
12211 var chart = this.chart,
12212 options = this.options,
12213 // Use the first letter of each alignment option in order to detect the side
12214 alignment = options.align.charAt(0) + options.verticalAlign.charAt(0) + options.layout.charAt(0); // #4189 - use charAt(x) notation instead of [x] for IE7
12215
12216 if (!options.floating) {
12217
12218 each([
12219 /(lth|ct|rth)/,
12220 /(rtv|rm|rbv)/,
12221 /(rbh|cb|lbh)/,
12222 /(lbv|lm|ltv)/
12223 ], function(alignments, side) {
12224 if (alignments.test(alignment) && !defined(margin[side])) {
12225 // Now we have detected on which side of the chart we should reserve space for the legend
12226 chart[marginNames[side]] = Math.max(
12227 chart[marginNames[side]],
12228 chart.legend[(side + 1) % 2 ? 'legendHeight' : 'legendWidth'] + [1, -1, -1, 1][side] * options[(side % 2) ? 'x' : 'y'] +
12229 pick(options.margin, 12) +
12230 spacing[side]
12231 );
12232 }
12233 });
12234 }
12235 },
12236
12237 /**
12238 * Render the legend. This method can be called both before and after
12239 * chart.render. If called after, it will only rearrange items instead
12240 * of creating new ones.
12241 */
12242 render: function() {
12243 var legend = this,
12244 chart = legend.chart,
12245 renderer = chart.renderer,
12246 legendGroup = legend.group,
12247 allItems,
12248 display,
12249 legendWidth,
12250 legendHeight,
12251 box = legend.box,
12252 options = legend.options,
12253 padding = legend.padding;
12254
12255 legend.itemX = legend.initialItemX;
12256 legend.itemY = legend.initialItemY;
12257 legend.offsetWidth = 0;
12258 legend.lastItemY = 0;
12259
12260 if (!legendGroup) {
12261 legend.group = legendGroup = renderer.g('legend')
12262 .attr({
12263 zIndex: 7
12264 })
12265 .add();
12266 legend.contentGroup = renderer.g()
12267 .attr({
12268 zIndex: 1
12269 }) // above background
12270 .add(legendGroup);
12271 legend.scrollGroup = renderer.g()
12272 .add(legend.contentGroup);
12273 }
12274
12275 legend.renderTitle();
12276
12277 // add each series or point
12278 allItems = legend.getAllItems();
12279
12280 // sort by legendIndex
12281 stableSort(allItems, function(a, b) {
12282 return ((a.options && a.options.legendIndex) || 0) - ((b.options && b.options.legendIndex) || 0);
12283 });
12284
12285 // reversed legend
12286 if (options.reversed) {
12287 allItems.reverse();
12288 }
12289
12290 legend.allItems = allItems;
12291 legend.display = display = !!allItems.length;
12292
12293 // render the items
12294 legend.lastLineHeight = 0;
12295 each(allItems, function(item) {
12296 legend.renderItem(item);
12297 });
12298
12299 // Get the box
12300 legendWidth = (options.width || legend.offsetWidth) + padding;
12301 legendHeight = legend.lastItemY + legend.lastLineHeight + legend.titleHeight;
12302 legendHeight = legend.handleOverflow(legendHeight);
12303 legendHeight += padding;
12304
12305 // Draw the border and/or background
12306 if (!box) {
12307 legend.box = box = renderer.rect()
12308 .addClass('highcharts-legend-box')
12309 .attr({
12310 r: options.borderRadius
12311 })
12312 .add(legendGroup);
12313 box.isNew = true;
12314 }
12315
12316
12317 // Presentational
12318 box.attr({
12319 stroke: options.borderColor,
12320 'stroke-width': options.borderWidth || 0,
12321 fill: options.backgroundColor || 'none'
12322 })
12323 .shadow(options.shadow);
12324
12325
12326 if (legendWidth > 0 && legendHeight > 0) {
12327 box[box.isNew ? 'attr' : 'animate'](
12328 box.crisp({
12329 x: 0,
12330 y: 0,
12331 width: legendWidth,
12332 height: legendHeight
12333 }, box.strokeWidth())
12334 );
12335 box.isNew = false;
12336 }
12337
12338 // hide the border if no items
12339 box[display ? 'show' : 'hide']();
12340
12341
12342
12343 legend.legendWidth = legendWidth;
12344 legend.legendHeight = legendHeight;
12345
12346 // Now that the legend width and height are established, put the items in the
12347 // final position
12348 each(allItems, function(item) {
12349 legend.positionItem(item);
12350 });
12351
12352 // 1.x compatibility: positioning based on style
12353 /*var props = ['left', 'right', 'top', 'bottom'],
12354 prop,
12355 i = 4;
12356 while (i--) {
12357 prop = props[i];
12358 if (options.style[prop] && options.style[prop] !== 'auto') {
12359 options[i < 2 ? 'align' : 'verticalAlign'] = prop;
12360 options[i < 2 ? 'x' : 'y'] = pInt(options.style[prop]) * (i % 2 ? -1 : 1);
12361 }
12362 }*/
12363
12364 if (display) {
12365 legendGroup.align(extend({
12366 width: legendWidth,
12367 height: legendHeight
12368 }, options), true, 'spacingBox');
12369 }
12370
12371 if (!chart.isResizing) {
12372 this.positionCheckboxes();
12373 }
12374 },
12375
12376 /**
12377 * Set up the overflow handling by adding navigation with up and down arrows below the
12378 * legend.
12379 */
12380 handleOverflow: function(legendHeight) {
12381 var legend = this,
12382 chart = this.chart,
12383 renderer = chart.renderer,
12384 options = this.options,
12385 optionsY = options.y,
12386 alignTop = options.verticalAlign === 'top',
12387 spaceHeight = chart.spacingBox.height + (alignTop ? -optionsY : optionsY) - this.padding,
12388 maxHeight = options.maxHeight,
12389 clipHeight,
12390 clipRect = this.clipRect,
12391 navOptions = options.navigation,
12392 animation = pick(navOptions.animation, true),
12393 arrowSize = navOptions.arrowSize || 12,
12394 nav = this.nav,
12395 pages = this.pages,
12396 padding = this.padding,
12397 lastY,
12398 allItems = this.allItems,
12399 clipToHeight = function(height) {
12400 clipRect.attr({
12401 height: height
12402 });
12403
12404 // useHTML
12405 if (legend.contentGroup.div) {
12406 legend.contentGroup.div.style.clip = 'rect(' + padding + 'px,9999px,' + (padding + height) + 'px,0)';
12407 }
12408 };
12409
12410
12411 // Adjust the height
12412 if (options.layout === 'horizontal') {
12413 spaceHeight /= 2;
12414 }
12415 if (maxHeight) {
12416 spaceHeight = Math.min(spaceHeight, maxHeight);
12417 }
12418
12419 // Reset the legend height and adjust the clipping rectangle
12420 pages.length = 0;
12421 if (legendHeight > spaceHeight && navOptions.enabled !== false) {
12422
12423 this.clipHeight = clipHeight = Math.max(spaceHeight - 20 - this.titleHeight - padding, 0);
12424 this.currentPage = pick(this.currentPage, 1);
12425 this.fullHeight = legendHeight;
12426
12427 // Fill pages with Y positions so that the top of each a legend item defines
12428 // the scroll top for each page (#2098)
12429 each(allItems, function(item, i) {
12430 var y = item._legendItemPos[1],
12431 h = Math.round(item.legendItem.getBBox().height),
12432 len = pages.length;
12433
12434 if (!len || (y - pages[len - 1] > clipHeight && (lastY || y) !== pages[len - 1])) {
12435 pages.push(lastY || y);
12436 len++;
12437 }
12438
12439 if (i === allItems.length - 1 && y + h - pages[len - 1] > clipHeight) {
12440 pages.push(y);
12441 }
12442 if (y !== lastY) {
12443 lastY = y;
12444 }
12445 });
12446
12447 // Only apply clipping if needed. Clipping causes blurred legend in PDF export (#1787)
12448 if (!clipRect) {
12449 clipRect = legend.clipRect = renderer.clipRect(0, padding, 9999, 0);
12450 legend.contentGroup.clip(clipRect);
12451 }
12452
12453 clipToHeight(clipHeight);
12454
12455 // Add navigation elements
12456 if (!nav) {
12457 this.nav = nav = renderer.g().attr({
12458 zIndex: 1
12459 }).add(this.group);
12460 this.up = renderer.symbol('triangle', 0, 0, arrowSize, arrowSize)
12461 .on('click', function() {
12462 legend.scroll(-1, animation);
12463 })
12464 .add(nav);
12465 this.pager = renderer.text('', 15, 10)
12466 .addClass('highcharts-legend-navigation')
12467
12468 .css(navOptions.style)
12469
12470 .add(nav);
12471 this.down = renderer.symbol('triangle-down', 0, 0, arrowSize, arrowSize)
12472 .on('click', function() {
12473 legend.scroll(1, animation);
12474 })
12475 .add(nav);
12476 }
12477
12478 // Set initial position
12479 legend.scroll(0);
12480
12481 legendHeight = spaceHeight;
12482
12483 } else if (nav) {
12484 clipToHeight(chart.chartHeight);
12485 nav.hide();
12486 this.scrollGroup.attr({
12487 translateY: 1
12488 });
12489 this.clipHeight = 0; // #1379
12490 }
12491
12492 return legendHeight;
12493 },
12494
12495 /**
12496 * Scroll the legend by a number of pages
12497 * @param {Object} scrollBy
12498 * @param {Object} animation
12499 */
12500 scroll: function(scrollBy, animation) {
12501 var pages = this.pages,
12502 pageCount = pages.length,
12503 currentPage = this.currentPage + scrollBy,
12504 clipHeight = this.clipHeight,
12505 navOptions = this.options.navigation,
12506 pager = this.pager,
12507 padding = this.padding,
12508 scrollOffset;
12509
12510 // When resizing while looking at the last page
12511 if (currentPage > pageCount) {
12512 currentPage = pageCount;
12513 }
12514
12515 if (currentPage > 0) {
12516
12517 if (animation !== undefined) {
12518 setAnimation(animation, this.chart);
12519 }
12520
12521 this.nav.attr({
12522 translateX: padding,
12523 translateY: clipHeight + this.padding + 7 + this.titleHeight,
12524 visibility: 'visible'
12525 });
12526 this.up.attr({
12527 'class': currentPage === 1 ? 'highcharts-legend-nav-inactive' : 'highcharts-legend-nav-active'
12528 });
12529 pager.attr({
12530 text: currentPage + '/' + pageCount
12531 });
12532 this.down.attr({
12533 'x': 18 + this.pager.getBBox().width, // adjust to text width
12534 'class': currentPage === pageCount ? 'highcharts-legend-nav-inactive' : 'highcharts-legend-nav-active'
12535 });
12536
12537
12538 this.up
12539 .attr({
12540 fill: currentPage === 1 ? navOptions.inactiveColor : navOptions.activeColor
12541 })
12542 .css({
12543 cursor: currentPage === 1 ? 'default' : 'pointer'
12544 });
12545 this.down
12546 .attr({
12547 fill: currentPage === pageCount ? navOptions.inactiveColor : navOptions.activeColor
12548 })
12549 .css({
12550 cursor: currentPage === pageCount ? 'default' : 'pointer'
12551 });
12552
12553
12554 scrollOffset = -pages[currentPage - 1] + this.initialItemY;
12555
12556 this.scrollGroup.animate({
12557 translateY: scrollOffset
12558 });
12559
12560 this.currentPage = currentPage;
12561 this.positionCheckboxes(scrollOffset);
12562 }
12563
12564 }
12565
12566 };
12567
12568 /*
12569 * LegendSymbolMixin
12570 */
12571
12572 H.LegendSymbolMixin = {
12573
12574 /**
12575 * Get the series' symbol in the legend
12576 *
12577 * @param {Object} legend The legend object
12578 * @param {Object} item The series (this) or point
12579 */
12580 drawRectangle: function(legend, item) {
12581 var options = legend.options,
12582 symbolHeight = options.symbolHeight || legend.fontMetrics.f,
12583 square = options.squareSymbol,
12584 symbolWidth = square ? symbolHeight : legend.symbolWidth; // docs: square
12585
12586 item.legendSymbol = this.chart.renderer.rect(
12587 square ? (legend.symbolWidth - symbolHeight) / 2 : 0,
12588 legend.baseline - symbolHeight + 1, // #3988
12589 symbolWidth,
12590 symbolHeight,
12591 pick(legend.options.symbolRadius, symbolHeight / 2) // docs: new default
12592 )
12593 .addClass('highcharts-point')
12594 .attr({
12595 zIndex: 3
12596 }).add(item.legendGroup);
12597
12598 },
12599
12600 /**
12601 * Get the series' symbol in the legend. This method should be overridable to create custom
12602 * symbols through Highcharts.seriesTypes[type].prototype.drawLegendSymbols.
12603 *
12604 * @param {Object} legend The legend object
12605 */
12606 drawLineMarker: function(legend) {
12607
12608 var options = this.options,
12609 markerOptions = options.marker,
12610 radius,
12611 legendSymbol,
12612 symbolWidth = legend.symbolWidth,
12613 renderer = this.chart.renderer,
12614 legendItemGroup = this.legendGroup,
12615 verticalCenter = legend.baseline - Math.round(legend.fontMetrics.b * 0.3),
12616 attr = {};
12617
12618 // Draw the line
12619
12620 attr = {
12621 'stroke-width': options.lineWidth || 0
12622 };
12623 if (options.dashStyle) {
12624 attr.dashstyle = options.dashStyle;
12625 }
12626
12627
12628 this.legendLine = renderer.path([
12629 'M',
12630 0,
12631 verticalCenter,
12632 'L',
12633 symbolWidth,
12634 verticalCenter
12635 ])
12636 .addClass('highcharts-graph')
12637 .attr(attr)
12638 .add(legendItemGroup);
12639
12640 // Draw the marker
12641 if (markerOptions && markerOptions.enabled !== false) {
12642 radius = markerOptions.radius;
12643 this.legendSymbol = legendSymbol = renderer.symbol(
12644 this.symbol,
12645 (symbolWidth / 2) - radius,
12646 verticalCenter - radius,
12647 2 * radius,
12648 2 * radius,
12649 markerOptions
12650 )
12651 .addClass('highcharts-point')
12652 .add(legendItemGroup);
12653 legendSymbol.isMarker = true;
12654 }
12655 }
12656 };
12657
12658 // Workaround for #2030, horizontal legend items not displaying in IE11 Preview,
12659 // and for #2580, a similar drawing flaw in Firefox 26.
12660 // Explore if there's a general cause for this. The problem may be related
12661 // to nested group elements, as the legend item texts are within 4 group elements.
12662 if (/Trident\/7\.0/.test(win.navigator.userAgent) || isFirefox) {
12663 wrap(Legend.prototype, 'positionItem', function(proceed, item) {
12664 var legend = this,
12665 runPositionItem = function() { // If chart destroyed in sync, this is undefined (#2030)
12666 if (item._legendItemPos) {
12667 proceed.call(legend, item);
12668 }
12669 };
12670
12671 // Do it now, for export and to get checkbox placement
12672 runPositionItem();
12673
12674 // Do it after to work around the core issue
12675 setTimeout(runPositionItem);
12676 });
12677 }
12678
12679 }(Highcharts));
12680 (function(H) {
12681 /**
12682 * (c) 2010-2016 Torstein Honsi
12683 *
12684 * License: www.highcharts.com/license
12685 */
12686 'use strict';
12687 var addEvent = H.addEvent,
12688 animate = H.animate,
12689 animObject = H.animObject,
12690 attr = H.attr,
12691 doc = H.doc,
12692 Axis = H.Axis, // @todo add as requirement
12693 createElement = H.createElement,
12694 defaultOptions = H.defaultOptions,
12695 discardElement = H.discardElement,
12696 charts = H.charts,
12697 css = H.css,
12698 defined = H.defined,
12699 each = H.each,
12700 error = H.error,
12701 extend = H.extend,
12702 fireEvent = H.fireEvent,
12703 getStyle = H.getStyle,
12704 grep = H.grep,
12705 isNumber = H.isNumber,
12706 isObject = H.isObject,
12707 isString = H.isString,
12708 Legend = H.Legend, // @todo add as requirement
12709 marginNames = H.marginNames,
12710 merge = H.merge,
12711 Pointer = H.Pointer, // @todo add as requirement
12712 pick = H.pick,
12713 pInt = H.pInt,
12714 removeEvent = H.removeEvent,
12715 seriesTypes = H.seriesTypes,
12716 splat = H.splat,
12717 svg = H.svg,
12718 syncTimeout = H.syncTimeout,
12719 win = H.win,
12720 Renderer = H.Renderer;
12721 /**
12722 * The Chart class
12723 * @param {String|Object} renderTo The DOM element to render to, or its id
12724 * @param {Object} options
12725 * @param {Function} callback Function to run when the chart has loaded
12726 */
12727 var Chart = H.Chart = function() {
12728 this.getArgs.apply(this, arguments);
12729 };
12730
12731 H.chart = function(a, b, c) {
12732 return new Chart(a, b, c);
12733 };
12734
12735 Chart.prototype = {
12736
12737 /**
12738 * Hook for modules
12739 */
12740 callbacks: [],
12741
12742 /**
12743 * Handle the arguments passed to the constructor
12744 * @returns {Array} Arguments without renderTo
12745 */
12746 getArgs: function() {
12747 var args = [].slice.call(arguments);
12748
12749 // Remove the optional first argument, renderTo, and
12750 // set it on this.
12751 if (isString(args[0]) || args[0].nodeName) {
12752 this.renderTo = args.shift();
12753 }
12754 this.init(args[0], args[1]);
12755 },
12756
12757 /**
12758 * Initialize the chart
12759 */
12760 init: function(userOptions, callback) {
12761
12762 // Handle regular options
12763 var options,
12764 seriesOptions = userOptions.series; // skip merging data points to increase performance
12765
12766 userOptions.series = null;
12767 options = merge(defaultOptions, userOptions); // do the merge
12768 options.series = userOptions.series = seriesOptions; // set back the series data
12769 this.userOptions = userOptions;
12770 this.respRules = [];
12771
12772 var optionsChart = options.chart;
12773
12774 var chartEvents = optionsChart.events;
12775
12776 this.margin = [];
12777 this.spacing = [];
12778
12779 //this.runChartClick = chartEvents && !!chartEvents.click;
12780 this.bounds = {
12781 h: {},
12782 v: {}
12783 }; // Pixel data bounds for touch zoom
12784
12785 this.callback = callback;
12786 this.isResizing = 0;
12787 this.options = options;
12788 //chartTitleOptions = undefined;
12789 //chartSubtitleOptions = undefined;
12790
12791 this.axes = [];
12792 this.series = [];
12793 this.hasCartesianSeries = optionsChart.showAxes;
12794 //this.axisOffset = undefined;
12795 //this.inverted = undefined;
12796 //this.loadingShown = undefined;
12797 //this.container = undefined;
12798 //this.chartWidth = undefined;
12799 //this.chartHeight = undefined;
12800 //this.marginRight = undefined;
12801 //this.marginBottom = undefined;
12802 //this.containerWidth = undefined;
12803 //this.containerHeight = undefined;
12804 //this.oldChartWidth = undefined;
12805 //this.oldChartHeight = undefined;
12806
12807 //this.renderTo = undefined;
12808 //this.renderToClone = undefined;
12809
12810 //this.spacingBox = undefined
12811
12812 //this.legend = undefined;
12813
12814 // Elements
12815 //this.chartBackground = undefined;
12816 //this.plotBackground = undefined;
12817 //this.plotBGImage = undefined;
12818 //this.plotBorder = undefined;
12819 //this.loadingDiv = undefined;
12820 //this.loadingSpan = undefined;
12821
12822 var chart = this,
12823 eventType;
12824
12825 // Add the chart to the global lookup
12826 chart.index = charts.length;
12827 charts.push(chart);
12828 H.chartCount++;
12829
12830 // Chart event handlers
12831 if (chartEvents) {
12832 for (eventType in chartEvents) {
12833 addEvent(chart, eventType, chartEvents[eventType]);
12834 }
12835 }
12836
12837 chart.xAxis = [];
12838 chart.yAxis = [];
12839
12840 chart.pointCount = chart.colorCounter = chart.symbolCounter = 0;
12841
12842 chart.firstRender();
12843 },
12844
12845 /**
12846 * Initialize an individual series, called internally before render time
12847 */
12848 initSeries: function(options) {
12849 var chart = this,
12850 optionsChart = chart.options.chart,
12851 type = options.type || optionsChart.type || optionsChart.defaultSeriesType,
12852 series,
12853 Constr = seriesTypes[type];
12854
12855 // No such series type
12856 if (!Constr) {
12857 error(17, true);
12858 }
12859
12860 series = new Constr();
12861 series.init(this, options);
12862 return series;
12863 },
12864
12865 /**
12866 * Check whether a given point is within the plot area
12867 *
12868 * @param {Number} plotX Pixel x relative to the plot area
12869 * @param {Number} plotY Pixel y relative to the plot area
12870 * @param {Boolean} inverted Whether the chart is inverted
12871 */
12872 isInsidePlot: function(plotX, plotY, inverted) {
12873 var x = inverted ? plotY : plotX,
12874 y = inverted ? plotX : plotY;
12875
12876 return x >= 0 &&
12877 x <= this.plotWidth &&
12878 y >= 0 &&
12879 y <= this.plotHeight;
12880 },
12881
12882 /**
12883 * Redraw legend, axes or series based on updated data
12884 *
12885 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
12886 * configuration
12887 */
12888 redraw: function(animation) {
12889 var chart = this,
12890 axes = chart.axes,
12891 series = chart.series,
12892 pointer = chart.pointer,
12893 legend = chart.legend,
12894 redrawLegend = chart.isDirtyLegend,
12895 hasStackedSeries,
12896 hasDirtyStacks,
12897 hasCartesianSeries = chart.hasCartesianSeries,
12898 isDirtyBox = chart.isDirtyBox,
12899 seriesLength = series.length,
12900 i = seriesLength,
12901 serie,
12902 renderer = chart.renderer,
12903 isHiddenChart = renderer.isHidden(),
12904 afterRedraw = [];
12905
12906 H.setAnimation(animation, chart);
12907
12908 if (isHiddenChart) {
12909 chart.cloneRenderTo();
12910 }
12911
12912 // Adjust title layout (reflow multiline text)
12913 chart.layOutTitles();
12914
12915 // link stacked series
12916 while (i--) {
12917 serie = series[i];
12918
12919 if (serie.options.stacking) {
12920 hasStackedSeries = true;
12921
12922 if (serie.isDirty) {
12923 hasDirtyStacks = true;
12924 break;
12925 }
12926 }
12927 }
12928 if (hasDirtyStacks) { // mark others as dirty
12929 i = seriesLength;
12930 while (i--) {
12931 serie = series[i];
12932 if (serie.options.stacking) {
12933 serie.isDirty = true;
12934 }
12935 }
12936 }
12937
12938 // Handle updated data in the series
12939 each(series, function(serie) {
12940 if (serie.isDirty) {
12941 if (serie.options.legendType === 'point') {
12942 if (serie.updateTotals) {
12943 serie.updateTotals();
12944 }
12945 redrawLegend = true;
12946 }
12947 }
12948 if (serie.isDirtyData) {
12949 fireEvent(serie, 'updatedData');
12950 }
12951 });
12952
12953 // handle added or removed series
12954 if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed
12955 // draw legend graphics
12956 legend.render();
12957
12958 chart.isDirtyLegend = false;
12959 }
12960
12961 // reset stacks
12962 if (hasStackedSeries) {
12963 chart.getStacks();
12964 }
12965
12966
12967 if (hasCartesianSeries) {
12968 // set axes scales
12969 each(axes, function(axis) {
12970 axis.updateNames();
12971 axis.setScale();
12972 });
12973 }
12974
12975 chart.getMargins(); // #3098
12976
12977 if (hasCartesianSeries) {
12978 // If one axis is dirty, all axes must be redrawn (#792, #2169)
12979 each(axes, function(axis) {
12980 if (axis.isDirty) {
12981 isDirtyBox = true;
12982 }
12983 });
12984
12985 // redraw axes
12986 each(axes, function(axis) {
12987
12988 // Fire 'afterSetExtremes' only if extremes are set
12989 var key = axis.min + ',' + axis.max;
12990 if (axis.extKey !== key) { // #821, #4452
12991 axis.extKey = key;
12992 afterRedraw.push(function() { // prevent a recursive call to chart.redraw() (#1119)
12993 fireEvent(axis, 'afterSetExtremes', extend(axis.eventArgs, axis.getExtremes())); // #747, #751
12994 delete axis.eventArgs;
12995 });
12996 }
12997 if (isDirtyBox || hasStackedSeries) {
12998 axis.redraw();
12999 }
13000 });
13001 }
13002
13003 // the plot areas size has changed
13004 if (isDirtyBox) {
13005 chart.drawChartBox();
13006 }
13007
13008
13009 // redraw affected series
13010 each(series, function(serie) {
13011 if (serie.isDirty && serie.visible &&
13012 (!serie.isCartesian || serie.xAxis)) { // issue #153
13013 serie.redraw();
13014 }
13015 });
13016
13017 // move tooltip or reset
13018 if (pointer) {
13019 pointer.reset(true);
13020 }
13021
13022 // redraw if canvas
13023 renderer.draw();
13024
13025 // fire the event
13026 fireEvent(chart, 'redraw');
13027
13028 if (isHiddenChart) {
13029 chart.cloneRenderTo(true);
13030 }
13031
13032 // Fire callbacks that are put on hold until after the redraw
13033 each(afterRedraw, function(callback) {
13034 callback.call();
13035 });
13036 },
13037
13038 /**
13039 * Get an axis, series or point object by id.
13040 * @param id {String} The id as given in the configuration options
13041 */
13042 get: function(id) {
13043 var chart = this,
13044 axes = chart.axes,
13045 series = chart.series;
13046
13047 var i,
13048 j,
13049 points;
13050
13051 // search axes
13052 for (i = 0; i < axes.length; i++) {
13053 if (axes[i].options.id === id) {
13054 return axes[i];
13055 }
13056 }
13057
13058 // search series
13059 for (i = 0; i < series.length; i++) {
13060 if (series[i].options.id === id) {
13061 return series[i];
13062 }
13063 }
13064
13065 // search points
13066 for (i = 0; i < series.length; i++) {
13067 points = series[i].points || [];
13068 for (j = 0; j < points.length; j++) {
13069 if (points[j].id === id) {
13070 return points[j];
13071 }
13072 }
13073 }
13074 return null;
13075 },
13076
13077 /**
13078 * Create the Axis instances based on the config options
13079 */
13080 getAxes: function() {
13081 var chart = this,
13082 options = this.options,
13083 xAxisOptions = options.xAxis = splat(options.xAxis || {}),
13084 yAxisOptions = options.yAxis = splat(options.yAxis || {}),
13085 optionsArray;
13086
13087 // make sure the options are arrays and add some members
13088 each(xAxisOptions, function(axis, i) {
13089 axis.index = i;
13090 axis.isX = true;
13091 });
13092
13093 each(yAxisOptions, function(axis, i) {
13094 axis.index = i;
13095 });
13096
13097 // concatenate all axis options into one array
13098 optionsArray = xAxisOptions.concat(yAxisOptions);
13099
13100 each(optionsArray, function(axisOptions) {
13101 new Axis(chart, axisOptions); // eslint-disable-line no-new
13102 });
13103 },
13104
13105
13106 /**
13107 * Get the currently selected points from all series
13108 */
13109 getSelectedPoints: function() {
13110 var points = [];
13111 each(this.series, function(serie) {
13112 points = points.concat(grep(serie.points || [], function(point) {
13113 return point.selected;
13114 }));
13115 });
13116 return points;
13117 },
13118
13119 /**
13120 * Get the currently selected series
13121 */
13122 getSelectedSeries: function() {
13123 return grep(this.series, function(serie) {
13124 return serie.selected;
13125 });
13126 },
13127
13128 /**
13129 * Show the title and subtitle of the chart
13130 *
13131 * @param titleOptions {Object} New title options
13132 * @param subtitleOptions {Object} New subtitle options
13133 *
13134 */
13135 setTitle: function(titleOptions, subtitleOptions, redraw) {
13136 var chart = this,
13137 options = chart.options,
13138 chartTitleOptions,
13139 chartSubtitleOptions;
13140
13141 chartTitleOptions = options.title = merge(options.title, titleOptions);
13142 chartSubtitleOptions = options.subtitle = merge(options.subtitle, subtitleOptions);
13143
13144 // add title and subtitle
13145 each([
13146 ['title', titleOptions, chartTitleOptions],
13147 ['subtitle', subtitleOptions, chartSubtitleOptions]
13148 ], function(arr, i) {
13149 var name = arr[0],
13150 title = chart[name],
13151 titleOptions = arr[1],
13152 chartTitleOptions = arr[2];
13153
13154 if (title && titleOptions) {
13155 chart[name] = title = title.destroy(); // remove old
13156 }
13157
13158 if (chartTitleOptions && chartTitleOptions.text && !title) {
13159 chart[name] = chart.renderer.text(
13160 chartTitleOptions.text,
13161 0,
13162 0,
13163 chartTitleOptions.useHTML
13164 )
13165 .attr({
13166 align: chartTitleOptions.align,
13167 'class': 'highcharts-' + name,
13168 zIndex: chartTitleOptions.zIndex || 4
13169 })
13170 .add();
13171
13172 // Update methods, shortcut to Chart.setTitle
13173 chart[name].update = function(o) {
13174 chart.setTitle(!i && o, i && o);
13175 };
13176
13177
13178 // Presentational
13179 chart[name].css(chartTitleOptions.style);
13180
13181
13182 }
13183 });
13184 chart.layOutTitles(redraw);
13185 },
13186
13187 /**
13188 * Lay out the chart titles and cache the full offset height for use in getMargins
13189 */
13190 layOutTitles: function(redraw) {
13191 var titleOffset = 0,
13192 requiresDirtyBox,
13193 renderer = this.renderer,
13194 spacingBox = this.spacingBox;
13195
13196 // Lay out the title and the subtitle respectively
13197 each(['title', 'subtitle'], function(key) {
13198 var title = this[key],
13199 titleOptions = this.options[key],
13200 titleSize;
13201
13202 if (title) {
13203
13204 titleSize = titleOptions.style.fontSize;
13205
13206 titleSize = renderer.fontMetrics(titleSize, title).b;
13207
13208 title
13209 .css({
13210 width: (titleOptions.width || spacingBox.width + titleOptions.widthAdjust) + 'px'
13211 })
13212 .align(extend({
13213 y: titleOffset + titleSize + (key === 'title' ? -3 : 2)
13214 }, titleOptions), false, 'spacingBox');
13215
13216 if (!titleOptions.floating && !titleOptions.verticalAlign) {
13217 titleOffset = Math.ceil(titleOffset + title.getBBox().height);
13218 }
13219 }
13220 }, this);
13221
13222 requiresDirtyBox = this.titleOffset !== titleOffset;
13223 this.titleOffset = titleOffset; // used in getMargins
13224
13225 if (!this.isDirtyBox && requiresDirtyBox) {
13226 this.isDirtyBox = requiresDirtyBox;
13227 // Redraw if necessary (#2719, #2744)
13228 if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) {
13229 this.redraw();
13230 }
13231 }
13232 },
13233
13234 /**
13235 * Get chart width and height according to options and container size
13236 */
13237 getChartSize: function() {
13238 var chart = this,
13239 optionsChart = chart.options.chart,
13240 widthOption = optionsChart.width,
13241 heightOption = optionsChart.height,
13242 renderTo = chart.renderToClone || chart.renderTo;
13243
13244 // Get inner width and height
13245 if (!defined(widthOption)) {
13246 chart.containerWidth = getStyle(renderTo, 'width');
13247 }
13248 if (!defined(heightOption)) {
13249 chart.containerHeight = getStyle(renderTo, 'height');
13250 }
13251
13252 chart.chartWidth = Math.max(0, widthOption || chart.containerWidth || 600); // #1393, 1460
13253 chart.chartHeight = Math.max(0, pick(heightOption,
13254 // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7:
13255 chart.containerHeight > 19 ? chart.containerHeight : 400));
13256 },
13257
13258 /**
13259 * Create a clone of the chart's renderTo div and place it outside the viewport to allow
13260 * size computation on chart.render and chart.redraw
13261 */
13262 cloneRenderTo: function(revert) {
13263 var clone = this.renderToClone,
13264 container = this.container;
13265
13266 // Destroy the clone and bring the container back to the real renderTo div
13267 if (revert) {
13268 if (clone) {
13269 while (clone.childNodes.length) { // #5231
13270 this.renderTo.appendChild(clone.firstChild);
13271 }
13272 discardElement(clone);
13273 delete this.renderToClone;
13274 }
13275
13276 // Set up the clone
13277 } else {
13278 if (container && container.parentNode === this.renderTo) {
13279 this.renderTo.removeChild(container); // do not clone this
13280 }
13281 this.renderToClone = clone = this.renderTo.cloneNode(0);
13282 css(clone, {
13283 position: 'absolute',
13284 top: '-9999px',
13285 display: 'block' // #833
13286 });
13287 if (clone.style.setProperty) { // #2631
13288 clone.style.setProperty('display', 'block', 'important');
13289 }
13290 doc.body.appendChild(clone);
13291 if (container) {
13292 clone.appendChild(container);
13293 }
13294 }
13295 },
13296
13297 /**
13298 * Setter for the chart class name
13299 */
13300 setClassName: function(className) {
13301 this.container.className = 'highcharts-container ' + (className || '');
13302 },
13303
13304 /**
13305 * Get the containing element, determine the size and create the inner container
13306 * div to hold the chart
13307 */
13308 getContainer: function() {
13309 var chart = this,
13310 container,
13311 options = chart.options,
13312 optionsChart = options.chart,
13313 chartWidth,
13314 chartHeight,
13315 renderTo = chart.renderTo,
13316 indexAttrName = 'data-highcharts-chart',
13317 oldChartIndex,
13318 Ren,
13319 containerId = 'highcharts-' + H.idCounter++,
13320 containerStyle,
13321 key;
13322
13323 if (!renderTo) {
13324 chart.renderTo = renderTo = optionsChart.renderTo;
13325 }
13326
13327 if (isString(renderTo)) {
13328 chart.renderTo = renderTo = doc.getElementById(renderTo);
13329 }
13330
13331 // Display an error if the renderTo is wrong
13332 if (!renderTo) {
13333 error(13, true);
13334 }
13335
13336 // If the container already holds a chart, destroy it. The check for hasRendered is there
13337 // because web pages that are saved to disk from the browser, will preserve the data-highcharts-chart
13338 // attribute and the SVG contents, but not an interactive chart. So in this case,
13339 // charts[oldChartIndex] will point to the wrong chart if any (#2609).
13340 oldChartIndex = pInt(attr(renderTo, indexAttrName));
13341 if (isNumber(oldChartIndex) && charts[oldChartIndex] && charts[oldChartIndex].hasRendered) {
13342 charts[oldChartIndex].destroy();
13343 }
13344
13345 // Make a reference to the chart from the div
13346 attr(renderTo, indexAttrName, chart.index);
13347
13348 // remove previous chart
13349 renderTo.innerHTML = '';
13350
13351 // If the container doesn't have an offsetWidth, it has or is a child of a node
13352 // that has display:none. We need to temporarily move it out to a visible
13353 // state to determine the size, else the legend and tooltips won't render
13354 // properly. The allowClone option is used in sparklines as a micro optimization,
13355 // saving about 1-2 ms each chart.
13356 if (!optionsChart.skipClone && !renderTo.offsetWidth) {
13357 chart.cloneRenderTo();
13358 }
13359
13360 // get the width and height
13361 chart.getChartSize();
13362 chartWidth = chart.chartWidth;
13363 chartHeight = chart.chartHeight;
13364
13365 // Create the inner container
13366
13367 containerStyle = extend({
13368 position: 'relative',
13369 overflow: 'hidden', // needed for context menu (avoid scrollbars) and
13370 // content overflow in IE
13371 width: chartWidth + 'px',
13372 height: chartHeight + 'px',
13373 textAlign: 'left',
13374 lineHeight: 'normal', // #427
13375 zIndex: 0, // #1072
13376 '-webkit-tap-highlight-color': 'rgba(0,0,0,0)'
13377 });
13378
13379 chart.container = container = createElement('div', {
13380 id: containerId
13381 },
13382 containerStyle,
13383 chart.renderToClone || renderTo
13384 );
13385
13386 // cache the cursor (#1650)
13387 chart._cursor = container.style.cursor;
13388
13389 // Initialize the renderer
13390 Ren = H[optionsChart.renderer] || Renderer;
13391 chart.renderer = new Ren(
13392 container,
13393 chartWidth,
13394 chartHeight,
13395 null,
13396 optionsChart.forExport,
13397 options.exporting && options.exporting.allowHTML
13398 );
13399
13400
13401 chart.setClassName(optionsChart.className);
13402
13403 chart.renderer.setStyle(optionsChart.style);
13404
13405
13406 // Add a reference to the charts index
13407 chart.renderer.chartIndex = chart.index;
13408 },
13409
13410 /**
13411 * Calculate margins by rendering axis labels in a preliminary position. Title,
13412 * subtitle and legend have already been rendered at this stage, but will be
13413 * moved into their final positions
13414 */
13415 getMargins: function(skipAxes) {
13416 var chart = this,
13417 spacing = chart.spacing,
13418 margin = chart.margin,
13419 titleOffset = chart.titleOffset;
13420
13421 chart.resetMargins();
13422
13423 // Adjust for title and subtitle
13424 if (titleOffset && !defined(margin[0])) {
13425 chart.plotTop = Math.max(chart.plotTop, titleOffset + chart.options.title.margin + spacing[0]);
13426 }
13427
13428 // Adjust for legend
13429 if (chart.legend.display) {
13430 chart.legend.adjustMargins(margin, spacing);
13431 }
13432
13433 // adjust for scroller
13434 if (chart.extraBottomMargin) {
13435 chart.marginBottom += chart.extraBottomMargin;
13436 }
13437 if (chart.extraTopMargin) {
13438 chart.plotTop += chart.extraTopMargin;
13439 }
13440 if (!skipAxes) {
13441 this.getAxisMargins();
13442 }
13443 },
13444
13445 getAxisMargins: function() {
13446
13447 var chart = this,
13448 axisOffset = chart.axisOffset = [0, 0, 0, 0], // top, right, bottom, left
13449 margin = chart.margin;
13450
13451 // pre-render axes to get labels offset width
13452 if (chart.hasCartesianSeries) {
13453 each(chart.axes, function(axis) {
13454 if (axis.visible) {
13455 axis.getOffset();
13456 }
13457 });
13458 }
13459
13460 // Add the axis offsets
13461 each(marginNames, function(m, side) {
13462 if (!defined(margin[side])) {
13463 chart[m] += axisOffset[side];
13464 }
13465 });
13466
13467 chart.setChartSize();
13468
13469 },
13470
13471 /**
13472 * Resize the chart to its container if size is not explicitly set
13473 */
13474 reflow: function(e) {
13475 var chart = this,
13476 optionsChart = chart.options.chart,
13477 renderTo = chart.renderTo,
13478 hasUserWidth = defined(optionsChart.width),
13479 width = optionsChart.width || getStyle(renderTo, 'width'),
13480 height = optionsChart.height || getStyle(renderTo, 'height'),
13481 target = e ? e.target : win;
13482
13483 // Width and height checks for display:none. Target is doc in IE8 and Opera,
13484 // win in Firefox, Chrome and IE9.
13485 if (!hasUserWidth && !chart.isPrinting && width && height && (target === win || target === doc)) { // #1093
13486 if (width !== chart.containerWidth || height !== chart.containerHeight) {
13487 clearTimeout(chart.reflowTimeout);
13488 // When called from window.resize, e is set, else it's called directly (#2224)
13489 chart.reflowTimeout = syncTimeout(function() {
13490 if (chart.container) { // It may have been destroyed in the meantime (#1257)
13491 chart.setSize(undefined, undefined, false);
13492 }
13493 }, e ? 100 : 0);
13494 }
13495 chart.containerWidth = width;
13496 chart.containerHeight = height;
13497 }
13498 },
13499
13500 /**
13501 * Add the event handlers necessary for auto resizing
13502 */
13503 initReflow: function() {
13504 var chart = this,
13505 reflow = function(e) {
13506 chart.reflow(e);
13507 };
13508
13509
13510 addEvent(win, 'resize', reflow);
13511 addEvent(chart, 'destroy', function() {
13512 removeEvent(win, 'resize', reflow);
13513 });
13514 },
13515
13516 /**
13517 * Resize the chart to a given width and height
13518 * @param {Number} width
13519 * @param {Number} height
13520 * @param {Object|Boolean} animation
13521 */
13522 setSize: function(width, height, animation) {
13523 var chart = this,
13524 renderer = chart.renderer,
13525 globalAnimation;
13526
13527 // Handle the isResizing counter
13528 chart.isResizing += 1;
13529
13530 // set the animation for the current process
13531 H.setAnimation(animation, chart);
13532
13533 chart.oldChartHeight = chart.chartHeight;
13534 chart.oldChartWidth = chart.chartWidth;
13535 if (width !== undefined) {
13536 chart.options.chart.width = width;
13537 }
13538 if (height !== undefined) {
13539 chart.options.chart.height = height;
13540 }
13541 chart.getChartSize();
13542
13543 // Resize the container with the global animation applied if enabled (#2503)
13544
13545 globalAnimation = renderer.globalAnimation;
13546 (globalAnimation ? animate : css)(chart.container, {
13547 width: chart.chartWidth + 'px',
13548 height: chart.chartHeight + 'px'
13549 }, globalAnimation);
13550
13551
13552 chart.setChartSize(true);
13553 renderer.setSize(chart.chartWidth, chart.chartHeight, animation);
13554
13555 // handle axes
13556 each(chart.axes, function(axis) {
13557 axis.isDirty = true;
13558 axis.setScale();
13559 });
13560
13561 // make sure non-cartesian series are also handled
13562 each(chart.series, function(serie) {
13563 serie.isDirty = true;
13564 });
13565
13566 chart.isDirtyLegend = true; // force legend redraw
13567 chart.isDirtyBox = true; // force redraw of plot and chart border
13568
13569 chart.layOutTitles(); // #2857
13570 chart.getMargins();
13571
13572 if (chart.setResponsive) {
13573 chart.setResponsive(false);
13574 }
13575 chart.redraw(animation);
13576
13577
13578 chart.oldChartHeight = null;
13579 fireEvent(chart, 'resize');
13580
13581 // Fire endResize and set isResizing back. If animation is disabled, fire without delay
13582 syncTimeout(function() {
13583 if (chart) {
13584 fireEvent(chart, 'endResize', null, function() {
13585 chart.isResizing -= 1;
13586 });
13587 }
13588 }, animObject(globalAnimation).duration);
13589 },
13590
13591 /**
13592 * Set the public chart properties. This is done before and after the pre-render
13593 * to determine margin sizes
13594 */
13595 setChartSize: function(skipAxes) {
13596 var chart = this,
13597 inverted = chart.inverted,
13598 renderer = chart.renderer,
13599 chartWidth = chart.chartWidth,
13600 chartHeight = chart.chartHeight,
13601 optionsChart = chart.options.chart,
13602 spacing = chart.spacing,
13603 clipOffset = chart.clipOffset,
13604 clipX,
13605 clipY,
13606 plotLeft,
13607 plotTop,
13608 plotWidth,
13609 plotHeight,
13610 plotBorderWidth;
13611
13612 chart.plotLeft = plotLeft = Math.round(chart.plotLeft);
13613 chart.plotTop = plotTop = Math.round(chart.plotTop);
13614 chart.plotWidth = plotWidth = Math.max(0, Math.round(chartWidth - plotLeft - chart.marginRight));
13615 chart.plotHeight = plotHeight = Math.max(0, Math.round(chartHeight - plotTop - chart.marginBottom));
13616
13617 chart.plotSizeX = inverted ? plotHeight : plotWidth;
13618 chart.plotSizeY = inverted ? plotWidth : plotHeight;
13619
13620 chart.plotBorderWidth = optionsChart.plotBorderWidth || 0;
13621
13622 // Set boxes used for alignment
13623 chart.spacingBox = renderer.spacingBox = {
13624 x: spacing[3],
13625 y: spacing[0],
13626 width: chartWidth - spacing[3] - spacing[1],
13627 height: chartHeight - spacing[0] - spacing[2]
13628 };
13629 chart.plotBox = renderer.plotBox = {
13630 x: plotLeft,
13631 y: plotTop,
13632 width: plotWidth,
13633 height: plotHeight
13634 };
13635
13636 plotBorderWidth = 2 * Math.floor(chart.plotBorderWidth / 2);
13637 clipX = Math.ceil(Math.max(plotBorderWidth, clipOffset[3]) / 2);
13638 clipY = Math.ceil(Math.max(plotBorderWidth, clipOffset[0]) / 2);
13639 chart.clipBox = {
13640 x: clipX,
13641 y: clipY,
13642 width: Math.floor(chart.plotSizeX - Math.max(plotBorderWidth, clipOffset[1]) / 2 - clipX),
13643 height: Math.max(0, Math.floor(chart.plotSizeY - Math.max(plotBorderWidth, clipOffset[2]) / 2 - clipY))
13644 };
13645
13646 if (!skipAxes) {
13647 each(chart.axes, function(axis) {
13648 axis.setAxisSize();
13649 axis.setAxisTranslation();
13650 });
13651 }
13652 },
13653
13654 /**
13655 * Initial margins before auto size margins are applied
13656 */
13657 resetMargins: function() {
13658 var chart = this,
13659 chartOptions = chart.options.chart;
13660
13661 // Create margin and spacing array
13662 each(['margin', 'spacing'], function splashArrays(target) {
13663 var value = chartOptions[target],
13664 values = isObject(value) ? value : [value, value, value, value];
13665
13666 each(['Top', 'Right', 'Bottom', 'Left'], function(sideName, side) {
13667 chart[target][side] = pick(chartOptions[target + sideName], values[side]);
13668 });
13669 });
13670
13671 // Set margin names like chart.plotTop, chart.plotLeft, chart.marginRight, chart.marginBottom.
13672 each(marginNames, function(m, side) {
13673 chart[m] = pick(chart.margin[side], chart.spacing[side]);
13674 });
13675 chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left
13676 chart.clipOffset = [0, 0, 0, 0];
13677 },
13678
13679 /**
13680 * Draw the borders and backgrounds for chart and plot area
13681 */
13682 drawChartBox: function() {
13683 var chart = this,
13684 optionsChart = chart.options.chart,
13685 renderer = chart.renderer,
13686 chartWidth = chart.chartWidth,
13687 chartHeight = chart.chartHeight,
13688 chartBackground = chart.chartBackground,
13689 plotBackground = chart.plotBackground,
13690 plotBorder = chart.plotBorder,
13691 chartBorderWidth,
13692
13693 plotBGImage = chart.plotBGImage,
13694 chartBackgroundColor = optionsChart.backgroundColor,
13695 plotBackgroundColor = optionsChart.plotBackgroundColor,
13696 plotBackgroundImage = optionsChart.plotBackgroundImage,
13697
13698 mgn,
13699 bgAttr,
13700 plotLeft = chart.plotLeft,
13701 plotTop = chart.plotTop,
13702 plotWidth = chart.plotWidth,
13703 plotHeight = chart.plotHeight,
13704 plotBox = chart.plotBox,
13705 clipRect = chart.clipRect,
13706 clipBox = chart.clipBox,
13707 verb = 'animate';
13708
13709 // Chart area
13710 if (!chartBackground) {
13711 chart.chartBackground = chartBackground = renderer.rect()
13712 .addClass('highcharts-background')
13713 .add();
13714 verb = 'attr';
13715 }
13716
13717
13718 // Presentational
13719 chartBorderWidth = optionsChart.borderWidth || 0;
13720 mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0);
13721
13722 bgAttr = {
13723 fill: chartBackgroundColor || 'none'
13724 };
13725
13726 if (chartBorderWidth || chartBackground['stroke-width']) { // #980
13727 bgAttr.stroke = optionsChart.borderColor;
13728 bgAttr['stroke-width'] = chartBorderWidth;
13729 }
13730 chartBackground
13731 .attr(bgAttr)
13732 .shadow(optionsChart.shadow);
13733
13734 chartBackground[verb]({
13735 x: mgn / 2,
13736 y: mgn / 2,
13737 width: chartWidth - mgn - chartBorderWidth % 2,
13738 height: chartHeight - mgn - chartBorderWidth % 2,
13739 r: optionsChart.borderRadius
13740 });
13741
13742 // Plot background
13743 verb = 'animate';
13744 if (!plotBackground) {
13745 verb = 'attr';
13746 chart.plotBackground = plotBackground = renderer.rect()
13747 .addClass('highcharts-plot-background')
13748 .add();
13749 }
13750 plotBackground[verb](plotBox);
13751
13752
13753 // Presentational attributes for the background
13754 plotBackground
13755 .attr({
13756 fill: plotBackgroundColor || 'none'
13757 })
13758 .shadow(optionsChart.plotShadow);
13759
13760 // Create the background image
13761 if (plotBackgroundImage) {
13762 if (!plotBGImage) {
13763 chart.plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight)
13764 .add();
13765 } else {
13766 plotBGImage.animate(plotBox);
13767 }
13768 }
13769
13770
13771 // Plot clip
13772 if (!clipRect) {
13773 chart.clipRect = renderer.clipRect(clipBox);
13774 } else {
13775 clipRect.animate({
13776 width: clipBox.width,
13777 height: clipBox.height
13778 });
13779 }
13780
13781 // Plot area border
13782 verb = 'animate';
13783 if (!plotBorder) {
13784 verb = 'attr';
13785 chart.plotBorder = plotBorder = renderer.rect()
13786 .addClass('highcharts-plot-border')
13787 .attr({
13788 zIndex: 1 // Above the grid
13789 })
13790 .add();
13791 }
13792
13793
13794 // Presentational
13795 plotBorder.attr({
13796 stroke: optionsChart.plotBorderColor,
13797 'stroke-width': optionsChart.plotBorderWidth || 0,
13798 fill: 'none'
13799 });
13800
13801
13802 plotBorder[verb](plotBorder.crisp({
13803 x: plotLeft,
13804 y: plotTop,
13805 width: plotWidth,
13806 height: plotHeight
13807 }, -plotBorder.strokeWidth())); //#3282 plotBorder should be negative;
13808
13809 // reset
13810 chart.isDirtyBox = false;
13811 },
13812
13813 /**
13814 * Detect whether a certain chart property is needed based on inspecting its options
13815 * and series. This mainly applies to the chart.inverted property, and in extensions to
13816 * the chart.angular and chart.polar properties.
13817 */
13818 propFromSeries: function() {
13819 var chart = this,
13820 optionsChart = chart.options.chart,
13821 klass,
13822 seriesOptions = chart.options.series,
13823 i,
13824 value;
13825
13826
13827 each(['inverted', 'angular', 'polar'], function(key) {
13828
13829 // The default series type's class
13830 klass = seriesTypes[optionsChart.type || optionsChart.defaultSeriesType];
13831
13832 // Get the value from available chart-wide properties
13833 value =
13834 optionsChart[key] || // It is set in the options
13835 (klass && klass.prototype[key]); // The default series class requires it
13836
13837 // 4. Check if any the chart's series require it
13838 i = seriesOptions && seriesOptions.length;
13839 while (!value && i--) {
13840 klass = seriesTypes[seriesOptions[i].type];
13841 if (klass && klass.prototype[key]) {
13842 value = true;
13843 }
13844 }
13845
13846 // Set the chart property
13847 chart[key] = value;
13848 });
13849
13850 },
13851
13852 /**
13853 * Link two or more series together. This is done initially from Chart.render,
13854 * and after Chart.addSeries and Series.remove.
13855 */
13856 linkSeries: function() {
13857 var chart = this,
13858 chartSeries = chart.series;
13859
13860 // Reset links
13861 each(chartSeries, function(series) {
13862 series.linkedSeries.length = 0;
13863 });
13864
13865 // Apply new links
13866 each(chartSeries, function(series) {
13867 var linkedTo = series.options.linkedTo;
13868 if (isString(linkedTo)) {
13869 if (linkedTo === ':previous') {
13870 linkedTo = chart.series[series.index - 1];
13871 } else {
13872 linkedTo = chart.get(linkedTo);
13873 }
13874 if (linkedTo && linkedTo.linkedParent !== series) { // #3341 avoid mutual linking
13875 linkedTo.linkedSeries.push(series);
13876 series.linkedParent = linkedTo;
13877 series.visible = pick(series.options.visible, linkedTo.options.visible, series.visible); // #3879
13878 }
13879 }
13880 });
13881 },
13882
13883 /**
13884 * Render series for the chart
13885 */
13886 renderSeries: function() {
13887 each(this.series, function(serie) {
13888 serie.translate();
13889 serie.render();
13890 });
13891 },
13892
13893 /**
13894 * Render labels for the chart
13895 */
13896 renderLabels: function() {
13897 var chart = this,
13898 labels = chart.options.labels;
13899 if (labels.items) {
13900 each(labels.items, function(label) {
13901 var style = extend(labels.style, label.style),
13902 x = pInt(style.left) + chart.plotLeft,
13903 y = pInt(style.top) + chart.plotTop + 12;
13904
13905 // delete to prevent rewriting in IE
13906 delete style.left;
13907 delete style.top;
13908
13909 chart.renderer.text(
13910 label.html,
13911 x,
13912 y
13913 )
13914 .attr({
13915 zIndex: 2
13916 })
13917 .css(style)
13918 .add();
13919
13920 });
13921 }
13922 },
13923
13924 /**
13925 * Render all graphics for the chart
13926 */
13927 render: function() {
13928 var chart = this,
13929 axes = chart.axes,
13930 renderer = chart.renderer,
13931 options = chart.options,
13932 tempWidth,
13933 tempHeight,
13934 redoHorizontal,
13935 redoVertical;
13936
13937 // Title
13938 chart.setTitle();
13939
13940
13941 // Legend
13942 chart.legend = new Legend(chart, options.legend);
13943
13944 // Get stacks
13945 if (chart.getStacks) {
13946 chart.getStacks();
13947 }
13948
13949 // Get chart margins
13950 chart.getMargins(true);
13951 chart.setChartSize();
13952
13953 // Record preliminary dimensions for later comparison
13954 tempWidth = chart.plotWidth;
13955 tempHeight = chart.plotHeight = chart.plotHeight - 21; // 21 is the most common correction for X axis labels
13956
13957 // Get margins by pre-rendering axes
13958 each(axes, function(axis) {
13959 axis.setScale();
13960 });
13961 chart.getAxisMargins();
13962
13963 // If the plot area size has changed significantly, calculate tick positions again
13964 redoHorizontal = tempWidth / chart.plotWidth > 1.1;
13965 redoVertical = tempHeight / chart.plotHeight > 1.05; // Height is more sensitive
13966
13967 if (redoHorizontal || redoVertical) {
13968
13969 each(axes, function(axis) {
13970 if ((axis.horiz && redoHorizontal) || (!axis.horiz && redoVertical)) {
13971 axis.setTickInterval(true); // update to reflect the new margins
13972 }
13973 });
13974 chart.getMargins(); // second pass to check for new labels
13975 }
13976
13977 // Draw the borders and backgrounds
13978 chart.drawChartBox();
13979
13980
13981 // Axes
13982 if (chart.hasCartesianSeries) {
13983 each(axes, function(axis) {
13984 if (axis.visible) {
13985 axis.render();
13986 }
13987 });
13988 }
13989
13990 // The series
13991 if (!chart.seriesGroup) {
13992 chart.seriesGroup = renderer.g('series-group')
13993 .attr({
13994 zIndex: 3
13995 })
13996 .add();
13997 }
13998 chart.renderSeries();
13999
14000 // Labels
14001 chart.renderLabels();
14002
14003 // Credits
14004 chart.addCredits();
14005
14006 // Handle responsiveness
14007 if (chart.setResponsive) {
14008 chart.setResponsive();
14009 }
14010
14011 // Set flag
14012 chart.hasRendered = true;
14013
14014 },
14015
14016 /**
14017 * Show chart credits based on config options
14018 */
14019 addCredits: function(credits) {
14020 var chart = this;
14021
14022 credits = merge(true, this.options.credits, credits);
14023 if (credits.enabled && !this.credits) {
14024 this.credits = this.renderer.text(
14025 credits.text + (this.mapCredits || ''),
14026 0,
14027 0
14028 )
14029 .addClass('highcharts-credits')
14030 .on('click', function() {
14031 if (credits.href) {
14032 win.location.href = credits.href;
14033 }
14034 })
14035 .attr({
14036 align: credits.position.align,
14037 zIndex: 8
14038 })
14039
14040 .css(credits.style)
14041
14042 .add()
14043 .align(credits.position);
14044
14045 // Dynamically update
14046 this.credits.update = function(options) {
14047 chart.credits = chart.credits.destroy();
14048 chart.addCredits(options);
14049 };
14050 }
14051 },
14052
14053 /**
14054 * Clean up memory usage
14055 */
14056 destroy: function() {
14057 var chart = this,
14058 axes = chart.axes,
14059 series = chart.series,
14060 container = chart.container,
14061 i,
14062 parentNode = container && container.parentNode;
14063
14064 // fire the chart.destoy event
14065 fireEvent(chart, 'destroy');
14066
14067 // Delete the chart from charts lookup array
14068 charts[chart.index] = undefined;
14069 H.chartCount--;
14070 chart.renderTo.removeAttribute('data-highcharts-chart');
14071
14072 // remove events
14073 removeEvent(chart);
14074
14075 // ==== Destroy collections:
14076 // Destroy axes
14077 i = axes.length;
14078 while (i--) {
14079 axes[i] = axes[i].destroy();
14080 }
14081
14082 // Destroy scroller & scroller series before destroying base series
14083 if (this.scroller && this.scroller.destroy) {
14084 this.scroller.destroy();
14085 }
14086
14087 // Destroy each series
14088 i = series.length;
14089 while (i--) {
14090 series[i] = series[i].destroy();
14091 }
14092
14093 // ==== Destroy chart properties:
14094 each(['title', 'subtitle', 'chartBackground', 'plotBackground', 'plotBGImage',
14095 'plotBorder', 'seriesGroup', 'clipRect', 'credits', 'pointer',
14096 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', 'renderer'
14097 ], function(name) {
14098 var prop = chart[name];
14099
14100 if (prop && prop.destroy) {
14101 chart[name] = prop.destroy();
14102 }
14103 });
14104
14105 // remove container and all SVG
14106 if (container) { // can break in IE when destroyed before finished loading
14107 container.innerHTML = '';
14108 removeEvent(container);
14109 if (parentNode) {
14110 discardElement(container);
14111 }
14112
14113 }
14114
14115 // clean it all up
14116 for (i in chart) {
14117 delete chart[i];
14118 }
14119
14120 },
14121
14122
14123 /**
14124 * VML namespaces can't be added until after complete. Listening
14125 * for Perini's doScroll hack is not enough.
14126 */
14127 isReadyToRender: function() {
14128 var chart = this;
14129
14130 // Note: win == win.top is required
14131 if ((!svg && (win == win.top && doc.readyState !== 'complete'))) { // eslint-disable-line eqeqeq
14132 doc.attachEvent('onreadystatechange', function() {
14133 doc.detachEvent('onreadystatechange', chart.firstRender);
14134 if (doc.readyState === 'complete') {
14135 chart.firstRender();
14136 }
14137 });
14138 return false;
14139 }
14140 return true;
14141 },
14142
14143 /**
14144 * Prepare for first rendering after all data are loaded
14145 */
14146 firstRender: function() {
14147 var chart = this,
14148 options = chart.options;
14149
14150 // Check whether the chart is ready to render
14151 if (!chart.isReadyToRender()) {
14152 return;
14153 }
14154
14155 // Create the container
14156 chart.getContainer();
14157
14158 // Run an early event after the container and renderer are established
14159 fireEvent(chart, 'init');
14160
14161
14162 chart.resetMargins();
14163 chart.setChartSize();
14164
14165 // Set the common chart properties (mainly invert) from the given series
14166 chart.propFromSeries();
14167
14168 // get axes
14169 chart.getAxes();
14170
14171 // Initialize the series
14172 each(options.series || [], function(serieOptions) {
14173 chart.initSeries(serieOptions);
14174 });
14175
14176 chart.linkSeries();
14177
14178 // Run an event after axes and series are initialized, but before render. At this stage,
14179 // the series data is indexed and cached in the xData and yData arrays, so we can access
14180 // those before rendering. Used in Highstock.
14181 fireEvent(chart, 'beforeRender');
14182
14183 // depends on inverted and on margins being set
14184 if (Pointer) {
14185 chart.pointer = new Pointer(chart, options);
14186 }
14187
14188 chart.render();
14189
14190 // add canvas
14191 chart.renderer.draw();
14192
14193 // Fire the load event if there are no external images
14194 if (!chart.renderer.imgCount && chart.onload) {
14195 chart.onload();
14196 }
14197
14198 // If the chart was rendered outside the top container, put it back in (#3679)
14199 chart.cloneRenderTo(true);
14200
14201 },
14202
14203 /**
14204 * On chart load
14205 */
14206 onload: function() {
14207
14208 // Run callbacks
14209 each([this.callback].concat(this.callbacks), function(fn) {
14210 if (fn && this.index !== undefined) { // Chart destroyed in its own callback (#3600)
14211 fn.apply(this, [this]);
14212 }
14213 }, this);
14214
14215 fireEvent(this, 'load');
14216
14217 // Set up auto resize
14218 this.initReflow();
14219
14220 // Don't run again
14221 this.onload = null;
14222 }
14223
14224 }; // end Chart
14225
14226 }(Highcharts));
14227 (function(H) {
14228 /**
14229 * (c) 2010-2016 Torstein Honsi
14230 *
14231 * License: www.highcharts.com/license
14232 */
14233 'use strict';
14234 var Point,
14235
14236 each = H.each,
14237 extend = H.extend,
14238 erase = H.erase,
14239 fireEvent = H.fireEvent,
14240 format = H.format,
14241 isArray = H.isArray,
14242 isNumber = H.isNumber,
14243 pick = H.pick,
14244 removeEvent = H.removeEvent;
14245
14246 /**
14247 * The Point object and prototype. Inheritable and used as base for PiePoint
14248 */
14249 Point = H.Point = function() {};
14250 Point.prototype = {
14251
14252 /**
14253 * Initialize the point
14254 * @param {Object} series The series object containing this point
14255 * @param {Object} options The data in either number, array or object format
14256 */
14257 init: function(series, options, x) {
14258
14259 var point = this,
14260 colors,
14261 colorCount = series.chart.options.chart.colorCount,
14262 colorIndex;
14263
14264 point.series = series;
14265
14266 point.color = series.color; // #3445
14267
14268 point.applyOptions(options, x);
14269
14270 if (series.options.colorByPoint) {
14271
14272 colors = series.options.colors || series.chart.options.colors;
14273 point.color = point.color || colors[series.colorCounter];
14274 colorCount = colors.length;
14275
14276 colorIndex = series.colorCounter;
14277 series.colorCounter++;
14278 // loop back to zero
14279 if (series.colorCounter === colorCount) {
14280 series.colorCounter = 0;
14281 }
14282 } else {
14283 colorIndex = series.colorIndex;
14284 }
14285 point.colorIndex = pick(point.colorIndex, colorIndex);
14286
14287 series.chart.pointCount++;
14288 return point;
14289 },
14290 /**
14291 * Apply the options containing the x and y data and possible some extra properties.
14292 * This is called on point init or from point.update.
14293 *
14294 * @param {Object} options
14295 */
14296 applyOptions: function(options, x) {
14297 var point = this,
14298 series = point.series,
14299 pointValKey = series.options.pointValKey || series.pointValKey;
14300
14301 options = Point.prototype.optionsToObject.call(this, options);
14302
14303 // copy options directly to point
14304 extend(point, options);
14305 point.options = point.options ? extend(point.options, options) : options;
14306
14307 // Since options are copied into the Point instance, some accidental options must be shielded (#5681)
14308 if (options.group) {
14309 delete point.group;
14310 }
14311
14312 // For higher dimension series types. For instance, for ranges, point.y is mapped to point.low.
14313 if (pointValKey) {
14314 point.y = point[pointValKey];
14315 }
14316 point.isNull = pick(
14317 point.isValid && !point.isValid(),
14318 point.x === null || !isNumber(point.y, true)
14319 ); // #3571, check for NaN
14320
14321 // If no x is set by now, get auto incremented value. All points must have an
14322 // x value, however the y value can be null to create a gap in the series
14323 if ('name' in point && x === undefined && series.xAxis && series.xAxis.hasNames) {
14324 point.x = series.xAxis.nameToX(point);
14325 }
14326 if (point.x === undefined && series) {
14327 if (x === undefined) {
14328 point.x = series.autoIncrement(point);
14329 } else {
14330 point.x = x;
14331 }
14332 }
14333
14334 return point;
14335 },
14336
14337 /**
14338 * Transform number or array configs into objects
14339 */
14340 optionsToObject: function(options) {
14341 var ret = {},
14342 series = this.series,
14343 keys = series.options.keys,
14344 pointArrayMap = keys || series.pointArrayMap || ['y'],
14345 valueCount = pointArrayMap.length,
14346 firstItemType,
14347 i = 0,
14348 j = 0;
14349
14350 if (isNumber(options) || options === null) {
14351 ret[pointArrayMap[0]] = options;
14352
14353 } else if (isArray(options)) {
14354 // with leading x value
14355 if (!keys && options.length > valueCount) {
14356 firstItemType = typeof options[0];
14357 if (firstItemType === 'string') {
14358 ret.name = options[0];
14359 } else if (firstItemType === 'number') {
14360 ret.x = options[0];
14361 }
14362 i++;
14363 }
14364 while (j < valueCount) {
14365 if (!keys || options[i] !== undefined) { // Skip undefined positions for keys
14366 ret[pointArrayMap[j]] = options[i];
14367 }
14368 i++;
14369 j++;
14370 }
14371 } else if (typeof options === 'object') {
14372 ret = options;
14373
14374 // This is the fastest way to detect if there are individual point dataLabels that need
14375 // to be considered in drawDataLabels. These can only occur in object configs.
14376 if (options.dataLabels) {
14377 series._hasPointLabels = true;
14378 }
14379
14380 // Same approach as above for markers
14381 if (options.marker) {
14382 series._hasPointMarkers = true;
14383 }
14384 }
14385 return ret;
14386 },
14387
14388 /**
14389 * Get the CSS class names for individual points
14390 * @returns {String} The class name
14391 */
14392 getClassName: function() {
14393 return 'highcharts-point' +
14394 (this.selected ? ' highcharts-point-select' : '') +
14395 (this.negative ? ' highcharts-negative' : '') +
14396 (this.colorIndex !== undefined ? ' highcharts-color-' + this.colorIndex : '') +
14397 (this.options.className ? ' ' + this.options.className : '');
14398 },
14399
14400 /**
14401 * Return the zone that the point belongs to
14402 */
14403 getZone: function() {
14404 var series = this.series,
14405 zones = series.zones,
14406 zoneAxis = series.zoneAxis || 'y',
14407 i = 0,
14408 zone;
14409
14410 zone = zones[i];
14411 while (this[zoneAxis] >= zone.value) {
14412 zone = zones[++i];
14413 }
14414
14415 if (zone && zone.color && !this.options.color) {
14416 this.color = zone.color;
14417 }
14418
14419 return zone;
14420 },
14421
14422 /**
14423 * Destroy a point to clear memory. Its reference still stays in series.data.
14424 */
14425 destroy: function() {
14426 var point = this,
14427 series = point.series,
14428 chart = series.chart,
14429 hoverPoints = chart.hoverPoints,
14430 prop;
14431
14432 chart.pointCount--;
14433
14434 if (hoverPoints) {
14435 point.setState();
14436 erase(hoverPoints, point);
14437 if (!hoverPoints.length) {
14438 chart.hoverPoints = null;
14439 }
14440
14441 }
14442 if (point === chart.hoverPoint) {
14443 point.onMouseOut();
14444 }
14445
14446 // remove all events
14447 if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive
14448 removeEvent(point);
14449 point.destroyElements();
14450 }
14451
14452 if (point.legendItem) { // pies have legend items
14453 chart.legend.destroyItem(point);
14454 }
14455
14456 for (prop in point) {
14457 point[prop] = null;
14458 }
14459
14460
14461 },
14462
14463 /**
14464 * Destroy SVG elements associated with the point
14465 */
14466 destroyElements: function() {
14467 var point = this,
14468 props = ['graphic', 'dataLabel', 'dataLabelUpper', 'connector', 'shadowGroup'],
14469 prop,
14470 i = 6;
14471 while (i--) {
14472 prop = props[i];
14473 if (point[prop]) {
14474 point[prop] = point[prop].destroy();
14475 }
14476 }
14477 },
14478
14479 /**
14480 * Return the configuration hash needed for the data label and tooltip formatters
14481 */
14482 getLabelConfig: function() {
14483 return {
14484 x: this.category,
14485 y: this.y,
14486 color: this.color,
14487 key: this.name || this.category,
14488 series: this.series,
14489 point: this,
14490 percentage: this.percentage,
14491 total: this.total || this.stackTotal
14492 };
14493 },
14494
14495 /**
14496 * Extendable method for formatting each point's tooltip line
14497 *
14498 * @return {String} A string to be concatenated in to the common tooltip text
14499 */
14500 tooltipFormatter: function(pointFormat) {
14501
14502 // Insert options for valueDecimals, valuePrefix, and valueSuffix
14503 var series = this.series,
14504 seriesTooltipOptions = series.tooltipOptions,
14505 valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''),
14506 valuePrefix = seriesTooltipOptions.valuePrefix || '',
14507 valueSuffix = seriesTooltipOptions.valueSuffix || '';
14508
14509 // Loop over the point array map and replace unformatted values with sprintf formatting markup
14510 each(series.pointArrayMap || ['y'], function(key) {
14511 key = '{point.' + key; // without the closing bracket
14512 if (valuePrefix || valueSuffix) {
14513 pointFormat = pointFormat.replace(key + '}', valuePrefix + key + '}' + valueSuffix);
14514 }
14515 pointFormat = pointFormat.replace(key + '}', key + ':,.' + valueDecimals + 'f}');
14516 });
14517
14518 return format(pointFormat, {
14519 point: this,
14520 series: this.series
14521 });
14522 },
14523
14524 /**
14525 * Fire an event on the Point object.
14526 * @param {String} eventType
14527 * @param {Object} eventArgs Additional event arguments
14528 * @param {Function} defaultFunction Default event handler
14529 */
14530 firePointEvent: function(eventType, eventArgs, defaultFunction) {
14531 var point = this,
14532 series = this.series,
14533 seriesOptions = series.options;
14534
14535 // load event handlers on demand to save time on mouseover/out
14536 if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) {
14537 this.importEvents();
14538 }
14539
14540 // add default handler if in selection mode
14541 if (eventType === 'click' && seriesOptions.allowPointSelect) {
14542 defaultFunction = function(event) {
14543 // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera
14544 if (point.select) { // Could be destroyed by prior event handlers (#2911)
14545 point.select(null, event.ctrlKey || event.metaKey || event.shiftKey);
14546 }
14547 };
14548 }
14549
14550 fireEvent(this, eventType, eventArgs, defaultFunction);
14551 },
14552 visible: true
14553 };
14554
14555 }(Highcharts));
14556 (function(H) {
14557 /**
14558 * (c) 2010-2016 Torstein Honsi
14559 *
14560 * License: www.highcharts.com/license
14561 */
14562 'use strict';
14563 var addEvent = H.addEvent,
14564 animObject = H.animObject,
14565 arrayMax = H.arrayMax,
14566 arrayMin = H.arrayMin,
14567 correctFloat = H.correctFloat,
14568 Date = H.Date,
14569 defaultOptions = H.defaultOptions,
14570 defaultPlotOptions = H.defaultPlotOptions,
14571 defined = H.defined,
14572 each = H.each,
14573 erase = H.erase,
14574 error = H.error,
14575 extend = H.extend,
14576 fireEvent = H.fireEvent,
14577 grep = H.grep,
14578 isArray = H.isArray,
14579 isNumber = H.isNumber,
14580 isString = H.isString,
14581 LegendSymbolMixin = H.LegendSymbolMixin, // @todo add as a requirement
14582 merge = H.merge,
14583 pick = H.pick,
14584 Point = H.Point, // @todo add as a requirement
14585 removeEvent = H.removeEvent,
14586 splat = H.splat,
14587 stableSort = H.stableSort,
14588 SVGElement = H.SVGElement,
14589 syncTimeout = H.syncTimeout,
14590 win = H.win;
14591
14592 /**
14593 * @classDescription The base function which all other series types inherit from. The data in the series is stored
14594 * in various arrays.
14595 *
14596 * - First, series.options.data contains all the original config options for
14597 * each point whether added by options or methods like series.addPoint.
14598 * - Next, series.data contains those values converted to points, but in case the series data length
14599 * exceeds the cropThreshold, or if the data is grouped, series.data doesn't contain all the points. It
14600 * only contains the points that have been created on demand.
14601 * - Then there's series.points that contains all currently visible point objects. In case of cropping,
14602 * the cropped-away points are not part of this array. The series.points array starts at series.cropStart
14603 * compared to series.data and series.options.data. If however the series data is grouped, these can't
14604 * be correlated one to one.
14605 * - series.xData and series.processedXData contain clean x values, equivalent to series.data and series.points.
14606 * - series.yData and series.processedYData contain clean x values, equivalent to series.data and series.points.
14607 *
14608 * @param {Object} chart
14609 * @param {Object} options
14610 */
14611 H.Series = H.seriesType('line', null, { // base series options
14612
14613 //cursor: 'default',
14614 //dashStyle: null,
14615 //linecap: 'round',
14616 lineWidth: 2,
14617 //shadow: false,
14618
14619 allowPointSelect: false,
14620 showCheckbox: false,
14621 animation: {
14622 duration: 1000
14623 },
14624 //clip: true,
14625 //connectNulls: false,
14626 //enableMouseTracking: true,
14627 events: {},
14628 //legendIndex: 0,
14629 // stacking: null,
14630 marker: {
14631
14632 lineWidth: 0,
14633 lineColor: '#ffffff',
14634 //fillColor: null,
14635
14636 //enabled: true,
14637 //symbol: null,
14638 radius: 4,
14639 states: { // states for a single point
14640 hover: {
14641 enabled: true,
14642 radiusPlus: 2,
14643
14644 lineWidthPlus: 1
14645
14646 },
14647
14648 select: {
14649 fillColor: '#cccccc',
14650 lineColor: '#000000',
14651 lineWidth: 2
14652 }
14653
14654 }
14655 },
14656 point: {
14657 events: {}
14658 },
14659 dataLabels: {
14660 align: 'center',
14661 // defer: true,
14662 // enabled: false,
14663 formatter: function() {
14664 return this.y === null ? '' : H.numberFormat(this.y, -1);
14665 },
14666
14667 style: {
14668 fontSize: '11px',
14669 fontWeight: 'bold',
14670 color: 'contrast',
14671 textShadow: '1px 1px contrast, -1px -1px contrast, -1px 1px contrast, 1px -1px contrast'
14672 },
14673 // backgroundColor: undefined,
14674 // borderColor: undefined,
14675 // borderWidth: undefined,
14676 // shadow: false
14677
14678 verticalAlign: 'bottom', // above singular point
14679 x: 0,
14680 y: 0,
14681 // borderRadius: undefined,
14682 padding: 5
14683 },
14684 cropThreshold: 300, // draw points outside the plot area when the number of points is less than this
14685 pointRange: 0,
14686 //pointStart: 0,
14687 //pointInterval: 1,
14688 //showInLegend: null, // auto: true for standalone series, false for linked series
14689 softThreshold: true,
14690 states: { // states for the entire series
14691 hover: {
14692 //enabled: false,
14693 lineWidthPlus: 1,
14694 marker: {
14695 // lineWidth: base + 1,
14696 // radius: base + 1
14697 },
14698 halo: {
14699 size: 10,
14700
14701 opacity: 0.25
14702
14703 }
14704 },
14705 select: {
14706 marker: {}
14707 }
14708 },
14709 stickyTracking: true,
14710 //tooltip: {
14711 //pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b>'
14712 //valueDecimals: null,
14713 //xDateFormat: '%A, %b %e, %Y',
14714 //valuePrefix: '',
14715 //ySuffix: ''
14716 //}
14717 turboThreshold: 1000
14718 // zIndex: null
14719 },
14720
14721 // Prototype properties
14722 {
14723 isCartesian: true,
14724 pointClass: Point,
14725 sorted: true, // requires the data to be sorted
14726 requireSorting: true,
14727 directTouch: false,
14728 axisTypes: ['xAxis', 'yAxis'],
14729 colorCounter: 0,
14730 parallelArrays: ['x', 'y'], // each point's x and y values are stored in this.xData and this.yData
14731 coll: 'series',
14732 init: function(chart, options) {
14733 var series = this,
14734 eventType,
14735 events,
14736 chartSeries = chart.series,
14737 sortByIndex = function(a, b) {
14738 return pick(a.options.index, a._i) - pick(b.options.index, b._i);
14739 };
14740
14741 series.chart = chart;
14742 series.options = options = series.setOptions(options); // merge with plotOptions
14743 series.linkedSeries = [];
14744
14745 // bind the axes
14746 series.bindAxes();
14747
14748 // set some variables
14749 extend(series, {
14750 name: options.name,
14751 state: '',
14752 visible: options.visible !== false, // true by default
14753 selected: options.selected === true // false by default
14754 });
14755
14756 // register event listeners
14757 events = options.events;
14758 for (eventType in events) {
14759 addEvent(series, eventType, events[eventType]);
14760 }
14761 if (
14762 (events && events.click) ||
14763 (options.point && options.point.events && options.point.events.click) ||
14764 options.allowPointSelect
14765 ) {
14766 chart.runTrackerClick = true;
14767 }
14768
14769 series.getColor();
14770 series.getSymbol();
14771
14772 // Set the data
14773 each(series.parallelArrays, function(key) {
14774 series[key + 'Data'] = [];
14775 });
14776 series.setData(options.data, false);
14777
14778 // Mark cartesian
14779 if (series.isCartesian) {
14780 chart.hasCartesianSeries = true;
14781 }
14782
14783 // Register it in the chart
14784 chartSeries.push(series);
14785 series._i = chartSeries.length - 1;
14786
14787 // Sort series according to index option (#248, #1123, #2456)
14788 stableSort(chartSeries, sortByIndex);
14789 if (this.yAxis) {
14790 stableSort(this.yAxis.series, sortByIndex);
14791 }
14792
14793 each(chartSeries, function(series, i) {
14794 series.index = i;
14795 series.name = series.name || 'Series ' + (i + 1);
14796 });
14797
14798 },
14799
14800 /**
14801 * Set the xAxis and yAxis properties of cartesian series, and register the series
14802 * in the axis.series array
14803 */
14804 bindAxes: function() {
14805 var series = this,
14806 seriesOptions = series.options,
14807 chart = series.chart,
14808 axisOptions;
14809
14810 each(series.axisTypes || [], function(AXIS) { // repeat for xAxis and yAxis
14811
14812 each(chart[AXIS], function(axis) { // loop through the chart's axis objects
14813 axisOptions = axis.options;
14814
14815 // apply if the series xAxis or yAxis option mathches the number of the
14816 // axis, or if undefined, use the first axis
14817 if ((seriesOptions[AXIS] === axisOptions.index) ||
14818 (seriesOptions[AXIS] !== undefined && seriesOptions[AXIS] === axisOptions.id) ||
14819 (seriesOptions[AXIS] === undefined && axisOptions.index === 0)) {
14820
14821 // register this series in the axis.series lookup
14822 axis.series.push(series);
14823
14824 // set this series.xAxis or series.yAxis reference
14825 series[AXIS] = axis;
14826
14827 // mark dirty for redraw
14828 axis.isDirty = true;
14829 }
14830 });
14831
14832 // The series needs an X and an Y axis
14833 if (!series[AXIS] && series.optionalAxis !== AXIS) {
14834 error(18, true);
14835 }
14836
14837 });
14838 },
14839
14840 /**
14841 * For simple series types like line and column, the data values are held in arrays like
14842 * xData and yData for quick lookup to find extremes and more. For multidimensional series
14843 * like bubble and map, this can be extended with arrays like zData and valueData by
14844 * adding to the series.parallelArrays array.
14845 */
14846 updateParallelArrays: function(point, i) {
14847 var series = point.series,
14848 args = arguments,
14849 fn = isNumber(i) ?
14850 // Insert the value in the given position
14851 function(key) {
14852 var val = key === 'y' && series.toYData ? series.toYData(point) : point[key];
14853 series[key + 'Data'][i] = val;
14854 } :
14855 // Apply the method specified in i with the following arguments as arguments
14856 function(key) {
14857 Array.prototype[i].apply(series[key + 'Data'], Array.prototype.slice.call(args, 2));
14858 };
14859
14860 each(series.parallelArrays, fn);
14861 },
14862
14863 /**
14864 * Return an auto incremented x value based on the pointStart and pointInterval options.
14865 * This is only used if an x value is not given for the point that calls autoIncrement.
14866 */
14867 autoIncrement: function() {
14868
14869 var options = this.options,
14870 xIncrement = this.xIncrement,
14871 date,
14872 pointInterval,
14873 pointIntervalUnit = options.pointIntervalUnit;
14874
14875 xIncrement = pick(xIncrement, options.pointStart, 0);
14876
14877 this.pointInterval = pointInterval = pick(this.pointInterval, options.pointInterval, 1);
14878
14879 // Added code for pointInterval strings
14880 if (pointIntervalUnit) {
14881 date = new Date(xIncrement);
14882
14883 if (pointIntervalUnit === 'day') {
14884 date = +date[Date.hcSetDate](date[Date.hcGetDate]() + pointInterval);
14885 } else if (pointIntervalUnit === 'month') {
14886 date = +date[Date.hcSetMonth](date[Date.hcGetMonth]() + pointInterval);
14887 } else if (pointIntervalUnit === 'year') {
14888 date = +date[Date.hcSetFullYear](date[Date.hcGetFullYear]() + pointInterval);
14889 }
14890 pointInterval = date - xIncrement;
14891
14892 }
14893
14894 this.xIncrement = xIncrement + pointInterval;
14895 return xIncrement;
14896 },
14897
14898 /**
14899 * Set the series options by merging from the options tree
14900 * @param {Object} itemOptions
14901 */
14902 setOptions: function(itemOptions) {
14903 var chart = this.chart,
14904 chartOptions = chart.options,
14905 plotOptions = chartOptions.plotOptions,
14906 userOptions = chart.userOptions || {},
14907 userPlotOptions = userOptions.plotOptions || {},
14908 typeOptions = plotOptions[this.type],
14909 options,
14910 zones;
14911
14912 this.userOptions = itemOptions;
14913
14914 // General series options take precedence over type options because otherwise, default
14915 // type options like column.animation would be overwritten by the general option.
14916 // But issues have been raised here (#3881), and the solution may be to distinguish
14917 // between default option and userOptions like in the tooltip below.
14918 options = merge(
14919 typeOptions,
14920 plotOptions.series,
14921 itemOptions
14922 );
14923
14924 // The tooltip options are merged between global and series specific options
14925 this.tooltipOptions = merge(
14926 defaultOptions.tooltip,
14927 defaultOptions.plotOptions[this.type].tooltip,
14928 userOptions.tooltip,
14929 userPlotOptions.series && userPlotOptions.series.tooltip,
14930 userPlotOptions[this.type] && userPlotOptions[this.type].tooltip,
14931 itemOptions.tooltip
14932 );
14933
14934 // Delete marker object if not allowed (#1125)
14935 if (typeOptions.marker === null) {
14936 delete options.marker;
14937 }
14938
14939 // Handle color zones
14940 this.zoneAxis = options.zoneAxis;
14941 zones = this.zones = (options.zones || []).slice();
14942 if ((options.negativeColor || options.negativeFillColor) && !options.zones) {
14943 zones.push({
14944 value: options[this.zoneAxis + 'Threshold'] || options.threshold || 0,
14945 className: 'highcharts-negative',
14946
14947 color: options.negativeColor,
14948 fillColor: options.negativeFillColor
14949
14950 });
14951 }
14952 if (zones.length) { // Push one extra zone for the rest
14953 if (defined(zones[zones.length - 1].value)) {
14954 zones.push({
14955
14956 color: this.color,
14957 fillColor: this.fillColor
14958
14959 });
14960 }
14961 }
14962 return options;
14963 },
14964
14965 getCyclic: function(prop, value, defaults) {
14966 var i,
14967 userOptions = this.userOptions,
14968 indexName = prop + 'Index',
14969 counterName = prop + 'Counter',
14970 len = defaults ? defaults.length : pick(this.chart.options.chart[prop + 'Count'], this.chart[prop + 'Count']),
14971 setting;
14972
14973 if (!value) {
14974 // Pick up either the colorIndex option, or the _colorIndex after Series.update()
14975 setting = pick(userOptions[indexName], userOptions['_' + indexName]);
14976 if (defined(setting)) { // after Series.update()
14977 i = setting;
14978 } else {
14979 userOptions['_' + indexName] = i = this.chart[counterName] % len;
14980 this.chart[counterName] += 1;
14981 }
14982 if (defaults) {
14983 value = defaults[i];
14984 }
14985 }
14986 // Set the colorIndex
14987 if (i !== undefined) {
14988 this[indexName] = i;
14989 }
14990 this[prop] = value;
14991 },
14992
14993 /**
14994 * Get the series' color
14995 */
14996
14997 getColor: function() {
14998 if (this.options.colorByPoint) {
14999 this.options.color = null; // #4359, selected slice got series.color even when colorByPoint was set.
15000 } else {
15001 this.getCyclic('color', this.options.color || defaultPlotOptions[this.type].color, this.chart.options.colors);
15002 }
15003 },
15004
15005 /**
15006 * Get the series' symbol
15007 */
15008 getSymbol: function() {
15009 var seriesMarkerOption = this.options.marker;
15010
15011 this.getCyclic('symbol', seriesMarkerOption.symbol, this.chart.options.symbols);
15012
15013 // don't substract radius in image symbols (#604)
15014 if (/^url/.test(this.symbol)) {
15015 seriesMarkerOption.radius = 0;
15016 }
15017 },
15018
15019 drawLegendSymbol: LegendSymbolMixin.drawLineMarker,
15020
15021 /**
15022 * Replace the series data with a new set of data
15023 * @param {Object} data
15024 * @param {Object} redraw
15025 */
15026 setData: function(data, redraw, animation, updatePoints) {
15027 var series = this,
15028 oldData = series.points,
15029 oldDataLength = (oldData && oldData.length) || 0,
15030 dataLength,
15031 options = series.options,
15032 chart = series.chart,
15033 firstPoint = null,
15034 xAxis = series.xAxis,
15035 i,
15036 turboThreshold = options.turboThreshold,
15037 pt,
15038 xData = this.xData,
15039 yData = this.yData,
15040 pointArrayMap = series.pointArrayMap,
15041 valueCount = pointArrayMap && pointArrayMap.length;
15042
15043 data = data || [];
15044 dataLength = data.length;
15045 redraw = pick(redraw, true);
15046
15047 // If the point count is the same as is was, just run Point.update which is
15048 // cheaper, allows animation, and keeps references to points.
15049 if (updatePoints !== false && dataLength && oldDataLength === dataLength && !series.cropped && !series.hasGroupedData && series.visible) {
15050 each(data, function(point, i) {
15051 // .update doesn't exist on a linked, hidden series (#3709)
15052 if (oldData[i].update && point !== options.data[i]) {
15053 oldData[i].update(point, false, null, false);
15054 }
15055 });
15056
15057 } else {
15058
15059 // Reset properties
15060 series.xIncrement = null;
15061
15062 series.colorCounter = 0; // for series with colorByPoint (#1547)
15063
15064 // Update parallel arrays
15065 each(this.parallelArrays, function(key) {
15066 series[key + 'Data'].length = 0;
15067 });
15068
15069 // In turbo mode, only one- or twodimensional arrays of numbers are allowed. The
15070 // first value is tested, and we assume that all the rest are defined the same
15071 // way. Although the 'for' loops are similar, they are repeated inside each
15072 // if-else conditional for max performance.
15073 if (turboThreshold && dataLength > turboThreshold) {
15074
15075 // find the first non-null point
15076 i = 0;
15077 while (firstPoint === null && i < dataLength) {
15078 firstPoint = data[i];
15079 i++;
15080 }
15081
15082
15083 if (isNumber(firstPoint)) { // assume all points are numbers
15084 for (i = 0; i < dataLength; i++) {
15085 xData[i] = this.autoIncrement();
15086 yData[i] = data[i];
15087 }
15088 } else if (isArray(firstPoint)) { // assume all points are arrays
15089 if (valueCount) { // [x, low, high] or [x, o, h, l, c]
15090 for (i = 0; i < dataLength; i++) {
15091 pt = data[i];
15092 xData[i] = pt[0];
15093 yData[i] = pt.slice(1, valueCount + 1);
15094 }
15095 } else { // [x, y]
15096 for (i = 0; i < dataLength; i++) {
15097 pt = data[i];
15098 xData[i] = pt[0];
15099 yData[i] = pt[1];
15100 }
15101 }
15102 } else {
15103 error(12); // Highcharts expects configs to be numbers or arrays in turbo mode
15104 }
15105 } else {
15106 for (i = 0; i < dataLength; i++) {
15107 if (data[i] !== undefined) { // stray commas in oldIE
15108 pt = {
15109 series: series
15110 };
15111 series.pointClass.prototype.applyOptions.apply(pt, [data[i]]);
15112 series.updateParallelArrays(pt, i);
15113 }
15114 }
15115 }
15116
15117 // Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON
15118 if (isString(yData[0])) {
15119 error(14, true);
15120 }
15121
15122 series.data = [];
15123 series.options.data = series.userOptions.data = data;
15124
15125 // destroy old points
15126 i = oldDataLength;
15127 while (i--) {
15128 if (oldData[i] && oldData[i].destroy) {
15129 oldData[i].destroy();
15130 }
15131 }
15132
15133 // reset minRange (#878)
15134 if (xAxis) {
15135 xAxis.minRange = xAxis.userMinRange;
15136 }
15137
15138 // redraw
15139 series.isDirty = chart.isDirtyBox = true;
15140 series.isDirtyData = !!oldData;
15141 animation = false;
15142 }
15143
15144 // Typically for pie series, points need to be processed and generated
15145 // prior to rendering the legend
15146 if (options.legendType === 'point') {
15147 this.processData();
15148 this.generatePoints();
15149 }
15150
15151 if (redraw) {
15152 chart.redraw(animation);
15153 }
15154 },
15155
15156 /**
15157 * Process the data by cropping away unused data points if the series is longer
15158 * than the crop threshold. This saves computing time for lage series.
15159 */
15160 processData: function(force) {
15161 var series = this,
15162 processedXData = series.xData, // copied during slice operation below
15163 processedYData = series.yData,
15164 dataLength = processedXData.length,
15165 croppedData,
15166 cropStart = 0,
15167 cropped,
15168 distance,
15169 closestPointRange,
15170 xAxis = series.xAxis,
15171 i, // loop variable
15172 options = series.options,
15173 cropThreshold = options.cropThreshold,
15174 getExtremesFromAll = series.getExtremesFromAll || options.getExtremesFromAll, // #4599
15175 isCartesian = series.isCartesian,
15176 xExtremes,
15177 val2lin = xAxis && xAxis.val2lin,
15178 isLog = xAxis && xAxis.isLog,
15179 min,
15180 max;
15181
15182 // If the series data or axes haven't changed, don't go through this. Return false to pass
15183 // the message on to override methods like in data grouping.
15184 if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) {
15185 return false;
15186 }
15187
15188 if (xAxis) {
15189 xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053)
15190 min = xExtremes.min;
15191 max = xExtremes.max;
15192 }
15193
15194 // optionally filter out points outside the plot area
15195 if (isCartesian && series.sorted && !getExtremesFromAll && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) {
15196
15197 // it's outside current extremes
15198 if (processedXData[dataLength - 1] < min || processedXData[0] > max) {
15199 processedXData = [];
15200 processedYData = [];
15201
15202 // only crop if it's actually spilling out
15203 } else if (processedXData[0] < min || processedXData[dataLength - 1] > max) {
15204 croppedData = this.cropData(series.xData, series.yData, min, max);
15205 processedXData = croppedData.xData;
15206 processedYData = croppedData.yData;
15207 cropStart = croppedData.start;
15208 cropped = true;
15209 }
15210 }
15211
15212
15213 // Find the closest distance between processed points
15214 i = processedXData.length || 1;
15215 while (--i) {
15216 distance = isLog ?
15217 val2lin(processedXData[i]) - val2lin(processedXData[i - 1]) :
15218 processedXData[i] - processedXData[i - 1];
15219
15220 if (distance > 0 && (closestPointRange === undefined || distance < closestPointRange)) {
15221 closestPointRange = distance;
15222
15223 // Unsorted data is not supported by the line tooltip, as well as data grouping and
15224 // navigation in Stock charts (#725) and width calculation of columns (#1900)
15225 } else if (distance < 0 && series.requireSorting) {
15226 error(15);
15227 }
15228 }
15229
15230 // Record the properties
15231 series.cropped = cropped; // undefined or true
15232 series.cropStart = cropStart;
15233 series.processedXData = processedXData;
15234 series.processedYData = processedYData;
15235
15236 series.closestPointRange = closestPointRange;
15237
15238 },
15239
15240 /**
15241 * Iterate over xData and crop values between min and max. Returns object containing crop start/end
15242 * cropped xData with corresponding part of yData, dataMin and dataMax within the cropped range
15243 */
15244 cropData: function(xData, yData, min, max) {
15245 var dataLength = xData.length,
15246 cropStart = 0,
15247 cropEnd = dataLength,
15248 cropShoulder = pick(this.cropShoulder, 1), // line-type series need one point outside
15249 i,
15250 j;
15251
15252 // iterate up to find slice start
15253 for (i = 0; i < dataLength; i++) {
15254 if (xData[i] >= min) {
15255 cropStart = Math.max(0, i - cropShoulder);
15256 break;
15257 }
15258 }
15259
15260 // proceed to find slice end
15261 for (j = i; j < dataLength; j++) {
15262 if (xData[j] > max) {
15263 cropEnd = j + cropShoulder;
15264 break;
15265 }
15266 }
15267
15268 return {
15269 xData: xData.slice(cropStart, cropEnd),
15270 yData: yData.slice(cropStart, cropEnd),
15271 start: cropStart,
15272 end: cropEnd
15273 };
15274 },
15275
15276
15277 /**
15278 * Generate the data point after the data has been processed by cropping away
15279 * unused points and optionally grouped in Highcharts Stock.
15280 */
15281 generatePoints: function() {
15282 var series = this,
15283 options = series.options,
15284 dataOptions = options.data,
15285 data = series.data,
15286 dataLength,
15287 processedXData = series.processedXData,
15288 processedYData = series.processedYData,
15289 PointClass = series.pointClass,
15290 processedDataLength = processedXData.length,
15291 cropStart = series.cropStart || 0,
15292 cursor,
15293 hasGroupedData = series.hasGroupedData,
15294 point,
15295 points = [],
15296 i;
15297
15298 if (!data && !hasGroupedData) {
15299 var arr = [];
15300 arr.length = dataOptions.length;
15301 data = series.data = arr;
15302 }
15303
15304 for (i = 0; i < processedDataLength; i++) {
15305 cursor = cropStart + i;
15306 if (!hasGroupedData) {
15307 if (data[cursor]) {
15308 point = data[cursor];
15309 } else if (dataOptions[cursor] !== undefined) { // #970
15310 data[cursor] = point = (new PointClass()).init(series, dataOptions[cursor], processedXData[i]);
15311 }
15312 points[i] = point;
15313 } else {
15314 // splat the y data in case of ohlc data array
15315 points[i] = (new PointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i])));
15316 points[i].dataGroup = series.groupMap[i];
15317 }
15318 points[i].index = cursor; // For faster access in Point.update
15319 }
15320
15321 // Hide cropped-away points - this only runs when the number of points is above cropThreshold, or when
15322 // swithching view from non-grouped data to grouped data (#637)
15323 if (data && (processedDataLength !== (dataLength = data.length) || hasGroupedData)) {
15324 for (i = 0; i < dataLength; i++) {
15325 if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points
15326 i += processedDataLength;
15327 }
15328 if (data[i]) {
15329 data[i].destroyElements();
15330 data[i].plotX = undefined; // #1003
15331 }
15332 }
15333 }
15334
15335 series.data = data;
15336 series.points = points;
15337 },
15338
15339 /**
15340 * Calculate Y extremes for visible data
15341 */
15342 getExtremes: function(yData) {
15343 var xAxis = this.xAxis,
15344 yAxis = this.yAxis,
15345 xData = this.processedXData,
15346 yDataLength,
15347 activeYData = [],
15348 activeCounter = 0,
15349 xExtremes = xAxis.getExtremes(), // #2117, need to compensate for log X axis
15350 xMin = xExtremes.min,
15351 xMax = xExtremes.max,
15352 validValue,
15353 withinRange,
15354 x,
15355 y,
15356 i,
15357 j;
15358
15359 yData = yData || this.stackedYData || this.processedYData || [];
15360 yDataLength = yData.length;
15361
15362 for (i = 0; i < yDataLength; i++) {
15363
15364 x = xData[i];
15365 y = yData[i];
15366
15367 // For points within the visible range, including the first point outside the
15368 // visible range, consider y extremes
15369 validValue = (isNumber(y, true) || isArray(y)) && (!yAxis.isLog || (y.length || y > 0));
15370 withinRange = this.getExtremesFromAll || this.options.getExtremesFromAll || this.cropped ||
15371 ((xData[i + 1] || x) >= xMin && (xData[i - 1] || x) <= xMax);
15372
15373 if (validValue && withinRange) {
15374
15375 j = y.length;
15376 if (j) { // array, like ohlc or range data
15377 while (j--) {
15378 if (y[j] !== null) {
15379 activeYData[activeCounter++] = y[j];
15380 }
15381 }
15382 } else {
15383 activeYData[activeCounter++] = y;
15384 }
15385 }
15386 }
15387 this.dataMin = arrayMin(activeYData);
15388 this.dataMax = arrayMax(activeYData);
15389 },
15390
15391 /**
15392 * Translate data points from raw data values to chart specific positioning data
15393 * needed later in drawPoints, drawGraph and drawTracker.
15394 */
15395 translate: function() {
15396 if (!this.processedXData) { // hidden series
15397 this.processData();
15398 }
15399 this.generatePoints();
15400 var series = this,
15401 options = series.options,
15402 stacking = options.stacking,
15403 xAxis = series.xAxis,
15404 categories = xAxis.categories,
15405 yAxis = series.yAxis,
15406 points = series.points,
15407 dataLength = points.length,
15408 hasModifyValue = !!series.modifyValue,
15409 i,
15410 pointPlacement = options.pointPlacement,
15411 dynamicallyPlaced = pointPlacement === 'between' || isNumber(pointPlacement),
15412 threshold = options.threshold,
15413 stackThreshold = options.startFromThreshold ? threshold : 0,
15414 plotX,
15415 plotY,
15416 lastPlotX,
15417 stackIndicator,
15418 closestPointRangePx = Number.MAX_VALUE;
15419
15420 // Translate each point
15421 for (i = 0; i < dataLength; i++) {
15422 var point = points[i],
15423 xValue = point.x,
15424 yValue = point.y,
15425 yBottom = point.low,
15426 stack = stacking && yAxis.stacks[(series.negStacks && yValue < (stackThreshold ? 0 : threshold) ? '-' : '') + series.stackKey],
15427 pointStack,
15428 stackValues;
15429
15430 // Discard disallowed y values for log axes (#3434)
15431 if (yAxis.isLog && yValue !== null && yValue <= 0) {
15432 point.isNull = true;
15433 }
15434
15435 // Get the plotX translation
15436 point.plotX = plotX = correctFloat( // #5236
15437 Math.min(Math.max(-1e5, xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement, this.type === 'flags')), 1e5) // #3923
15438 );
15439
15440 // Calculate the bottom y value for stacked series
15441 if (stacking && series.visible && !point.isNull && stack && stack[xValue]) {
15442 stackIndicator = series.getStackIndicator(stackIndicator, xValue, series.index);
15443 pointStack = stack[xValue];
15444 stackValues = pointStack.points[stackIndicator.key];
15445 yBottom = stackValues[0];
15446 yValue = stackValues[1];
15447
15448 if (yBottom === stackThreshold && stackIndicator.key === stack[xValue].base) {
15449 yBottom = pick(threshold, yAxis.min);
15450 }
15451 if (yAxis.isLog && yBottom <= 0) { // #1200, #1232
15452 yBottom = null;
15453 }
15454
15455 point.total = point.stackTotal = pointStack.total;
15456 point.percentage = pointStack.total && (point.y / pointStack.total * 100);
15457 point.stackY = yValue;
15458
15459 // Place the stack label
15460 pointStack.setOffset(series.pointXOffset || 0, series.barW || 0);
15461
15462 }
15463
15464 // Set translated yBottom or remove it
15465 point.yBottom = defined(yBottom) ?
15466 yAxis.translate(yBottom, 0, 1, 0, 1) :
15467 null;
15468
15469 // general hook, used for Highstock compare mode
15470 if (hasModifyValue) {
15471 yValue = series.modifyValue(yValue, point);
15472 }
15473
15474 // Set the the plotY value, reset it for redraws
15475 point.plotY = plotY = (typeof yValue === 'number' && yValue !== Infinity) ?
15476 Math.min(Math.max(-1e5, yAxis.translate(yValue, 0, 1, 0, 1)), 1e5) : // #3201
15477 undefined;
15478 point.isInside = plotY !== undefined && plotY >= 0 && plotY <= yAxis.len && // #3519
15479 plotX >= 0 && plotX <= xAxis.len;
15480
15481
15482 // Set client related positions for mouse tracking
15483 point.clientX = dynamicallyPlaced ? correctFloat(xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement)) : plotX; // #1514, #5383, #5518
15484
15485 point.negative = point.y < (threshold || 0);
15486
15487 // some API data
15488 point.category = categories && categories[point.x] !== undefined ?
15489 categories[point.x] : point.x;
15490
15491 // Determine auto enabling of markers (#3635, #5099)
15492 if (!point.isNull) {
15493 if (lastPlotX !== undefined) {
15494 closestPointRangePx = Math.min(closestPointRangePx, Math.abs(plotX - lastPlotX));
15495 }
15496 lastPlotX = plotX;
15497 }
15498
15499 }
15500 series.closestPointRangePx = closestPointRangePx;
15501 },
15502
15503 /**
15504 * Return the series points with null points filtered out
15505 */
15506 getValidPoints: function(points, insideOnly) {
15507 var chart = this.chart;
15508 return grep(points || this.points || [], function isValidPoint(point) { // #3916, #5029
15509 if (insideOnly && !chart.isInsidePlot(point.plotX, point.plotY, chart.inverted)) { // #5085
15510 return false;
15511 }
15512 return !point.isNull;
15513 });
15514 },
15515
15516 /**
15517 * Set the clipping for the series. For animated series it is called twice, first to initiate
15518 * animating the clip then the second time without the animation to set the final clip.
15519 */
15520 setClip: function(animation) {
15521 var chart = this.chart,
15522 options = this.options,
15523 renderer = chart.renderer,
15524 inverted = chart.inverted,
15525 seriesClipBox = this.clipBox,
15526 clipBox = seriesClipBox || chart.clipBox,
15527 sharedClipKey = this.sharedClipKey || ['_sharedClip', animation && animation.duration, animation && animation.easing, clipBox.height, options.xAxis, options.yAxis].join(','), // #4526
15528 clipRect = chart[sharedClipKey],
15529 markerClipRect = chart[sharedClipKey + 'm'];
15530
15531 // If a clipping rectangle with the same properties is currently present in the chart, use that.
15532 if (!clipRect) {
15533
15534 // When animation is set, prepare the initial positions
15535 if (animation) {
15536 clipBox.width = 0;
15537
15538 chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect(-99, // include the width of the first marker
15539 inverted ? -chart.plotLeft : -chart.plotTop,
15540 99,
15541 inverted ? chart.chartWidth : chart.chartHeight
15542 );
15543 }
15544 chart[sharedClipKey] = clipRect = renderer.clipRect(clipBox);
15545 // Create hashmap for series indexes
15546 clipRect.count = {
15547 length: 0
15548 };
15549
15550 }
15551 if (animation) {
15552 if (!clipRect.count[this.index]) {
15553 clipRect.count[this.index] = true;
15554 clipRect.count.length += 1;
15555 }
15556 }
15557
15558 if (options.clip !== false) {
15559 this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect);
15560 this.markerGroup.clip(markerClipRect);
15561 this.sharedClipKey = sharedClipKey;
15562 }
15563
15564 // Remove the shared clipping rectangle when all series are shown
15565 if (!animation) {
15566 if (clipRect.count[this.index]) {
15567 delete clipRect.count[this.index];
15568 clipRect.count.length -= 1;
15569 }
15570
15571 if (clipRect.count.length === 0 && sharedClipKey && chart[sharedClipKey]) {
15572 if (!seriesClipBox) {
15573 chart[sharedClipKey] = chart[sharedClipKey].destroy();
15574 }
15575 if (chart[sharedClipKey + 'm']) {
15576 chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy();
15577 }
15578 }
15579 }
15580 },
15581
15582 /**
15583 * Animate in the series
15584 */
15585 animate: function(init) {
15586 var series = this,
15587 chart = series.chart,
15588 clipRect,
15589 animation = animObject(series.options.animation),
15590 sharedClipKey;
15591
15592 // Initialize the animation. Set up the clipping rectangle.
15593 if (init) {
15594
15595 series.setClip(animation);
15596
15597 // Run the animation
15598 } else {
15599 sharedClipKey = this.sharedClipKey;
15600 clipRect = chart[sharedClipKey];
15601 if (clipRect) {
15602 clipRect.animate({
15603 width: chart.plotSizeX
15604 }, animation);
15605 }
15606 if (chart[sharedClipKey + 'm']) {
15607 chart[sharedClipKey + 'm'].animate({
15608 width: chart.plotSizeX + 99
15609 }, animation);
15610 }
15611
15612 // Delete this function to allow it only once
15613 series.animate = null;
15614
15615 }
15616 },
15617
15618 /**
15619 * This runs after animation to land on the final plot clipping
15620 */
15621 afterAnimate: function() {
15622 this.setClip();
15623 fireEvent(this, 'afterAnimate');
15624 },
15625
15626 /**
15627 * Draw the markers
15628 */
15629 drawPoints: function() {
15630 var series = this,
15631 points = series.points,
15632 chart = series.chart,
15633 plotX,
15634 plotY,
15635 i,
15636 point,
15637 radius,
15638 symbol,
15639 isImage,
15640 graphic,
15641 options = series.options,
15642 seriesMarkerOptions = options.marker,
15643 pointMarkerOptions,
15644 hasPointMarker,
15645 enabled,
15646 isInside,
15647 markerGroup = series.markerGroup,
15648 xAxis = series.xAxis,
15649 globallyEnabled = pick(
15650 seriesMarkerOptions.enabled,
15651 xAxis.isRadial ? true : null,
15652 series.closestPointRangePx > 2 * seriesMarkerOptions.radius
15653 );
15654
15655 if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) {
15656
15657 i = points.length;
15658 while (i--) {
15659 point = points[i];
15660 plotX = Math.floor(point.plotX); // #1843
15661 plotY = point.plotY;
15662 graphic = point.graphic;
15663 pointMarkerOptions = point.marker || {};
15664 hasPointMarker = !!point.marker;
15665 enabled = (globallyEnabled && pointMarkerOptions.enabled === undefined) || pointMarkerOptions.enabled;
15666 isInside = point.isInside;
15667
15668 // only draw the point if y is defined
15669 if (enabled && isNumber(plotY) && point.y !== null) {
15670
15671 // Shortcuts
15672 radius = seriesMarkerOptions.radius;
15673 symbol = pick(pointMarkerOptions.symbol, series.symbol);
15674 isImage = symbol.indexOf('url') === 0;
15675
15676 if (graphic) { // update
15677 graphic[isInside ? 'show' : 'hide'](true) // Since the marker group isn't clipped, each individual marker must be toggled
15678 //.attr(pointAttr) // #4759
15679 .animate(extend({
15680 x: plotX - radius,
15681 y: plotY - radius
15682 }, graphic.symbolName ? { // don't apply to image symbols #507
15683 width: 2 * radius,
15684 height: 2 * radius
15685 } : {}));
15686 } else if (isInside && (radius > 0 || isImage)) {
15687 point.graphic = graphic = chart.renderer.symbol(
15688 symbol,
15689 plotX - radius,
15690 plotY - radius,
15691 2 * radius,
15692 2 * radius,
15693 hasPointMarker ? pointMarkerOptions : seriesMarkerOptions
15694 )
15695 .attr({
15696 r: radius
15697 })
15698 .add(markerGroup);
15699 }
15700
15701
15702 // Presentational attributes
15703 if (graphic) {
15704 graphic.attr(series.pointAttribs(point, point.selected && 'select'));
15705 }
15706
15707
15708 if (graphic) {
15709 graphic.addClass(point.getClassName(), true);
15710 }
15711
15712 } else if (graphic) {
15713 point.graphic = graphic.destroy(); // #1269
15714 }
15715 }
15716 }
15717
15718 },
15719
15720
15721 /**
15722 * Get presentational attributes for marker-based series (line, spline, scatter, bubble, mappoint...)
15723 */
15724 pointAttribs: function(point, state) {
15725 var seriesMarkerOptions = this.options.marker,
15726 seriesStateOptions,
15727 pointOptions = point && point.options,
15728 pointMarkerOptions = (pointOptions && pointOptions.marker) || {},
15729 pointStateOptions,
15730 strokeWidth = seriesMarkerOptions.lineWidth,
15731 color = this.color,
15732 pointColorOption = pointOptions && pointOptions.color,
15733 pointColor = point && point.color,
15734 zoneColor,
15735 fill,
15736 stroke,
15737 zone;
15738
15739 if (point && this.zones.length) {
15740 zone = point.getZone();
15741 if (zone && zone.color) {
15742 zoneColor = zone.color;
15743 }
15744 }
15745
15746 color = pointColorOption || zoneColor || pointColor || color;
15747 fill = pointMarkerOptions.fillColor || seriesMarkerOptions.fillColor || color;
15748 stroke = pointMarkerOptions.lineColor || seriesMarkerOptions.lineColor || color;
15749
15750 // Handle hover and select states
15751 if (state) {
15752 seriesStateOptions = seriesMarkerOptions.states[state];
15753 pointStateOptions = (pointMarkerOptions.states && pointMarkerOptions.states[state]) || {};
15754 strokeWidth = seriesStateOptions.lineWidth || strokeWidth + seriesStateOptions.lineWidthPlus;
15755 fill = pointStateOptions.fillColor || seriesStateOptions.fillColor || fill;
15756 stroke = pointStateOptions.lineColor || seriesStateOptions.lineColor || stroke;
15757 }
15758
15759 return {
15760 'stroke': stroke,
15761 'stroke-width': strokeWidth,
15762 'fill': fill
15763 };
15764 },
15765
15766 /**
15767 * Clear DOM objects and free up memory
15768 */
15769 destroy: function() {
15770 var series = this,
15771 chart = series.chart,
15772 issue134 = /AppleWebKit\/533/.test(win.navigator.userAgent),
15773 destroy,
15774 i,
15775 data = series.data || [],
15776 point,
15777 prop,
15778 axis;
15779
15780 // add event hook
15781 fireEvent(series, 'destroy');
15782
15783 // remove all events
15784 removeEvent(series);
15785
15786 // erase from axes
15787 each(series.axisTypes || [], function(AXIS) {
15788 axis = series[AXIS];
15789 if (axis && axis.series) {
15790 erase(axis.series, series);
15791 axis.isDirty = axis.forceRedraw = true;
15792 }
15793 });
15794
15795 // remove legend items
15796 if (series.legendItem) {
15797 series.chart.legend.destroyItem(series);
15798 }
15799
15800 // destroy all points with their elements
15801 i = data.length;
15802 while (i--) {
15803 point = data[i];
15804 if (point && point.destroy) {
15805 point.destroy();
15806 }
15807 }
15808 series.points = null;
15809
15810 // Clear the animation timeout if we are destroying the series during initial animation
15811 clearTimeout(series.animationTimeout);
15812
15813 // Destroy all SVGElements associated to the series
15814 for (prop in series) {
15815 if (series[prop] instanceof SVGElement && !series[prop].survive) { // Survive provides a hook for not destroying
15816
15817 // issue 134 workaround
15818 destroy = issue134 && prop === 'group' ?
15819 'hide' :
15820 'destroy';
15821
15822 series[prop][destroy]();
15823 }
15824 }
15825
15826 // remove from hoverSeries
15827 if (chart.hoverSeries === series) {
15828 chart.hoverSeries = null;
15829 }
15830 erase(chart.series, series);
15831
15832 // clear all members
15833 for (prop in series) {
15834 delete series[prop];
15835 }
15836 },
15837
15838 /**
15839 * Get the graph path
15840 */
15841 getGraphPath: function(points, nullsAsZeroes, connectCliffs) {
15842 var series = this,
15843 options = series.options,
15844 step = options.step,
15845 reversed,
15846 graphPath = [],
15847 xMap = [],
15848 gap;
15849
15850 points = points || series.points;
15851
15852 // Bottom of a stack is reversed
15853 reversed = points.reversed;
15854 if (reversed) {
15855 points.reverse();
15856 }
15857 // Reverse the steps (#5004)
15858 step = {
15859 right: 1,
15860 center: 2
15861 }[step] || (step && 3);
15862 if (step && reversed) {
15863 step = 4 - step;
15864 }
15865
15866 // Remove invalid points, especially in spline (#5015)
15867 if (options.connectNulls && !nullsAsZeroes && !connectCliffs) {
15868 points = this.getValidPoints(points);
15869 }
15870
15871 // Build the line
15872 each(points, function(point, i) {
15873
15874 var plotX = point.plotX,
15875 plotY = point.plotY,
15876 lastPoint = points[i - 1],
15877 pathToPoint; // the path to this point from the previous
15878
15879 if ((point.leftCliff || (lastPoint && lastPoint.rightCliff)) && !connectCliffs) {
15880 gap = true; // ... and continue
15881 }
15882
15883 // Line series, nullsAsZeroes is not handled
15884 if (point.isNull && !defined(nullsAsZeroes) && i > 0) {
15885 gap = !options.connectNulls;
15886
15887 // Area series, nullsAsZeroes is set
15888 } else if (point.isNull && !nullsAsZeroes) {
15889 gap = true;
15890
15891 } else {
15892
15893 if (i === 0 || gap) {
15894 pathToPoint = ['M', point.plotX, point.plotY];
15895
15896 } else if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object
15897
15898 pathToPoint = series.getPointSpline(points, point, i);
15899
15900 } else if (step) {
15901
15902 if (step === 1) { // right
15903 pathToPoint = [
15904 'L',
15905 lastPoint.plotX,
15906 plotY
15907 ];
15908
15909 } else if (step === 2) { // center
15910 pathToPoint = [
15911 'L',
15912 (lastPoint.plotX + plotX) / 2,
15913 lastPoint.plotY,
15914 'L',
15915 (lastPoint.plotX + plotX) / 2,
15916 plotY
15917 ];
15918
15919 } else {
15920 pathToPoint = [
15921 'L',
15922 plotX,
15923 lastPoint.plotY
15924 ];
15925 }
15926 pathToPoint.push('L', plotX, plotY);
15927
15928 } else {
15929 // normal line to next point
15930 pathToPoint = [
15931 'L',
15932 plotX,
15933 plotY
15934 ];
15935 }
15936
15937 // Prepare for animation. When step is enabled, there are two path nodes for each x value.
15938 xMap.push(point.x);
15939 if (step) {
15940 xMap.push(point.x);
15941 }
15942
15943 graphPath.push.apply(graphPath, pathToPoint);
15944 gap = false;
15945 }
15946 });
15947
15948 graphPath.xMap = xMap;
15949 series.graphPath = graphPath;
15950
15951 return graphPath;
15952
15953 },
15954
15955 /**
15956 * Draw the actual graph
15957 */
15958 drawGraph: function() {
15959 var series = this,
15960 options = this.options,
15961 graphPath = (this.gappedPath || this.getGraphPath).call(this),
15962 props = [
15963 [
15964 'graph',
15965 'highcharts-graph',
15966
15967 options.lineColor || this.color,
15968 options.dashStyle
15969
15970 ]
15971 ];
15972
15973 // Add the zone properties if any
15974 each(this.zones, function(zone, i) {
15975 props.push([
15976 'zone-graph-' + i,
15977 'highcharts-graph highcharts-zone-graph-' + i + ' ' + (zone.className || ''),
15978
15979 zone.color || series.color,
15980 zone.dashStyle || options.dashStyle
15981
15982 ]);
15983 });
15984
15985 // Draw the graph
15986 each(props, function(prop, i) {
15987 var graphKey = prop[0],
15988 graph = series[graphKey],
15989 attribs;
15990
15991 if (graph) {
15992 graph.endX = graphPath.xMap;
15993 graph.animate({
15994 d: graphPath
15995 });
15996
15997 } else if (graphPath.length) { // #1487
15998
15999 series[graphKey] = series.chart.renderer.path(graphPath)
16000 .addClass(prop[1])
16001 .attr({
16002 zIndex: 1
16003 }) // #1069
16004 .add(series.group);
16005
16006
16007 attribs = {
16008 'stroke': prop[2],
16009 'stroke-width': options.lineWidth,
16010 'fill': (series.fillGraph && series.color) || 'none' // Polygon series use filled graph
16011 };
16012
16013 if (prop[3]) {
16014 attribs.dashstyle = prop[3];
16015 } else if (options.linecap !== 'square') {
16016 attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round';
16017 }
16018
16019 graph = series[graphKey]
16020 .attr(attribs)
16021 .shadow((i < 2) && options.shadow); // add shadow to normal series (0) or to first zone (1) #3932
16022
16023 }
16024
16025 // Helpers for animation
16026 if (graph) {
16027 graph.startX = graphPath.xMap;
16028 //graph.shiftUnit = options.step ? 2 : 1;
16029 graph.isArea = graphPath.isArea; // For arearange animation
16030 }
16031 });
16032 },
16033
16034 /**
16035 * Clip the graphs into the positive and negative coloured graphs
16036 */
16037 applyZones: function() {
16038 var series = this,
16039 chart = this.chart,
16040 renderer = chart.renderer,
16041 zones = this.zones,
16042 translatedFrom,
16043 translatedTo,
16044 clips = this.clips || [],
16045 clipAttr,
16046 graph = this.graph,
16047 area = this.area,
16048 chartSizeMax = Math.max(chart.chartWidth, chart.chartHeight),
16049 axis = this[(this.zoneAxis || 'y') + 'Axis'],
16050 extremes,
16051 reversed,
16052 inverted = chart.inverted,
16053 horiz,
16054 pxRange,
16055 pxPosMin,
16056 pxPosMax,
16057 ignoreZones = false;
16058
16059 if (zones.length && (graph || area) && axis && axis.min !== undefined) {
16060 reversed = axis.reversed;
16061 horiz = axis.horiz;
16062 // The use of the Color Threshold assumes there are no gaps
16063 // so it is safe to hide the original graph and area
16064 if (graph) {
16065 graph.hide();
16066 }
16067 if (area) {
16068 area.hide();
16069 }
16070
16071 // Create the clips
16072 extremes = axis.getExtremes();
16073 each(zones, function(threshold, i) {
16074
16075 translatedFrom = reversed ?
16076 (horiz ? chart.plotWidth : 0) :
16077 (horiz ? 0 : axis.toPixels(extremes.min));
16078 translatedFrom = Math.min(Math.max(pick(translatedTo, translatedFrom), 0), chartSizeMax);
16079 translatedTo = Math.min(Math.max(Math.round(axis.toPixels(pick(threshold.value, extremes.max), true)), 0), chartSizeMax);
16080
16081 if (ignoreZones) {
16082 translatedFrom = translatedTo = axis.toPixels(extremes.max);
16083 }
16084
16085 pxRange = Math.abs(translatedFrom - translatedTo);
16086 pxPosMin = Math.min(translatedFrom, translatedTo);
16087 pxPosMax = Math.max(translatedFrom, translatedTo);
16088 if (axis.isXAxis) {
16089 clipAttr = {
16090 x: inverted ? pxPosMax : pxPosMin,
16091 y: 0,
16092 width: pxRange,
16093 height: chartSizeMax
16094 };
16095 if (!horiz) {
16096 clipAttr.x = chart.plotHeight - clipAttr.x;
16097 }
16098 } else {
16099 clipAttr = {
16100 x: 0,
16101 y: inverted ? pxPosMax : pxPosMin,
16102 width: chartSizeMax,
16103 height: pxRange
16104 };
16105 if (horiz) {
16106 clipAttr.y = chart.plotWidth - clipAttr.y;
16107 }
16108 }
16109
16110
16111 /// VML SUPPPORT
16112 if (inverted && renderer.isVML) {
16113 if (axis.isXAxis) {
16114 clipAttr = {
16115 x: 0,
16116 y: reversed ? pxPosMin : pxPosMax,
16117 height: clipAttr.width,
16118 width: chart.chartWidth
16119 };
16120 } else {
16121 clipAttr = {
16122 x: clipAttr.y - chart.plotLeft - chart.spacingBox.x,
16123 y: 0,
16124 width: clipAttr.height,
16125 height: chart.chartHeight
16126 };
16127 }
16128 }
16129 /// END OF VML SUPPORT
16130
16131
16132 if (clips[i]) {
16133 clips[i].animate(clipAttr);
16134 } else {
16135 clips[i] = renderer.clipRect(clipAttr);
16136
16137 if (graph) {
16138 series['zone-graph-' + i].clip(clips[i]);
16139 }
16140
16141 if (area) {
16142 series['zone-area-' + i].clip(clips[i]);
16143 }
16144 }
16145 // if this zone extends out of the axis, ignore the others
16146 ignoreZones = threshold.value > extremes.max;
16147 });
16148 this.clips = clips;
16149 }
16150 },
16151
16152 /**
16153 * Initialize and perform group inversion on series.group and series.markerGroup
16154 */
16155 invertGroups: function(inverted) {
16156 var series = this,
16157 chart = series.chart;
16158
16159 // Pie, go away (#1736)
16160 if (!series.xAxis) {
16161 return;
16162 }
16163
16164 // A fixed size is needed for inversion to work
16165 function setInvert() {
16166 var size = {
16167 width: series.yAxis.len,
16168 height: series.xAxis.len
16169 };
16170
16171 each(['group', 'markerGroup'], function(groupName) {
16172 if (series[groupName]) {
16173 series[groupName].attr(size).invert(inverted);
16174 }
16175 });
16176 }
16177
16178 addEvent(chart, 'resize', setInvert); // do it on resize
16179 addEvent(series, 'destroy', function() {
16180 removeEvent(chart, 'resize', setInvert);
16181 });
16182
16183 // Do it now
16184 setInvert(inverted); // do it now
16185
16186 // On subsequent render and redraw, just do setInvert without setting up events again
16187 series.invertGroups = setInvert;
16188 },
16189
16190 /**
16191 * General abstraction for creating plot groups like series.group, series.dataLabelsGroup and
16192 * series.markerGroup. On subsequent calls, the group will only be adjusted to the updated plot size.
16193 */
16194 plotGroup: function(prop, name, visibility, zIndex, parent) {
16195 var group = this[prop],
16196 isNew = !group;
16197
16198 // Generate it on first call
16199 if (isNew) {
16200 this[prop] = group = this.chart.renderer.g(name)
16201 .attr({
16202 zIndex: zIndex || 0.1 // IE8 and pointer logic use this
16203 })
16204 .add(parent);
16205
16206 group.addClass('highcharts-series-' + this.index + ' highcharts-' + this.type + '-series highcharts-color-' + this.colorIndex +
16207 ' ' + (this.options.className || ''));
16208 }
16209
16210 // Place it on first and subsequent (redraw) calls
16211 group.attr({
16212 visibility: visibility
16213 })[isNew ? 'attr' : 'animate'](this.getPlotBox());
16214 return group;
16215 },
16216
16217 /**
16218 * Get the translation and scale for the plot area of this series
16219 */
16220 getPlotBox: function() {
16221 var chart = this.chart,
16222 xAxis = this.xAxis,
16223 yAxis = this.yAxis;
16224
16225 // Swap axes for inverted (#2339)
16226 if (chart.inverted) {
16227 xAxis = yAxis;
16228 yAxis = this.xAxis;
16229 }
16230 return {
16231 translateX: xAxis ? xAxis.left : chart.plotLeft,
16232 translateY: yAxis ? yAxis.top : chart.plotTop,
16233 scaleX: 1, // #1623
16234 scaleY: 1
16235 };
16236 },
16237
16238 /**
16239 * Render the graph and markers
16240 */
16241 render: function() {
16242 var series = this,
16243 chart = series.chart,
16244 group,
16245 options = series.options,
16246 // Animation doesn't work in IE8 quirks when the group div is hidden,
16247 // and looks bad in other oldIE
16248 animDuration = !!series.animate && chart.renderer.isSVG && animObject(options.animation).duration,
16249 visibility = series.visible ? 'inherit' : 'hidden', // #2597
16250 zIndex = options.zIndex,
16251 hasRendered = series.hasRendered,
16252 chartSeriesGroup = chart.seriesGroup,
16253 inverted = chart.inverted;
16254
16255 // the group
16256 group = series.plotGroup(
16257 'group',
16258 'series',
16259 visibility,
16260 zIndex,
16261 chartSeriesGroup
16262 );
16263
16264 series.markerGroup = series.plotGroup(
16265 'markerGroup',
16266 'markers',
16267 visibility,
16268 zIndex,
16269 chartSeriesGroup
16270 );
16271
16272 // initiate the animation
16273 if (animDuration) {
16274 series.animate(true);
16275 }
16276
16277 // SVGRenderer needs to know this before drawing elements (#1089, #1795)
16278 group.inverted = series.isCartesian ? inverted : false;
16279
16280 // draw the graph if any
16281 if (series.drawGraph) {
16282 series.drawGraph();
16283 series.applyZones();
16284 }
16285
16286 /* each(series.points, function (point) {
16287 if (point.redraw) {
16288 point.redraw();
16289 }
16290 });*/
16291
16292 // draw the data labels (inn pies they go before the points)
16293 if (series.drawDataLabels) {
16294 series.drawDataLabels();
16295 }
16296
16297 // draw the points
16298 if (series.visible) {
16299 series.drawPoints();
16300 }
16301
16302
16303 // draw the mouse tracking area
16304 if (series.drawTracker && series.options.enableMouseTracking !== false) {
16305 series.drawTracker();
16306 }
16307
16308 // Handle inverted series and tracker groups
16309 series.invertGroups(inverted);
16310
16311 // Initial clipping, must be defined after inverting groups for VML. Applies to columns etc. (#3839).
16312 if (options.clip !== false && !series.sharedClipKey && !hasRendered) {
16313 group.clip(chart.clipRect);
16314 }
16315
16316 // Run the animation
16317 if (animDuration) {
16318 series.animate();
16319 }
16320
16321 // Call the afterAnimate function on animation complete (but don't overwrite the animation.complete option
16322 // which should be available to the user).
16323 if (!hasRendered) {
16324 series.animationTimeout = syncTimeout(function() {
16325 series.afterAnimate();
16326 }, animDuration);
16327 }
16328
16329 series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
16330 // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
16331 series.hasRendered = true;
16332 },
16333
16334 /**
16335 * Redraw the series after an update in the axes.
16336 */
16337 redraw: function() {
16338 var series = this,
16339 chart = series.chart,
16340 wasDirty = series.isDirty || series.isDirtyData, // cache it here as it is set to false in render, but used after
16341 group = series.group,
16342 xAxis = series.xAxis,
16343 yAxis = series.yAxis;
16344
16345 // reposition on resize
16346 if (group) {
16347 if (chart.inverted) {
16348 group.attr({
16349 width: chart.plotWidth,
16350 height: chart.plotHeight
16351 });
16352 }
16353
16354 group.animate({
16355 translateX: pick(xAxis && xAxis.left, chart.plotLeft),
16356 translateY: pick(yAxis && yAxis.top, chart.plotTop)
16357 });
16358 }
16359
16360 series.translate();
16361 series.render();
16362 if (wasDirty) { // #3868, #3945
16363 delete this.kdTree;
16364 }
16365 },
16366
16367 /**
16368 * KD Tree && PointSearching Implementation
16369 */
16370
16371 kdDimensions: 1,
16372 kdAxisArray: ['clientX', 'plotY'],
16373
16374 searchPoint: function(e, compareX) {
16375 var series = this,
16376 xAxis = series.xAxis,
16377 yAxis = series.yAxis,
16378 inverted = series.chart.inverted;
16379
16380 return this.searchKDTree({
16381 clientX: inverted ? xAxis.len - e.chartY + xAxis.pos : e.chartX - xAxis.pos,
16382 plotY: inverted ? yAxis.len - e.chartX + yAxis.pos : e.chartY - yAxis.pos
16383 }, compareX);
16384 },
16385
16386 buildKDTree: function() {
16387 var series = this,
16388 dimensions = series.kdDimensions;
16389
16390 // Internal function
16391 function _kdtree(points, depth, dimensions) {
16392 var axis,
16393 median,
16394 length = points && points.length;
16395
16396 if (length) {
16397
16398 // alternate between the axis
16399 axis = series.kdAxisArray[depth % dimensions];
16400
16401 // sort point array
16402 points.sort(function(a, b) {
16403 return a[axis] - b[axis];
16404 });
16405
16406 median = Math.floor(length / 2);
16407
16408 // build and return nod
16409 return {
16410 point: points[median],
16411 left: _kdtree(points.slice(0, median), depth + 1, dimensions),
16412 right: _kdtree(points.slice(median + 1), depth + 1, dimensions)
16413 };
16414
16415 }
16416 }
16417
16418 // Start the recursive build process with a clone of the points array and null points filtered out (#3873)
16419 function startRecursive() {
16420 series.kdTree = _kdtree(
16421 series.getValidPoints(
16422 null, !series.directTouch // For line-type series restrict to plot area, but column-type series not (#3916, #4511)
16423 ),
16424 dimensions,
16425 dimensions
16426 );
16427 }
16428 delete series.kdTree;
16429
16430 // For testing tooltips, don't build async
16431 syncTimeout(startRecursive, series.options.kdNow ? 0 : 1);
16432 },
16433
16434 searchKDTree: function(point, compareX) {
16435 var series = this,
16436 kdX = this.kdAxisArray[0],
16437 kdY = this.kdAxisArray[1],
16438 kdComparer = compareX ? 'distX' : 'dist';
16439
16440 // Set the one and two dimensional distance on the point object
16441 function setDistance(p1, p2) {
16442 var x = (defined(p1[kdX]) && defined(p2[kdX])) ? Math.pow(p1[kdX] - p2[kdX], 2) : null,
16443 y = (defined(p1[kdY]) && defined(p2[kdY])) ? Math.pow(p1[kdY] - p2[kdY], 2) : null,
16444 r = (x || 0) + (y || 0);
16445
16446 p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE;
16447 p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE;
16448 }
16449
16450 function _search(search, tree, depth, dimensions) {
16451 var point = tree.point,
16452 axis = series.kdAxisArray[depth % dimensions],
16453 tdist,
16454 sideA,
16455 sideB,
16456 ret = point,
16457 nPoint1,
16458 nPoint2;
16459
16460 setDistance(search, point);
16461
16462 // Pick side based on distance to splitting point
16463 tdist = search[axis] - point[axis];
16464 sideA = tdist < 0 ? 'left' : 'right';
16465 sideB = tdist < 0 ? 'right' : 'left';
16466
16467 // End of tree
16468 if (tree[sideA]) {
16469 nPoint1 = _search(search, tree[sideA], depth + 1, dimensions);
16470
16471 ret = (nPoint1[kdComparer] < ret[kdComparer] ? nPoint1 : point);
16472 }
16473 if (tree[sideB]) {
16474 // compare distance to current best to splitting point to decide wether to check side B or not
16475 if (Math.sqrt(tdist * tdist) < ret[kdComparer]) {
16476 nPoint2 = _search(search, tree[sideB], depth + 1, dimensions);
16477 ret = (nPoint2[kdComparer] < ret[kdComparer] ? nPoint2 : ret);
16478 }
16479 }
16480
16481 return ret;
16482 }
16483
16484 if (!this.kdTree) {
16485 this.buildKDTree();
16486 }
16487
16488 if (this.kdTree) {
16489 return _search(point,
16490 this.kdTree, this.kdDimensions, this.kdDimensions);
16491 }
16492 }
16493
16494 }); // end Series prototype
16495
16496 }(Highcharts));
16497 (function(H) {
16498 /**
16499 * (c) 2010-2016 Torstein Honsi
16500 *
16501 * License: www.highcharts.com/license
16502 */
16503 'use strict';
16504 var addEvent = H.addEvent,
16505 animate = H.animate,
16506 Axis = H.Axis,
16507 Chart = H.Chart,
16508 createElement = H.createElement,
16509 css = H.css,
16510 defined = H.defined,
16511 each = H.each,
16512 erase = H.erase,
16513 extend = H.extend,
16514 fireEvent = H.fireEvent,
16515 inArray = H.inArray,
16516 isObject = H.isObject,
16517 merge = H.merge,
16518 pick = H.pick,
16519 Point = H.Point,
16520 Series = H.Series,
16521 seriesTypes = H.seriesTypes,
16522 setAnimation = H.setAnimation,
16523 splat = H.splat;
16524
16525 // Extend the Chart prototype for dynamic methods
16526 extend(Chart.prototype, {
16527
16528 /**
16529 * Add a series dynamically after time
16530 *
16531 * @param {Object} options The config options
16532 * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true.
16533 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
16534 * configuration
16535 *
16536 * @return {Object} series The newly created series object
16537 */
16538 addSeries: function(options, redraw, animation) {
16539 var series,
16540 chart = this;
16541
16542 if (options) {
16543 redraw = pick(redraw, true); // defaults to true
16544
16545 fireEvent(chart, 'addSeries', {
16546 options: options
16547 }, function() {
16548 series = chart.initSeries(options);
16549
16550 chart.isDirtyLegend = true; // the series array is out of sync with the display
16551 chart.linkSeries();
16552 if (redraw) {
16553 chart.redraw(animation);
16554 }
16555 });
16556 }
16557
16558 return series;
16559 },
16560
16561 /**
16562 * Add an axis to the chart
16563 * @param {Object} options The axis option
16564 * @param {Boolean} isX Whether it is an X axis or a value axis
16565 */
16566 addAxis: function(options, isX, redraw, animation) {
16567 var key = isX ? 'xAxis' : 'yAxis',
16568 chartOptions = this.options,
16569 userOptions = merge(options, {
16570 index: this[key].length,
16571 isX: isX
16572 });
16573
16574 new Axis(this, userOptions); // eslint-disable-line no-new
16575
16576 // Push the new axis options to the chart options
16577 chartOptions[key] = splat(chartOptions[key] || {});
16578 chartOptions[key].push(userOptions);
16579
16580 if (pick(redraw, true)) {
16581 this.redraw(animation);
16582 }
16583 },
16584
16585 /**
16586 * Dim the chart and show a loading text or symbol
16587 * @param {String} str An optional text to show in the loading label instead of the default one
16588 */
16589 showLoading: function(str) {
16590 var chart = this,
16591 options = chart.options,
16592 loadingDiv = chart.loadingDiv,
16593 loadingOptions = options.loading,
16594 setLoadingSize = function() {
16595 if (loadingDiv) {
16596 css(loadingDiv, {
16597 left: chart.plotLeft + 'px',
16598 top: chart.plotTop + 'px',
16599 width: chart.plotWidth + 'px',
16600 height: chart.plotHeight + 'px'
16601 });
16602 }
16603 };
16604
16605 // create the layer at the first call
16606 if (!loadingDiv) {
16607 chart.loadingDiv = loadingDiv = createElement('div', {
16608 className: 'highcharts-loading highcharts-loading-hidden'
16609 }, null, chart.container);
16610
16611 chart.loadingSpan = createElement(
16612 'span', {
16613 className: 'highcharts-loading-inner'
16614 },
16615 null,
16616 loadingDiv
16617 );
16618 addEvent(chart, 'redraw', setLoadingSize); // #1080
16619 }
16620 setTimeout(function() {
16621 loadingDiv.className = 'highcharts-loading';
16622 });
16623
16624 // Update text
16625 chart.loadingSpan.innerHTML = str || options.lang.loading;
16626
16627
16628 // Update visuals
16629 css(loadingDiv, extend(loadingOptions.style, {
16630 zIndex: 10
16631 }));
16632 css(chart.loadingSpan, loadingOptions.labelStyle);
16633
16634 // Show it
16635 if (!chart.loadingShown) {
16636 css(loadingDiv, {
16637 opacity: 0,
16638 display: ''
16639 });
16640 animate(loadingDiv, {
16641 opacity: loadingOptions.style.opacity || 0.5
16642 }, {
16643 duration: loadingOptions.showDuration || 0
16644 });
16645 }
16646
16647
16648 chart.loadingShown = true;
16649 setLoadingSize();
16650 },
16651
16652 /**
16653 * Hide the loading layer
16654 */
16655 hideLoading: function() {
16656 var options = this.options,
16657 loadingDiv = this.loadingDiv;
16658
16659 if (loadingDiv) {
16660 loadingDiv.className = 'highcharts-loading highcharts-loading-hidden';
16661
16662 animate(loadingDiv, {
16663 opacity: 0
16664 }, {
16665 duration: options.loading.hideDuration || 100,
16666 complete: function() {
16667 css(loadingDiv, {
16668 display: 'none'
16669 });
16670 }
16671 });
16672
16673 }
16674 this.loadingShown = false;
16675 },
16676
16677 /**
16678 * These properties cause isDirtyBox to be set to true when updating. Can be extended from plugins.
16679 */
16680 propsRequireDirtyBox: ['backgroundColor', 'borderColor', 'borderWidth', 'margin', 'marginTop', 'marginRight',
16681 'marginBottom', 'marginLeft', 'spacing', 'spacingTop', 'spacingRight', 'spacingBottom', 'spacingLeft',
16682 'borderRadius', 'plotBackgroundColor', 'plotBackgroundImage', 'plotBorderColor', 'plotBorderWidth',
16683 'plotShadow', 'shadow'
16684 ],
16685
16686 /**
16687 * These properties cause all series to be updated when updating. Can be extended from plugins.
16688 */
16689 propsRequireUpdateSeries: ['chart.polar', 'chart.ignoreHiddenSeries', 'chart.type', 'colors', 'plotOptions'],
16690
16691 /**
16692 * Chart.update function that takes the whole options stucture.
16693 */
16694 update: function(options, redraw) {
16695 var key,
16696 adders = {
16697 credits: 'addCredits',
16698 title: 'setTitle',
16699 subtitle: 'setSubtitle'
16700 },
16701 optionsChart = options.chart,
16702 updateAllAxes,
16703 updateAllSeries;
16704
16705 // If the top-level chart option is present, some special updates are required
16706 if (optionsChart) {
16707 merge(true, this.options.chart, optionsChart);
16708
16709 // Setter function
16710 if ('className' in optionsChart) {
16711 this.setClassName(optionsChart.className);
16712 }
16713
16714 if ('inverted' in optionsChart || 'polar' in optionsChart) {
16715 this.propFromSeries(); // Parses options.chart.inverted and options.chart.polar together with the available series
16716 updateAllAxes = true;
16717 }
16718
16719 for (key in optionsChart) {
16720 if (optionsChart.hasOwnProperty(key)) {
16721 if (inArray('chart.' + key, this.propsRequireUpdateSeries) !== -1) {
16722 updateAllSeries = true;
16723 }
16724 // Only dirty box
16725 if (inArray(key, this.propsRequireDirtyBox) !== -1) {
16726 this.isDirtyBox = true;
16727 }
16728
16729 }
16730 }
16731
16732
16733 if ('style' in optionsChart) {
16734 this.renderer.setStyle(optionsChart.style);
16735 }
16736
16737 }
16738
16739 // Some option stuctures correspond one-to-one to chart objects that have
16740 // update methods, for example
16741 // options.credits => chart.credits
16742 // options.legend => chart.legend
16743 // options.title => chart.title
16744 // options.tooltip => chart.tooltip
16745 // options.subtitle => chart.subtitle
16746 // options.navigator => chart.navigator
16747 // options.scrollbar => chart.scrollbar
16748 for (key in options) {
16749 if (this[key] && typeof this[key].update === 'function') {
16750 this[key].update(options[key], false);
16751
16752 // If a one-to-one object does not exist, look for an adder function
16753 } else if (typeof this[adders[key]] === 'function') {
16754 this[adders[key]](options[key]);
16755 }
16756
16757 if (key !== 'chart' && inArray(key, this.propsRequireUpdateSeries) !== -1) {
16758 updateAllSeries = true;
16759 }
16760 }
16761
16762
16763 if (options.colors) {
16764 this.options.colors = options.colors;
16765 }
16766
16767
16768 if (options.plotOptions) {
16769 merge(true, this.options.plotOptions, options.plotOptions);
16770 }
16771
16772 // Setters for collections. For axes and series, each item is referred by an id. If the
16773 // id is not found, it defaults to the first item in the collection, so setting series
16774 // without an id, will update the first series in the chart.
16775 each(['xAxis', 'yAxis', 'series'], function(coll) {
16776 if (options[coll]) {
16777 each(splat(options[coll]), function(newOptions) {
16778 var item = (defined(newOptions.id) && this.get(newOptions.id)) || this[coll][0];
16779 if (item && item.coll === coll) {
16780 item.update(newOptions, false);
16781 }
16782 }, this);
16783 }
16784 }, this);
16785
16786 if (updateAllAxes) {
16787 each(this.axes, function(axis) {
16788 axis.update({}, false);
16789 });
16790 }
16791
16792 // Certain options require the whole series structure to be thrown away
16793 // and rebuilt
16794 if (updateAllSeries) {
16795 each(this.series, function(series) {
16796 series.update({}, false);
16797 });
16798 }
16799
16800 // For loading, just update the options, do not redraw
16801 if (options.loading) {
16802 merge(true, this.options.loading, options.loading);
16803 }
16804
16805 // Update size. Redraw is forced.
16806 if (optionsChart && ('width' in optionsChart || 'height' in optionsChart)) {
16807 this.setSize(optionsChart.width, optionsChart.height);
16808 } else if (pick(redraw, true)) {
16809 this.redraw();
16810 }
16811 },
16812
16813 /**
16814 * Setter function to allow use from chart.update
16815 */
16816 setSubtitle: function(options) {
16817 this.setTitle(undefined, options);
16818 }
16819
16820
16821 });
16822
16823 // extend the Point prototype for dynamic methods
16824 extend(Point.prototype, {
16825 /**
16826 * Point.update with new options (typically x/y data) and optionally redraw the series.
16827 *
16828 * @param {Object} options Point options as defined in the series.data array
16829 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
16830 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
16831 * configuration
16832 */
16833 update: function(options, redraw, animation, runEvent) {
16834 var point = this,
16835 series = point.series,
16836 graphic = point.graphic,
16837 i,
16838 chart = series.chart,
16839 seriesOptions = series.options;
16840
16841 redraw = pick(redraw, true);
16842
16843 function update() {
16844
16845 point.applyOptions(options);
16846
16847 // Update visuals
16848 if (point.y === null && graphic) { // #4146
16849 point.graphic = graphic.destroy();
16850 }
16851 if (isObject(options, true)) {
16852 // Destroy so we can get new elements
16853 if (graphic && graphic.element) {
16854 if (options && options.marker && options.marker.symbol) {
16855 point.graphic = graphic.destroy();
16856 }
16857 }
16858 if (options && options.dataLabels && point.dataLabel) { // #2468
16859 point.dataLabel = point.dataLabel.destroy();
16860 }
16861 }
16862
16863 // record changes in the parallel arrays
16864 i = point.index;
16865 series.updateParallelArrays(point, i);
16866
16867 // Record the options to options.data. If there is an object from before,
16868 // use point options, otherwise use raw options. (#4701)
16869 seriesOptions.data[i] = isObject(seriesOptions.data[i], true) ? point.options : options;
16870
16871 // redraw
16872 series.isDirty = series.isDirtyData = true;
16873 if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320
16874 chart.isDirtyBox = true;
16875 }
16876
16877 if (seriesOptions.legendType === 'point') { // #1831, #1885
16878 chart.isDirtyLegend = true;
16879 }
16880 if (redraw) {
16881 chart.redraw(animation);
16882 }
16883 }
16884
16885 // Fire the event with a default handler of doing the update
16886 if (runEvent === false) { // When called from setData
16887 update();
16888 } else {
16889 point.firePointEvent('update', {
16890 options: options
16891 }, update);
16892 }
16893 },
16894
16895 /**
16896 * Remove a point and optionally redraw the series and if necessary the axes
16897 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
16898 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
16899 * configuration
16900 */
16901 remove: function(redraw, animation) {
16902 this.series.removePoint(inArray(this, this.series.data), redraw, animation);
16903 }
16904 });
16905
16906 // Extend the series prototype for dynamic methods
16907 extend(Series.prototype, {
16908 /**
16909 * Add a point dynamically after chart load time
16910 * @param {Object} options Point options as given in series.data
16911 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
16912 * @param {Boolean} shift If shift is true, a point is shifted off the start
16913 * of the series as one is appended to the end.
16914 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
16915 * configuration
16916 */
16917 addPoint: function(options, redraw, shift, animation) {
16918 var series = this,
16919 seriesOptions = series.options,
16920 data = series.data,
16921 chart = series.chart,
16922 names = series.xAxis && series.xAxis.names,
16923 dataOptions = seriesOptions.data,
16924 point,
16925 isInTheMiddle,
16926 xData = series.xData,
16927 i,
16928 x;
16929
16930 // Optional redraw, defaults to true
16931 redraw = pick(redraw, true);
16932
16933 // Get options and push the point to xData, yData and series.options. In series.generatePoints
16934 // the Point instance will be created on demand and pushed to the series.data array.
16935 point = {
16936 series: series
16937 };
16938 series.pointClass.prototype.applyOptions.apply(point, [options]);
16939 x = point.x;
16940
16941 // Get the insertion point
16942 i = xData.length;
16943 if (series.requireSorting && x < xData[i - 1]) {
16944 isInTheMiddle = true;
16945 while (i && xData[i - 1] > x) {
16946 i--;
16947 }
16948 }
16949
16950 series.updateParallelArrays(point, 'splice', i, 0, 0); // insert undefined item
16951 series.updateParallelArrays(point, i); // update it
16952
16953 if (names && point.name) {
16954 names[x] = point.name;
16955 }
16956 dataOptions.splice(i, 0, options);
16957
16958 if (isInTheMiddle) {
16959 series.data.splice(i, 0, null);
16960 series.processData();
16961 }
16962
16963 // Generate points to be added to the legend (#1329)
16964 if (seriesOptions.legendType === 'point') {
16965 series.generatePoints();
16966 }
16967
16968 // Shift the first point off the parallel arrays
16969 if (shift) {
16970 if (data[0] && data[0].remove) {
16971 data[0].remove(false);
16972 } else {
16973 data.shift();
16974 series.updateParallelArrays(point, 'shift');
16975
16976 dataOptions.shift();
16977 }
16978 }
16979
16980 // redraw
16981 series.isDirty = true;
16982 series.isDirtyData = true;
16983
16984 if (redraw) {
16985
16986 chart.redraw(animation); // Animation is set anyway on redraw, #5665
16987 }
16988 },
16989
16990 /**
16991 * Remove a point (rendered or not), by index
16992 */
16993 removePoint: function(i, redraw, animation) {
16994
16995 var series = this,
16996 data = series.data,
16997 point = data[i],
16998 points = series.points,
16999 chart = series.chart,
17000 remove = function() {
17001
17002 if (points && points.length === data.length) { // #4935
17003 points.splice(i, 1);
17004 }
17005 data.splice(i, 1);
17006 series.options.data.splice(i, 1);
17007 series.updateParallelArrays(point || {
17008 series: series
17009 }, 'splice', i, 1);
17010
17011 if (point) {
17012 point.destroy();
17013 }
17014
17015 // redraw
17016 series.isDirty = true;
17017 series.isDirtyData = true;
17018 if (redraw) {
17019 chart.redraw();
17020 }
17021 };
17022
17023 setAnimation(animation, chart);
17024 redraw = pick(redraw, true);
17025
17026 // Fire the event with a default handler of removing the point
17027 if (point) {
17028 point.firePointEvent('remove', null, remove);
17029 } else {
17030 remove();
17031 }
17032 },
17033
17034 /**
17035 * Remove a series and optionally redraw the chart
17036 *
17037 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
17038 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
17039 * configuration
17040 */
17041 remove: function(redraw, animation, withEvent) {
17042 var series = this,
17043 chart = series.chart;
17044
17045 function remove() {
17046
17047 // Destroy elements
17048 series.destroy();
17049
17050 // Redraw
17051 chart.isDirtyLegend = chart.isDirtyBox = true;
17052 chart.linkSeries();
17053
17054 if (pick(redraw, true)) {
17055 chart.redraw(animation);
17056 }
17057 }
17058
17059 // Fire the event with a default handler of removing the point
17060 if (withEvent !== false) {
17061 fireEvent(series, 'remove', null, remove);
17062 } else {
17063 remove();
17064 }
17065 },
17066
17067 /**
17068 * Series.update with a new set of options
17069 */
17070 update: function(newOptions, redraw) {
17071 var series = this,
17072 chart = this.chart,
17073 // must use user options when changing type because this.options is merged
17074 // in with type specific plotOptions
17075 oldOptions = this.userOptions,
17076 oldType = this.type,
17077 newType = newOptions.type || oldOptions.type || chart.options.chart.type,
17078 proto = seriesTypes[oldType].prototype,
17079 preserve = ['group', 'markerGroup', 'dataLabelsGroup'],
17080 n;
17081
17082 // If we're changing type or zIndex, create new groups (#3380, #3404)
17083 if ((newType && newType !== oldType) || newOptions.zIndex !== undefined) {
17084 preserve.length = 0;
17085 }
17086
17087 // Make sure groups are not destroyed (#3094)
17088 each(preserve, function(prop) {
17089 preserve[prop] = series[prop];
17090 delete series[prop];
17091 });
17092
17093 // Do the merge, with some forced options
17094 newOptions = merge(oldOptions, {
17095 animation: false,
17096 index: this.index,
17097 pointStart: this.xData[0] // when updating after addPoint
17098 }, {
17099 data: this.options.data
17100 }, newOptions);
17101
17102 // Destroy the series and delete all properties. Reinsert all methods
17103 // and properties from the new type prototype (#2270, #3719)
17104 this.remove(false, null, false);
17105 for (n in proto) {
17106 this[n] = undefined;
17107 }
17108 extend(this, seriesTypes[newType || oldType].prototype);
17109
17110 // Re-register groups (#3094)
17111 each(preserve, function(prop) {
17112 series[prop] = preserve[prop];
17113 });
17114
17115 this.init(chart, newOptions);
17116 chart.linkSeries(); // Links are lost in this.remove (#3028)
17117 if (pick(redraw, true)) {
17118 chart.redraw(false);
17119 }
17120 }
17121 });
17122
17123 // Extend the Axis.prototype for dynamic methods
17124 extend(Axis.prototype, {
17125
17126 /**
17127 * Axis.update with a new options structure
17128 */
17129 update: function(newOptions, redraw) {
17130 var chart = this.chart;
17131
17132 newOptions = chart.options[this.coll][this.options.index] = merge(this.userOptions, newOptions);
17133
17134 this.destroy(true);
17135
17136 this.init(chart, extend(newOptions, {
17137 events: undefined
17138 }));
17139
17140 chart.isDirtyBox = true;
17141 if (pick(redraw, true)) {
17142 chart.redraw();
17143 }
17144 },
17145
17146 /**
17147 * Remove the axis from the chart
17148 */
17149 remove: function(redraw) {
17150 var chart = this.chart,
17151 key = this.coll, // xAxis or yAxis
17152 axisSeries = this.series,
17153 i = axisSeries.length;
17154
17155 // Remove associated series (#2687)
17156 while (i--) {
17157 if (axisSeries[i]) {
17158 axisSeries[i].remove(false);
17159 }
17160 }
17161
17162 // Remove the axis
17163 erase(chart.axes, this);
17164 erase(chart[key], this);
17165 chart.options[key].splice(this.options.index, 1);
17166 each(chart[key], function(axis, i) { // Re-index, #1706
17167 axis.options.index = i;
17168 });
17169 this.destroy();
17170 chart.isDirtyBox = true;
17171
17172 if (pick(redraw, true)) {
17173 chart.redraw();
17174 }
17175 },
17176
17177 /**
17178 * Update the axis title by options
17179 */
17180 setTitle: function(newTitleOptions, redraw) {
17181 this.update({
17182 title: newTitleOptions
17183 }, redraw);
17184 },
17185
17186 /**
17187 * Set new axis categories and optionally redraw
17188 * @param {Array} categories
17189 * @param {Boolean} redraw
17190 */
17191 setCategories: function(categories, redraw) {
17192 this.update({
17193 categories: categories
17194 }, redraw);
17195 }
17196
17197 });
17198
17199 }(Highcharts));
17200 (function(H) {
17201 /**
17202 * (c) 2010-2016 Torstein Honsi
17203 *
17204 * License: www.highcharts.com/license
17205 */
17206 'use strict';
17207 var animObject = H.animObject,
17208 color = H.color,
17209 each = H.each,
17210 extend = H.extend,
17211 isNumber = H.isNumber,
17212 LegendSymbolMixin = H.LegendSymbolMixin,
17213 merge = H.merge,
17214 noop = H.noop,
17215 pick = H.pick,
17216 Series = H.Series,
17217 seriesType = H.seriesType,
17218 stop = H.stop,
17219 svg = H.svg;
17220 /**
17221 * The column series type
17222 */
17223 seriesType('column', 'line', {
17224 borderRadius: 0,
17225 //colorByPoint: undefined,
17226 groupPadding: 0.2,
17227 //grouping: true,
17228 marker: null, // point options are specified in the base options
17229 pointPadding: 0.1,
17230 //pointWidth: null,
17231 minPointLength: 0,
17232 cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes
17233 pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories
17234 states: {
17235 hover: {
17236 halo: false,
17237
17238 brightness: 0.1,
17239 shadow: false
17240
17241 },
17242
17243 select: {
17244 color: '#cccccc',
17245 borderColor: '#000000',
17246 shadow: false
17247 }
17248
17249 },
17250 dataLabels: {
17251 align: null, // auto
17252 verticalAlign: null, // auto
17253 y: null
17254 },
17255 softThreshold: false,
17256 startFromThreshold: true, // false doesn't work well: http://jsfiddle.net/highcharts/hz8fopan/14/
17257 stickyTracking: false,
17258 tooltip: {
17259 distance: 6
17260 },
17261 threshold: 0,
17262
17263 borderColor: '#ffffff'
17264 // borderWidth: 1
17265
17266
17267 // Prototype members
17268 }, {
17269 cropShoulder: 0,
17270 directTouch: true, // When tooltip is not shared, this series (and derivatives) requires direct touch/hover. KD-tree does not apply.
17271 trackerGroups: ['group', 'dataLabelsGroup'],
17272 negStacks: true, // use separate negative stacks, unlike area stacks where a negative
17273 // point is substracted from previous (#1910)
17274
17275 /**
17276 * Initialize the series
17277 */
17278 init: function() {
17279 Series.prototype.init.apply(this, arguments);
17280
17281 var series = this,
17282 chart = series.chart;
17283
17284 // if the series is added dynamically, force redraw of other
17285 // series affected by a new column
17286 if (chart.hasRendered) {
17287 each(chart.series, function(otherSeries) {
17288 if (otherSeries.type === series.type) {
17289 otherSeries.isDirty = true;
17290 }
17291 });
17292 }
17293 },
17294
17295 /**
17296 * Return the width and x offset of the columns adjusted for grouping, groupPadding, pointPadding,
17297 * pointWidth etc.
17298 */
17299 getColumnMetrics: function() {
17300
17301 var series = this,
17302 options = series.options,
17303 xAxis = series.xAxis,
17304 yAxis = series.yAxis,
17305 reversedXAxis = xAxis.reversed,
17306 stackKey,
17307 stackGroups = {},
17308 columnCount = 0;
17309
17310 // Get the total number of column type series.
17311 // This is called on every series. Consider moving this logic to a
17312 // chart.orderStacks() function and call it on init, addSeries and removeSeries
17313 if (options.grouping === false) {
17314 columnCount = 1;
17315 } else {
17316 each(series.chart.series, function(otherSeries) {
17317 var otherOptions = otherSeries.options,
17318 otherYAxis = otherSeries.yAxis,
17319 columnIndex;
17320 if (otherSeries.type === series.type && otherSeries.visible &&
17321 yAxis.len === otherYAxis.len && yAxis.pos === otherYAxis.pos) { // #642, #2086
17322 if (otherOptions.stacking) {
17323 stackKey = otherSeries.stackKey;
17324 if (stackGroups[stackKey] === undefined) {
17325 stackGroups[stackKey] = columnCount++;
17326 }
17327 columnIndex = stackGroups[stackKey];
17328 } else if (otherOptions.grouping !== false) { // #1162
17329 columnIndex = columnCount++;
17330 }
17331 otherSeries.columnIndex = columnIndex;
17332 }
17333 });
17334 }
17335
17336 var categoryWidth = Math.min(
17337 Math.abs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || xAxis.tickInterval || 1), // #2610
17338 xAxis.len // #1535
17339 ),
17340 groupPadding = categoryWidth * options.groupPadding,
17341 groupWidth = categoryWidth - 2 * groupPadding,
17342 pointOffsetWidth = groupWidth / columnCount,
17343 pointWidth = Math.min(
17344 options.maxPointWidth || xAxis.len,
17345 pick(options.pointWidth, pointOffsetWidth * (1 - 2 * options.pointPadding))
17346 ),
17347 pointPadding = (pointOffsetWidth - pointWidth) / 2,
17348 colIndex = (series.columnIndex || 0) + (reversedXAxis ? 1 : 0), // #1251, #3737
17349 pointXOffset = pointPadding + (groupPadding + colIndex *
17350 pointOffsetWidth - (categoryWidth / 2)) *
17351 (reversedXAxis ? -1 : 1);
17352
17353 // Save it for reading in linked series (Error bars particularly)
17354 series.columnMetrics = {
17355 width: pointWidth,
17356 offset: pointXOffset
17357 };
17358 return series.columnMetrics;
17359
17360 },
17361
17362 /**
17363 * Make the columns crisp. The edges are rounded to the nearest full pixel.
17364 */
17365 crispCol: function(x, y, w, h) {
17366 var chart = this.chart,
17367 borderWidth = this.borderWidth,
17368 xCrisp = -(borderWidth % 2 ? 0.5 : 0),
17369 yCrisp = borderWidth % 2 ? 0.5 : 1,
17370 right,
17371 bottom,
17372 fromTop;
17373
17374 if (chart.inverted && chart.renderer.isVML) {
17375 yCrisp += 1;
17376 }
17377
17378 // Horizontal. We need to first compute the exact right edge, then round it
17379 // and compute the width from there.
17380 right = Math.round(x + w) + xCrisp;
17381 x = Math.round(x) + xCrisp;
17382 w = right - x;
17383
17384 // Vertical
17385 bottom = Math.round(y + h) + yCrisp;
17386 fromTop = Math.abs(y) <= 0.5 && bottom > 0.5; // #4504, #4656
17387 y = Math.round(y) + yCrisp;
17388 h = bottom - y;
17389
17390 // Top edges are exceptions
17391 if (fromTop && h) { // #5146
17392 y -= 1;
17393 h += 1;
17394 }
17395
17396 return {
17397 x: x,
17398 y: y,
17399 width: w,
17400 height: h
17401 };
17402 },
17403
17404 /**
17405 * Translate each point to the plot area coordinate system and find shape positions
17406 */
17407 translate: function() {
17408 var series = this,
17409 chart = series.chart,
17410 options = series.options,
17411 dense = series.dense = series.closestPointRange * series.xAxis.transA < 2,
17412 borderWidth = series.borderWidth = pick(
17413 options.borderWidth,
17414 dense ? 0 : 1 // #3635
17415 ),
17416 yAxis = series.yAxis,
17417 threshold = options.threshold,
17418 translatedThreshold = series.translatedThreshold = yAxis.getThreshold(threshold),
17419 minPointLength = pick(options.minPointLength, 5),
17420 metrics = series.getColumnMetrics(),
17421 pointWidth = metrics.width,
17422 seriesBarW = series.barW = Math.max(pointWidth, 1 + 2 * borderWidth), // postprocessed for border width
17423 pointXOffset = series.pointXOffset = metrics.offset;
17424
17425 if (chart.inverted) {
17426 translatedThreshold -= 0.5; // #3355
17427 }
17428
17429 // When the pointPadding is 0, we want the columns to be packed tightly, so we allow individual
17430 // columns to have individual sizes. When pointPadding is greater, we strive for equal-width
17431 // columns (#2694).
17432 if (options.pointPadding) {
17433 seriesBarW = Math.ceil(seriesBarW);
17434 }
17435
17436 Series.prototype.translate.apply(series);
17437
17438 // Record the new values
17439 each(series.points, function(point) {
17440 var yBottom = pick(point.yBottom, translatedThreshold),
17441 safeDistance = 999 + Math.abs(yBottom),
17442 plotY = Math.min(Math.max(-safeDistance, point.plotY), yAxis.len + safeDistance), // Don't draw too far outside plot area (#1303, #2241, #4264)
17443 barX = point.plotX + pointXOffset,
17444 barW = seriesBarW,
17445 barY = Math.min(plotY, yBottom),
17446 up,
17447 barH = Math.max(plotY, yBottom) - barY;
17448
17449 // Handle options.minPointLength
17450 if (Math.abs(barH) < minPointLength) {
17451 if (minPointLength) {
17452 barH = minPointLength;
17453 up = (!yAxis.reversed && !point.negative) || (yAxis.reversed && point.negative);
17454 barY = Math.abs(barY - translatedThreshold) > minPointLength ? // stacked
17455 yBottom - minPointLength : // keep position
17456 translatedThreshold - (up ? minPointLength : 0); // #1485, #4051
17457 }
17458 }
17459
17460 // Cache for access in polar
17461 point.barX = barX;
17462 point.pointWidth = pointWidth;
17463
17464 // Fix the tooltip on center of grouped columns (#1216, #424, #3648)
17465 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];
17466
17467 // Register shape type and arguments to be used in drawPoints
17468 point.shapeType = 'rect';
17469 point.shapeArgs = series.crispCol.apply(
17470 series,
17471 point.isNull ? [point.plotX, yAxis.len / 2, 0, 0] : // #3169, drilldown from null must have a position to work from
17472 [barX, barY, barW, barH]
17473 );
17474 });
17475
17476 },
17477
17478 getSymbol: noop,
17479
17480 /**
17481 * Use a solid rectangle like the area series types
17482 */
17483 drawLegendSymbol: LegendSymbolMixin.drawRectangle,
17484
17485
17486 /**
17487 * Columns have no graph
17488 */
17489 drawGraph: function() {
17490 this.group[this.dense ? 'addClass' : 'removeClass']('highcharts-dense-data');
17491 },
17492
17493
17494 /**
17495 * Get presentational attributes
17496 */
17497 pointAttribs: function(point, state) {
17498 var options = this.options,
17499 stateOptions,
17500 ret,
17501 p2o = this.pointAttrToOptions || {},
17502 strokeOption = p2o.stroke || 'borderColor',
17503 strokeWidthOption = p2o['stroke-width'] || 'borderWidth',
17504 fill = (point && point.color) || this.color,
17505 stroke = options[strokeOption] || this.color || fill, // set to fill when borderColor = null on pies
17506 dashstyle = options.dashStyle,
17507 zone,
17508 brightness;
17509
17510 // Handle zone colors
17511 if (point && this.zones.length) {
17512 zone = point.getZone();
17513 fill = (zone && zone.color) || point.options.color || this.color; // When zones are present, don't use point.color (#4267)
17514 }
17515
17516 // Select or hover states
17517 if (state) {
17518 stateOptions = options.states[state];
17519 brightness = stateOptions.brightness;
17520 fill = stateOptions.color ||
17521 (brightness !== undefined && color(fill).brighten(stateOptions.brightness).get()) ||
17522 fill;
17523 stroke = stateOptions[strokeOption] || stroke;
17524 dashstyle = stateOptions.dashStyle || dashstyle;
17525 }
17526
17527 ret = {
17528 'fill': fill,
17529 'stroke': stroke,
17530 'stroke-width': point[strokeWidthOption] || options[strokeWidthOption] || this[strokeWidthOption] || 0
17531 };
17532 if (options.borderRadius) {
17533 ret.r = options.borderRadius;
17534 }
17535
17536 if (dashstyle) {
17537 ret.dashstyle = dashstyle;
17538 }
17539
17540 return ret;
17541 },
17542
17543
17544 /**
17545 * Draw the columns. For bars, the series.group is rotated, so the same coordinates
17546 * apply for columns and bars. This method is inherited by scatter series.
17547 *
17548 */
17549 drawPoints: function() {
17550 var series = this,
17551 chart = this.chart,
17552 options = series.options,
17553 renderer = chart.renderer,
17554 animationLimit = options.animationLimit || 250,
17555 shapeArgs;
17556
17557 // draw the columns
17558 each(series.points, function(point) {
17559 var plotY = point.plotY,
17560 graphic = point.graphic;
17561
17562 if (isNumber(plotY) && point.y !== null) {
17563 shapeArgs = point.shapeArgs;
17564
17565 if (graphic) { // update
17566 stop(graphic);
17567 graphic[chart.pointCount < animationLimit ? 'animate' : 'attr'](
17568 merge(shapeArgs)
17569 );
17570
17571 } else {
17572 point.graphic = graphic = renderer[point.shapeType](shapeArgs)
17573 .attr({
17574 'class': point.getClassName()
17575 })
17576 .add(point.group || series.group);
17577 }
17578
17579
17580 // Presentational
17581 graphic
17582 .attr(series.pointAttribs(point, point.selected && 'select'))
17583 .shadow(options.shadow, null, options.stacking && !options.borderRadius);
17584
17585
17586 } else if (graphic) {
17587 point.graphic = graphic.destroy(); // #1269
17588 }
17589 });
17590 },
17591
17592 /**
17593 * Animate the column heights one by one from zero
17594 * @param {Boolean} init Whether to initialize the animation or run it
17595 */
17596 animate: function(init) {
17597 var series = this,
17598 yAxis = this.yAxis,
17599 options = series.options,
17600 inverted = this.chart.inverted,
17601 attr = {},
17602 translatedThreshold;
17603
17604 if (svg) { // VML is too slow anyway
17605 if (init) {
17606 attr.scaleY = 0.001;
17607 translatedThreshold = Math.min(yAxis.pos + yAxis.len, Math.max(yAxis.pos, yAxis.toPixels(options.threshold)));
17608 if (inverted) {
17609 attr.translateX = translatedThreshold - yAxis.len;
17610 } else {
17611 attr.translateY = translatedThreshold;
17612 }
17613 series.group.attr(attr);
17614
17615 } else { // run the animation
17616
17617 attr[inverted ? 'translateX' : 'translateY'] = yAxis.pos;
17618 series.group.animate(attr, extend(animObject(series.options.animation), {
17619 // Do the scale synchronously to ensure smooth updating (#5030)
17620 step: function(val, fx) {
17621 series.group.attr({
17622 scaleY: Math.max(0.001, fx.pos) // #5250
17623 });
17624 }
17625 }));
17626
17627 // delete this function to allow it only once
17628 series.animate = null;
17629 }
17630 }
17631 },
17632
17633 /**
17634 * Remove this series from the chart
17635 */
17636 remove: function() {
17637 var series = this,
17638 chart = series.chart;
17639
17640 // column and bar series affects other series of the same type
17641 // as they are either stacked or grouped
17642 if (chart.hasRendered) {
17643 each(chart.series, function(otherSeries) {
17644 if (otherSeries.type === series.type) {
17645 otherSeries.isDirty = true;
17646 }
17647 });
17648 }
17649
17650 Series.prototype.remove.apply(series, arguments);
17651 }
17652 });
17653
17654 }(Highcharts));
17655 (function(H) {
17656 /**
17657 * (c) 2010-2016 Torstein Honsi
17658 *
17659 * License: www.highcharts.com/license
17660 */
17661 'use strict';
17662 var Series = H.Series,
17663 seriesType = H.seriesType;
17664 /**
17665 * The scatter series type
17666 */
17667 seriesType('scatter', 'line', {
17668 lineWidth: 0,
17669 marker: {
17670 enabled: true // Overrides auto-enabling in line series (#3647)
17671 },
17672 tooltip: {
17673 headerFormat: '<span style="color:{point.color}">\u25CF</span> <span style="font-size: 0.85em"> {series.name}</span><br/>',
17674 pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>'
17675 }
17676
17677 // Prototype members
17678 }, {
17679 sorted: false,
17680 requireSorting: false,
17681 noSharedTooltip: true,
17682 trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
17683 takeOrdinalPosition: false, // #2342
17684 kdDimensions: 2,
17685 drawGraph: function() {
17686 if (this.options.lineWidth) {
17687 Series.prototype.drawGraph.call(this);
17688 }
17689 }
17690 });
17691
17692 }(Highcharts));
17693 (function(H) {
17694 /**
17695 * (c) 2010-2016 Torstein Honsi
17696 *
17697 * License: www.highcharts.com/license
17698 */
17699 'use strict';
17700 var addEvent = H.addEvent,
17701 arrayMax = H.arrayMax,
17702 defined = H.defined,
17703 each = H.each,
17704 extend = H.extend,
17705 format = H.format,
17706 map = H.map,
17707 merge = H.merge,
17708 noop = H.noop,
17709 pick = H.pick,
17710 relativeLength = H.relativeLength,
17711 Series = H.Series,
17712 seriesTypes = H.seriesTypes,
17713 stableSort = H.stableSort,
17714 stop = H.stop;
17715
17716
17717 /**
17718 * Generatl distribution algorithm for distributing labels of differing size along a
17719 * confined length in two dimensions. The algorithm takes an array of objects containing
17720 * a size, a target and a rank. It will place the labels as close as possible to their
17721 * targets, skipping the lowest ranked labels if necessary.
17722 */
17723 H.distribute = function(boxes, len) {
17724
17725 var i,
17726 overlapping = true,
17727 origBoxes = boxes, // Original array will be altered with added .pos
17728 restBoxes = [], // The outranked overshoot
17729 box,
17730 target,
17731 total = 0;
17732
17733 function sortByTarget(a, b) {
17734 return a.target - b.target;
17735 }
17736
17737 // If the total size exceeds the len, remove those boxes with the lowest rank
17738 i = boxes.length;
17739 while (i--) {
17740 total += boxes[i].size;
17741 }
17742
17743 // Sort by rank, then slice away overshoot
17744 if (total > len) {
17745 stableSort(boxes, function(a, b) {
17746 return (b.rank || 0) - (a.rank || 0);
17747 });
17748 i = 0;
17749 total = 0;
17750 while (total <= len) {
17751 total += boxes[i].size;
17752 i++;
17753 }
17754 restBoxes = boxes.splice(i - 1, boxes.length);
17755 }
17756
17757 // Order by target
17758 stableSort(boxes, sortByTarget);
17759
17760
17761 // So far we have been mutating the original array. Now
17762 // create a copy with target arrays
17763 boxes = map(boxes, function(box) {
17764 return {
17765 size: box.size,
17766 targets: [box.target]
17767 };
17768 });
17769
17770 while (overlapping) {
17771 // Initial positions: target centered in box
17772 i = boxes.length;
17773 while (i--) {
17774 box = boxes[i];
17775 // Composite box, average of targets
17776 target = (Math.min.apply(0, box.targets) + Math.max.apply(0, box.targets)) / 2;
17777 box.pos = Math.min(Math.max(0, target - box.size / 2), len - box.size);
17778 }
17779
17780 // Detect overlap and join boxes
17781 i = boxes.length;
17782 overlapping = false;
17783 while (i--) {
17784 if (i > 0 && boxes[i - 1].pos + boxes[i - 1].size > boxes[i].pos) { // Overlap
17785 boxes[i - 1].size += boxes[i].size; // Add this size to the previous box
17786 boxes[i - 1].targets = boxes[i - 1].targets.concat(boxes[i].targets);
17787
17788 // Overlapping right, push left
17789 if (boxes[i - 1].pos + boxes[i - 1].size > len) {
17790 boxes[i - 1].pos = len - boxes[i - 1].size;
17791 }
17792 boxes.splice(i, 1); // Remove this item
17793 overlapping = true;
17794 }
17795 }
17796 }
17797
17798 // Now the composite boxes are placed, we need to put the original boxes within them
17799 i = 0;
17800 each(boxes, function(box) {
17801 var posInCompositeBox = 0;
17802 each(box.targets, function() {
17803 origBoxes[i].pos = box.pos + posInCompositeBox;
17804 posInCompositeBox += origBoxes[i].size;
17805 i++;
17806 });
17807 });
17808
17809 // Add the rest (hidden) boxes and sort by target
17810 origBoxes.push.apply(origBoxes, restBoxes);
17811 stableSort(origBoxes, sortByTarget);
17812 };
17813
17814
17815 /**
17816 * Draw the data labels
17817 */
17818 Series.prototype.drawDataLabels = function() {
17819
17820 var series = this,
17821 seriesOptions = series.options,
17822 options = seriesOptions.dataLabels,
17823 points = series.points,
17824 pointOptions,
17825 generalOptions,
17826 hasRendered = series.hasRendered || 0,
17827 str,
17828 dataLabelsGroup,
17829 defer = pick(options.defer, true),
17830 renderer = series.chart.renderer;
17831
17832 if (options.enabled || series._hasPointLabels) {
17833
17834 // Process default alignment of data labels for columns
17835 if (series.dlProcessOptions) {
17836 series.dlProcessOptions(options);
17837 }
17838
17839 // Create a separate group for the data labels to avoid rotation
17840 dataLabelsGroup = series.plotGroup(
17841 'dataLabelsGroup',
17842 'data-labels',
17843 defer && !hasRendered ? 'hidden' : 'visible', // #5133
17844 options.zIndex || 6
17845 );
17846
17847 if (defer) {
17848 dataLabelsGroup.attr({
17849 opacity: +hasRendered
17850 }); // #3300
17851 if (!hasRendered) {
17852 addEvent(series, 'afterAnimate', function() {
17853 if (series.visible) { // #2597, #3023, #3024
17854 dataLabelsGroup.show(true);
17855 }
17856 dataLabelsGroup[seriesOptions.animation ? 'animate' : 'attr']({
17857 opacity: 1
17858 }, {
17859 duration: 200
17860 });
17861 });
17862 }
17863 }
17864
17865 // Make the labels for each point
17866 generalOptions = options;
17867 each(points, function(point) {
17868
17869 var enabled,
17870 dataLabel = point.dataLabel,
17871 labelConfig,
17872 attr,
17873 name,
17874 rotation,
17875 connector = point.connector,
17876 isNew = true,
17877 style,
17878 moreStyle = {};
17879
17880 // Determine if each data label is enabled
17881 pointOptions = point.dlOptions || (point.options && point.options.dataLabels); // dlOptions is used in treemaps
17882 enabled = pick(pointOptions && pointOptions.enabled, generalOptions.enabled) && point.y !== null; // #2282, #4641
17883
17884
17885 // If the point is outside the plot area, destroy it. #678, #820
17886 if (dataLabel && !enabled) {
17887 point.dataLabel = dataLabel.destroy();
17888
17889 // Individual labels are disabled if the are explicitly disabled
17890 // in the point options, or if they fall outside the plot area.
17891 } else if (enabled) {
17892
17893 // Create individual options structure that can be extended without
17894 // affecting others
17895 options = merge(generalOptions, pointOptions);
17896 style = options.style;
17897
17898 rotation = options.rotation;
17899
17900 // Get the string
17901 labelConfig = point.getLabelConfig();
17902 str = options.format ?
17903 format(options.format, labelConfig) :
17904 options.formatter.call(labelConfig, options);
17905
17906
17907 // Determine the color
17908 style.color = pick(options.color, style.color, series.color, '#000000');
17909
17910
17911 // update existing label
17912 if (dataLabel) {
17913
17914 if (defined(str)) {
17915 dataLabel
17916 .attr({
17917 text: str
17918 });
17919 isNew = false;
17920
17921 } else { // #1437 - the label is shown conditionally
17922 point.dataLabel = dataLabel = dataLabel.destroy();
17923 if (connector) {
17924 point.connector = connector.destroy();
17925 }
17926 }
17927
17928 // create new label
17929 } else if (defined(str)) {
17930 attr = {
17931 //align: align,
17932
17933 fill: options.backgroundColor,
17934 stroke: options.borderColor,
17935 'stroke-width': options.borderWidth,
17936
17937 r: options.borderRadius || 0,
17938 rotation: rotation,
17939 padding: options.padding,
17940 zIndex: 1
17941 };
17942
17943
17944 // Get automated contrast color
17945 if (style.color === 'contrast') {
17946 moreStyle.color = options.inside || options.distance < 0 || !!seriesOptions.stacking ?
17947 renderer.getContrast(point.color || series.color) :
17948 '#000000';
17949 }
17950
17951 if (seriesOptions.cursor) {
17952 moreStyle.cursor = seriesOptions.cursor;
17953 }
17954
17955
17956
17957 // Remove unused attributes (#947)
17958 for (name in attr) {
17959 if (attr[name] === undefined) {
17960 delete attr[name];
17961 }
17962 }
17963
17964 dataLabel = point.dataLabel = renderer[rotation ? 'text' : 'label']( // labels don't support rotation
17965 str,
17966 0, -9999,
17967 options.shape,
17968 null,
17969 null,
17970 options.useHTML,
17971 null,
17972 'data-label'
17973 )
17974 .attr(attr);
17975
17976 dataLabel.addClass('highcharts-data-label-color-' + point.colorIndex + ' ' + (options.className || ''));
17977
17978
17979 // Styles must be applied before add in order to read text bounding box
17980 dataLabel.css(extend(style, moreStyle));
17981
17982
17983 dataLabel.add(dataLabelsGroup);
17984
17985
17986 dataLabel.shadow(options.shadow);
17987
17988
17989
17990 }
17991
17992 if (dataLabel) {
17993 // Now the data label is created and placed at 0,0, so we need to align it
17994 series.alignDataLabel(point, dataLabel, options, null, isNew);
17995 }
17996 }
17997 });
17998 }
17999 };
18000
18001 /**
18002 * Align each individual data label
18003 */
18004 Series.prototype.alignDataLabel = function(point, dataLabel, options, alignTo, isNew) {
18005 var chart = this.chart,
18006 inverted = chart.inverted,
18007 plotX = pick(point.plotX, -9999),
18008 plotY = pick(point.plotY, -9999),
18009 bBox = dataLabel.getBBox(),
18010 fontSize,
18011 baseline,
18012 rotation = options.rotation,
18013 normRotation,
18014 negRotation,
18015 align = options.align,
18016 rotCorr, // rotation correction
18017 // Math.round for rounding errors (#2683), alignTo to allow column labels (#2700)
18018 visible = this.visible && (point.series.forceDL || chart.isInsidePlot(plotX, Math.round(plotY), inverted) ||
18019 (alignTo && chart.isInsidePlot(plotX, inverted ? alignTo.x + 1 : alignTo.y + alignTo.height - 1, inverted))),
18020 alignAttr, // the final position;
18021 justify = pick(options.overflow, 'justify') === 'justify';
18022
18023 if (visible) {
18024
18025
18026 fontSize = options.style.fontSize;
18027
18028
18029 baseline = chart.renderer.fontMetrics(fontSize, dataLabel).b;
18030
18031 // The alignment box is a singular point
18032 alignTo = extend({
18033 x: inverted ? chart.plotWidth - plotY : plotX,
18034 y: Math.round(inverted ? chart.plotHeight - plotX : plotY),
18035 width: 0,
18036 height: 0
18037 }, alignTo);
18038
18039 // Add the text size for alignment calculation
18040 extend(options, {
18041 width: bBox.width,
18042 height: bBox.height
18043 });
18044
18045 // Allow a hook for changing alignment in the last moment, then do the alignment
18046 if (rotation) {
18047 justify = false; // Not supported for rotated text
18048 rotCorr = chart.renderer.rotCorr(baseline, rotation); // #3723
18049 alignAttr = {
18050 x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x,
18051 y: alignTo.y + options.y + {
18052 top: 0,
18053 middle: 0.5,
18054 bottom: 1
18055 }[options.verticalAlign] * alignTo.height
18056 };
18057 dataLabel[isNew ? 'attr' : 'animate'](alignAttr)
18058 .attr({ // #3003
18059 align: align
18060 });
18061
18062 // Compensate for the rotated label sticking out on the sides
18063 normRotation = (rotation + 720) % 360;
18064 negRotation = normRotation > 180 && normRotation < 360;
18065
18066 if (align === 'left') {
18067 alignAttr.y -= negRotation ? bBox.height : 0;
18068 } else if (align === 'center') {
18069 alignAttr.x -= bBox.width / 2;
18070 alignAttr.y -= bBox.height / 2;
18071 } else if (align === 'right') {
18072 alignAttr.x -= bBox.width;
18073 alignAttr.y -= negRotation ? 0 : bBox.height;
18074 }
18075
18076
18077 } else {
18078 dataLabel.align(options, null, alignTo);
18079 alignAttr = dataLabel.alignAttr;
18080 }
18081
18082 // Handle justify or crop
18083 if (justify) {
18084 this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew);
18085
18086 // Now check that the data label is within the plot area
18087 } else if (pick(options.crop, true)) {
18088 visible = chart.isInsidePlot(alignAttr.x, alignAttr.y) && chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height);
18089 }
18090
18091 // When we're using a shape, make it possible with a connector or an arrow pointing to thie point
18092 if (options.shape && !rotation) {
18093 dataLabel.attr({
18094 anchorX: point.plotX,
18095 anchorY: point.plotY
18096 });
18097 }
18098 }
18099
18100 // Show or hide based on the final aligned position
18101 if (!visible) {
18102 stop(dataLabel);
18103 dataLabel.attr({
18104 y: -9999
18105 });
18106 dataLabel.placed = false; // don't animate back in
18107 }
18108
18109 };
18110
18111 /**
18112 * If data labels fall partly outside the plot area, align them back in, in a way that
18113 * doesn't hide the point.
18114 */
18115 Series.prototype.justifyDataLabel = function(dataLabel, options, alignAttr, bBox, alignTo, isNew) {
18116 var chart = this.chart,
18117 align = options.align,
18118 verticalAlign = options.verticalAlign,
18119 off,
18120 justified,
18121 padding = dataLabel.box ? 0 : (dataLabel.padding || 0);
18122
18123 // Off left
18124 off = alignAttr.x + padding;
18125 if (off < 0) {
18126 if (align === 'right') {
18127 options.align = 'left';
18128 } else {
18129 options.x = -off;
18130 }
18131 justified = true;
18132 }
18133
18134 // Off right
18135 off = alignAttr.x + bBox.width - padding;
18136 if (off > chart.plotWidth) {
18137 if (align === 'left') {
18138 options.align = 'right';
18139 } else {
18140 options.x = chart.plotWidth - off;
18141 }
18142 justified = true;
18143 }
18144
18145 // Off top
18146 off = alignAttr.y + padding;
18147 if (off < 0) {
18148 if (verticalAlign === 'bottom') {
18149 options.verticalAlign = 'top';
18150 } else {
18151 options.y = -off;
18152 }
18153 justified = true;
18154 }
18155
18156 // Off bottom
18157 off = alignAttr.y + bBox.height - padding;
18158 if (off > chart.plotHeight) {
18159 if (verticalAlign === 'top') {
18160 options.verticalAlign = 'bottom';
18161 } else {
18162 options.y = chart.plotHeight - off;
18163 }
18164 justified = true;
18165 }
18166
18167 if (justified) {
18168 dataLabel.placed = !isNew;
18169 dataLabel.align(options, null, alignTo);
18170 }
18171 };
18172
18173 /**
18174 * Override the base drawDataLabels method by pie specific functionality
18175 */
18176 if (seriesTypes.pie) {
18177 seriesTypes.pie.prototype.drawDataLabels = function() {
18178 var series = this,
18179 data = series.data,
18180 point,
18181 chart = series.chart,
18182 options = series.options.dataLabels,
18183 connectorPadding = pick(options.connectorPadding, 10),
18184 connectorWidth = pick(options.connectorWidth, 1),
18185 plotWidth = chart.plotWidth,
18186 plotHeight = chart.plotHeight,
18187 connector,
18188 distanceOption = options.distance,
18189 seriesCenter = series.center,
18190 radius = seriesCenter[2] / 2,
18191 centerY = seriesCenter[1],
18192 outside = distanceOption > 0,
18193 dataLabel,
18194 dataLabelWidth,
18195 labelPos,
18196 labelHeight,
18197 halves = [ // divide the points into right and left halves for anti collision
18198 [], // right
18199 [] // left
18200 ],
18201 x,
18202 y,
18203 visibility,
18204 j,
18205 overflow = [0, 0, 0, 0]; // top, right, bottom, left
18206
18207 // get out if not enabled
18208 if (!series.visible || (!options.enabled && !series._hasPointLabels)) {
18209 return;
18210 }
18211
18212 // run parent method
18213 Series.prototype.drawDataLabels.apply(series);
18214
18215 each(data, function(point) {
18216 if (point.dataLabel && point.visible) { // #407, #2510
18217
18218 // Arrange points for detection collision
18219 halves[point.half].push(point);
18220
18221 // Reset positions (#4905)
18222 point.dataLabel._pos = null;
18223 }
18224 });
18225
18226 /* Loop over the points in each half, starting from the top and bottom
18227 * of the pie to detect overlapping labels.
18228 */
18229 each(halves, function(points, i) {
18230
18231 var top,
18232 bottom,
18233 length = points.length,
18234 positions,
18235 naturalY,
18236 size;
18237
18238 if (!length) {
18239 return;
18240 }
18241
18242 // Sort by angle
18243 series.sortByAngle(points, i - 0.5);
18244
18245 // Only do anti-collision when we are outside the pie and have connectors (#856)
18246 if (distanceOption > 0) {
18247 top = Math.max(0, centerY - radius - distanceOption);
18248 bottom = Math.min(centerY + radius + distanceOption, chart.plotHeight);
18249 positions = map(points, function(point) {
18250 if (point.dataLabel) {
18251 size = point.dataLabel.getBBox().height || 21;
18252 return {
18253 target: point.labelPos[1] - top + size / 2,
18254 size: size,
18255 rank: point.y
18256 };
18257 }
18258 });
18259 H.distribute(positions, bottom + size - top);
18260 }
18261
18262 // now the used slots are sorted, fill them up sequentially
18263 for (j = 0; j < length; j++) {
18264
18265 point = points[j];
18266 labelPos = point.labelPos;
18267 dataLabel = point.dataLabel;
18268 visibility = point.visible === false ? 'hidden' : 'inherit';
18269 naturalY = labelPos[1];
18270
18271 if (positions) {
18272 if (positions[j].pos === undefined) {
18273 visibility = 'hidden';
18274 } else {
18275 labelHeight = positions[j].size;
18276 y = top + positions[j].pos;
18277 }
18278
18279 } else {
18280 y = naturalY;
18281 }
18282
18283 // get the x - use the natural x position for labels near the top and bottom, to prevent the top
18284 // and botton slice connectors from touching each other on either side
18285 if (options.justify) {
18286 x = seriesCenter[0] + (i ? -1 : 1) * (radius + distanceOption);
18287 } else {
18288 x = series.getX(y < top + 2 || y > bottom - 2 ? naturalY : y, i);
18289 }
18290
18291
18292 // Record the placement and visibility
18293 dataLabel._attr = {
18294 visibility: visibility,
18295 align: labelPos[6]
18296 };
18297 dataLabel._pos = {
18298 x: x + options.x +
18299 ({
18300 left: connectorPadding,
18301 right: -connectorPadding
18302 }[labelPos[6]] || 0),
18303 y: y + options.y - 10 // 10 is for the baseline (label vs text)
18304 };
18305 labelPos.x = x;
18306 labelPos.y = y;
18307
18308
18309 // Detect overflowing data labels
18310 if (series.options.size === null) {
18311 dataLabelWidth = dataLabel.width;
18312 // Overflow left
18313 if (x - dataLabelWidth < connectorPadding) {
18314 overflow[3] = Math.max(Math.round(dataLabelWidth - x + connectorPadding), overflow[3]);
18315
18316 // Overflow right
18317 } else if (x + dataLabelWidth > plotWidth - connectorPadding) {
18318 overflow[1] = Math.max(Math.round(x + dataLabelWidth - plotWidth + connectorPadding), overflow[1]);
18319 }
18320
18321 // Overflow top
18322 if (y - labelHeight / 2 < 0) {
18323 overflow[0] = Math.max(Math.round(-y + labelHeight / 2), overflow[0]);
18324
18325 // Overflow left
18326 } else if (y + labelHeight / 2 > plotHeight) {
18327 overflow[2] = Math.max(Math.round(y + labelHeight / 2 - plotHeight), overflow[2]);
18328 }
18329 }
18330 } // for each point
18331 }); // for each half
18332
18333 // Do not apply the final placement and draw the connectors until we have verified
18334 // that labels are not spilling over.
18335 if (arrayMax(overflow) === 0 || this.verifyDataLabelOverflow(overflow)) {
18336
18337 // Place the labels in the final position
18338 this.placeDataLabels();
18339
18340 // Draw the connectors
18341 if (outside && connectorWidth) {
18342 each(this.points, function(point) {
18343 var isNew;
18344
18345 connector = point.connector;
18346 dataLabel = point.dataLabel;
18347
18348 if (dataLabel && dataLabel._pos && point.visible) {
18349 visibility = dataLabel._attr.visibility;
18350
18351 isNew = !connector;
18352
18353 if (isNew) {
18354 point.connector = connector = chart.renderer.path()
18355 .addClass('highcharts-data-label-connector highcharts-color-' + point.colorIndex)
18356 .add(series.dataLabelsGroup);
18357
18358
18359 connector.attr({
18360 'stroke-width': connectorWidth,
18361 'stroke': options.connectorColor || point.color || '#666666'
18362 });
18363
18364 }
18365 connector[isNew ? 'attr' : 'animate']({
18366 d: series.connectorPath(point.labelPos)
18367 });
18368 connector.attr('visibility', visibility);
18369
18370 } else if (connector) {
18371 point.connector = connector.destroy();
18372 }
18373 });
18374 }
18375 }
18376 };
18377
18378 /**
18379 * Extendable method for getting the path of the connector between the data label
18380 * and the pie slice.
18381 */
18382 seriesTypes.pie.prototype.connectorPath = function(labelPos) {
18383 var x = labelPos.x,
18384 y = labelPos.y;
18385 return pick(this.options.softConnector, true) ? [
18386 'M',
18387 x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
18388 'C',
18389 x, y, // first break, next to the label
18390 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5],
18391 labelPos[2], labelPos[3], // second break
18392 'L',
18393 labelPos[4], labelPos[5] // base
18394 ] : [
18395 'M',
18396 x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
18397 'L',
18398 labelPos[2], labelPos[3], // second break
18399 'L',
18400 labelPos[4], labelPos[5] // base
18401 ];
18402 };
18403
18404 /**
18405 * Perform the final placement of the data labels after we have verified that they
18406 * fall within the plot area.
18407 */
18408 seriesTypes.pie.prototype.placeDataLabels = function() {
18409 each(this.points, function(point) {
18410 var dataLabel = point.dataLabel,
18411 _pos;
18412
18413 if (dataLabel && point.visible) {
18414 _pos = dataLabel._pos;
18415 if (_pos) {
18416 dataLabel.attr(dataLabel._attr);
18417 dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos);
18418 dataLabel.moved = true;
18419 } else if (dataLabel) {
18420 dataLabel.attr({
18421 y: -9999
18422 });
18423 }
18424 }
18425 });
18426 };
18427
18428 seriesTypes.pie.prototype.alignDataLabel = noop;
18429
18430 /**
18431 * Verify whether the data labels are allowed to draw, or we should run more translation and data
18432 * label positioning to keep them inside the plot area. Returns true when data labels are ready
18433 * to draw.
18434 */
18435 seriesTypes.pie.prototype.verifyDataLabelOverflow = function(overflow) {
18436
18437 var center = this.center,
18438 options = this.options,
18439 centerOption = options.center,
18440 minSize = options.minSize || 80,
18441 newSize = minSize,
18442 ret;
18443
18444 // Handle horizontal size and center
18445 if (centerOption[0] !== null) { // Fixed center
18446 newSize = Math.max(center[2] - Math.max(overflow[1], overflow[3]), minSize);
18447
18448 } else { // Auto center
18449 newSize = Math.max(
18450 center[2] - overflow[1] - overflow[3], // horizontal overflow
18451 minSize
18452 );
18453 center[0] += (overflow[3] - overflow[1]) / 2; // horizontal center
18454 }
18455
18456 // Handle vertical size and center
18457 if (centerOption[1] !== null) { // Fixed center
18458 newSize = Math.max(Math.min(newSize, center[2] - Math.max(overflow[0], overflow[2])), minSize);
18459
18460 } else { // Auto center
18461 newSize = Math.max(
18462 Math.min(
18463 newSize,
18464 center[2] - overflow[0] - overflow[2] // vertical overflow
18465 ),
18466 minSize
18467 );
18468 center[1] += (overflow[0] - overflow[2]) / 2; // vertical center
18469 }
18470
18471 // If the size must be decreased, we need to run translate and drawDataLabels again
18472 if (newSize < center[2]) {
18473 center[2] = newSize;
18474 center[3] = Math.min(relativeLength(options.innerSize || 0, newSize), newSize); // #3632
18475 this.translate(center);
18476
18477 if (this.drawDataLabels) {
18478 this.drawDataLabels();
18479 }
18480 // Else, return true to indicate that the pie and its labels is within the plot area
18481 } else {
18482 ret = true;
18483 }
18484 return ret;
18485 };
18486 }
18487
18488 if (seriesTypes.column) {
18489
18490 /**
18491 * Override the basic data label alignment by adjusting for the position of the column
18492 */
18493 seriesTypes.column.prototype.alignDataLabel = function(point, dataLabel, options, alignTo, isNew) {
18494 var inverted = this.chart.inverted,
18495 series = point.series,
18496 dlBox = point.dlBox || point.shapeArgs, // data label box for alignment
18497 below = pick(point.below, point.plotY > pick(this.translatedThreshold, series.yAxis.len)), // point.below is used in range series
18498 inside = pick(options.inside, !!this.options.stacking), // draw it inside the box?
18499 overshoot;
18500
18501 // Align to the column itself, or the top of it
18502 if (dlBox) { // Area range uses this method but not alignTo
18503 alignTo = merge(dlBox);
18504
18505 if (alignTo.y < 0) {
18506 alignTo.height += alignTo.y;
18507 alignTo.y = 0;
18508 }
18509 overshoot = alignTo.y + alignTo.height - series.yAxis.len;
18510 if (overshoot > 0) {
18511 alignTo.height -= overshoot;
18512 }
18513
18514 if (inverted) {
18515 alignTo = {
18516 x: series.yAxis.len - alignTo.y - alignTo.height,
18517 y: series.xAxis.len - alignTo.x - alignTo.width,
18518 width: alignTo.height,
18519 height: alignTo.width
18520 };
18521 }
18522
18523 // Compute the alignment box
18524 if (!inside) {
18525 if (inverted) {
18526 alignTo.x += below ? 0 : alignTo.width;
18527 alignTo.width = 0;
18528 } else {
18529 alignTo.y += below ? alignTo.height : 0;
18530 alignTo.height = 0;
18531 }
18532 }
18533 }
18534
18535
18536 // When alignment is undefined (typically columns and bars), display the individual
18537 // point below or above the point depending on the threshold
18538 options.align = pick(
18539 options.align, !inverted || inside ? 'center' : below ? 'right' : 'left'
18540 );
18541 options.verticalAlign = pick(
18542 options.verticalAlign,
18543 inverted || inside ? 'middle' : below ? 'top' : 'bottom'
18544 );
18545
18546 // Call the parent method
18547 Series.prototype.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew);
18548 };
18549 }
18550
18551 }(Highcharts));
18552 (function(H) {
18553 /**
18554 * (c) 2009-2016 Torstein Honsi
18555 *
18556 * License: www.highcharts.com/license
18557 */
18558 'use strict';
18559 /**
18560 * Highcharts module to hide overlapping data labels. This module is included in Highcharts.
18561 */
18562 var Chart = H.Chart,
18563 each = H.each,
18564 pick = H.pick,
18565 addEvent = H.addEvent;
18566
18567 // Collect potensial overlapping data labels. Stack labels probably don't need to be
18568 // considered because they are usually accompanied by data labels that lie inside the columns.
18569 Chart.prototype.callbacks.push(function(chart) {
18570 function collectAndHide() {
18571 var labels = [];
18572
18573 each(chart.series, function(series) {
18574 var dlOptions = series.options.dataLabels,
18575 collections = series.dataLabelCollections || ['dataLabel']; // Range series have two collections
18576 if ((dlOptions.enabled || series._hasPointLabels) && !dlOptions.allowOverlap && series.visible) { // #3866
18577 each(collections, function(coll) {
18578 each(series.points, function(point) {
18579 if (point[coll]) {
18580 point[coll].labelrank = pick(point.labelrank, point.shapeArgs && point.shapeArgs.height); // #4118
18581 labels.push(point[coll]);
18582 }
18583 });
18584 });
18585 }
18586 });
18587 chart.hideOverlappingLabels(labels);
18588 }
18589
18590 // Do it now ...
18591 collectAndHide();
18592
18593 // ... and after each chart redraw
18594 addEvent(chart, 'redraw', collectAndHide);
18595
18596 });
18597
18598 /**
18599 * Hide overlapping labels. Labels are moved and faded in and out on zoom to provide a smooth
18600 * visual imression.
18601 */
18602 Chart.prototype.hideOverlappingLabels = function(labels) {
18603
18604 var len = labels.length,
18605 label,
18606 i,
18607 j,
18608 label1,
18609 label2,
18610 isIntersecting,
18611 pos1,
18612 pos2,
18613 parent1,
18614 parent2,
18615 padding,
18616 intersectRect = function(x1, y1, w1, h1, x2, y2, w2, h2) {
18617 return !(
18618 x2 > x1 + w1 ||
18619 x2 + w2 < x1 ||
18620 y2 > y1 + h1 ||
18621 y2 + h2 < y1
18622 );
18623 };
18624
18625 // Mark with initial opacity
18626 for (i = 0; i < len; i++) {
18627 label = labels[i];
18628 if (label) {
18629 label.oldOpacity = label.opacity;
18630 label.newOpacity = 1;
18631 }
18632 }
18633
18634 // Prevent a situation in a gradually rising slope, that each label
18635 // will hide the previous one because the previous one always has
18636 // lower rank.
18637 labels.sort(function(a, b) {
18638 return (b.labelrank || 0) - (a.labelrank || 0);
18639 });
18640
18641 // Detect overlapping labels
18642 for (i = 0; i < len; i++) {
18643 label1 = labels[i];
18644
18645 for (j = i + 1; j < len; ++j) {
18646 label2 = labels[j];
18647 if (label1 && label2 && label1.placed && label2.placed && label1.newOpacity !== 0 && label2.newOpacity !== 0) {
18648 pos1 = label1.alignAttr;
18649 pos2 = label2.alignAttr;
18650 parent1 = label1.parentGroup; // Different panes have different positions
18651 parent2 = label2.parentGroup;
18652 padding = 2 * (label1.box ? 0 : label1.padding); // Substract the padding if no background or border (#4333)
18653 isIntersecting = intersectRect(
18654 pos1.x + parent1.translateX,
18655 pos1.y + parent1.translateY,
18656 label1.width - padding,
18657 label1.height - padding,
18658 pos2.x + parent2.translateX,
18659 pos2.y + parent2.translateY,
18660 label2.width - padding,
18661 label2.height - padding
18662 );
18663
18664 if (isIntersecting) {
18665 (label1.labelrank < label2.labelrank ? label1 : label2).newOpacity = 0;
18666 }
18667 }
18668 }
18669 }
18670
18671 // Hide or show
18672 each(labels, function(label) {
18673 var complete,
18674 newOpacity;
18675
18676 if (label) {
18677 newOpacity = label.newOpacity;
18678
18679 if (label.oldOpacity !== newOpacity && label.placed) {
18680
18681 // Make sure the label is completely hidden to avoid catching clicks (#4362)
18682 if (newOpacity) {
18683 label.show(true);
18684 } else {
18685 complete = function() {
18686 label.hide();
18687 };
18688 }
18689
18690 // Animate or set the opacity
18691 label.alignAttr.opacity = newOpacity;
18692 label[label.isOld ? 'animate' : 'attr'](label.alignAttr, null, complete);
18693
18694 }
18695 label.isOld = true;
18696 }
18697 });
18698 };
18699
18700 }(Highcharts));
18701 (function(H) {
18702 /**
18703 * (c) 2010-2016 Torstein Honsi
18704 *
18705 * License: www.highcharts.com/license
18706 */
18707 'use strict';
18708 var Axis = H.Axis,
18709 each = H.each,
18710 pick = H.pick,
18711 wrap = H.wrap;
18712 /**
18713 * Override to use the extreme coordinates from the SVG shape, not the
18714 * data values
18715 */
18716 wrap(Axis.prototype, 'getSeriesExtremes', function(proceed) {
18717 var isXAxis = this.isXAxis,
18718 dataMin,
18719 dataMax,
18720 xData = [],
18721 useMapGeometry;
18722
18723 // Remove the xData array and cache it locally so that the proceed method doesn't use it
18724 if (isXAxis) {
18725 each(this.series, function(series, i) {
18726 if (series.useMapGeometry) {
18727 xData[i] = series.xData;
18728 series.xData = [];
18729 }
18730 });
18731 }
18732
18733 // Call base to reach normal cartesian series (like mappoint)
18734 proceed.call(this);
18735
18736 // Run extremes logic for map and mapline
18737 if (isXAxis) {
18738 dataMin = pick(this.dataMin, Number.MAX_VALUE);
18739 dataMax = pick(this.dataMax, -Number.MAX_VALUE);
18740 each(this.series, function(series, i) {
18741 if (series.useMapGeometry) {
18742 dataMin = Math.min(dataMin, pick(series.minX, dataMin));
18743 dataMax = Math.max(dataMax, pick(series.maxX, dataMin));
18744 series.xData = xData[i]; // Reset xData array
18745 useMapGeometry = true;
18746 }
18747 });
18748 if (useMapGeometry) {
18749 this.dataMin = dataMin;
18750 this.dataMax = dataMax;
18751 }
18752 }
18753 });
18754
18755 /**
18756 * Override axis translation to make sure the aspect ratio is always kept
18757 */
18758 wrap(Axis.prototype, 'setAxisTranslation', function(proceed) {
18759 var chart = this.chart,
18760 mapRatio,
18761 plotRatio = chart.plotWidth / chart.plotHeight,
18762 adjustedAxisLength,
18763 xAxis = chart.xAxis[0],
18764 padAxis,
18765 fixTo,
18766 fixDiff,
18767 preserveAspectRatio;
18768
18769
18770 // Run the parent method
18771 proceed.call(this);
18772
18773 // Check for map-like series
18774 if (this.coll === 'yAxis' && xAxis.transA !== undefined) {
18775 each(this.series, function(series) {
18776 if (series.preserveAspectRatio) {
18777 preserveAspectRatio = true;
18778 }
18779 });
18780 }
18781
18782 // On Y axis, handle both
18783 if (preserveAspectRatio) {
18784
18785 // Use the same translation for both axes
18786 this.transA = xAxis.transA = Math.min(this.transA, xAxis.transA);
18787
18788 mapRatio = plotRatio / ((xAxis.max - xAxis.min) / (this.max - this.min));
18789
18790 // What axis to pad to put the map in the middle
18791 padAxis = mapRatio < 1 ? this : xAxis;
18792
18793 // Pad it
18794 adjustedAxisLength = (padAxis.max - padAxis.min) * padAxis.transA;
18795 padAxis.pixelPadding = padAxis.len - adjustedAxisLength;
18796 padAxis.minPixelPadding = padAxis.pixelPadding / 2;
18797
18798 fixTo = padAxis.fixTo;
18799 if (fixTo) {
18800 fixDiff = fixTo[1] - padAxis.toValue(fixTo[0], true);
18801 fixDiff *= padAxis.transA;
18802 if (Math.abs(fixDiff) > padAxis.minPixelPadding || (padAxis.min === padAxis.dataMin && padAxis.max === padAxis.dataMax)) { // zooming out again, keep within restricted area
18803 fixDiff = 0;
18804 }
18805 padAxis.minPixelPadding -= fixDiff;
18806 }
18807 }
18808 });
18809
18810 /**
18811 * Override Axis.render in order to delete the fixTo prop
18812 */
18813 wrap(Axis.prototype, 'render', function(proceed) {
18814 proceed.call(this);
18815 this.fixTo = null;
18816 });
18817
18818 }(Highcharts));
18819 (function(H) {
18820 /**
18821 * (c) 2010-2016 Torstein Honsi
18822 *
18823 * License: www.highcharts.com/license
18824 */
18825 'use strict';
18826 var Axis = H.Axis,
18827 Chart = H.Chart,
18828 color = H.color,
18829 ColorAxis,
18830 each = H.each,
18831 extend = H.extend,
18832 isNumber = H.isNumber,
18833 Legend = H.Legend,
18834 LegendSymbolMixin = H.LegendSymbolMixin,
18835 noop = H.noop,
18836 merge = H.merge,
18837 pick = H.pick,
18838 wrap = H.wrap;
18839
18840 /**
18841 * The ColorAxis object for inclusion in gradient legends
18842 */
18843 ColorAxis = H.ColorAxis = function() {
18844 this.init.apply(this, arguments);
18845 };
18846 extend(ColorAxis.prototype, Axis.prototype);
18847 extend(ColorAxis.prototype, {
18848 defaultColorAxisOptions: {
18849 lineWidth: 0,
18850 minPadding: 0,
18851 maxPadding: 0,
18852 gridLineWidth: 1,
18853 tickPixelInterval: 72,
18854 startOnTick: true,
18855 endOnTick: true,
18856 offset: 0,
18857 marker: {
18858 animation: {
18859 duration: 50
18860 },
18861 width: 0.01,
18862
18863 color: '#999999'
18864
18865 },
18866 labels: {
18867 overflow: 'justify'
18868 },
18869 minColor: '#e6ebf5',
18870 maxColor: '#003399',
18871 tickLength: 5,
18872 showInLegend: true
18873 },
18874 init: function(chart, userOptions) {
18875 var horiz = chart.options.legend.layout !== 'vertical',
18876 options;
18877
18878 this.coll = 'colorAxis';
18879
18880 // Build the options
18881 options = merge(this.defaultColorAxisOptions, {
18882 side: horiz ? 2 : 1,
18883 reversed: !horiz
18884 }, userOptions, {
18885 opposite: !horiz,
18886 showEmpty: false,
18887 title: null
18888 });
18889
18890 Axis.prototype.init.call(this, chart, options);
18891
18892 // Base init() pushes it to the xAxis array, now pop it again
18893 //chart[this.isXAxis ? 'xAxis' : 'yAxis'].pop();
18894
18895 // Prepare data classes
18896 if (userOptions.dataClasses) {
18897 this.initDataClasses(userOptions);
18898 }
18899 this.initStops(userOptions);
18900
18901 // Override original axis properties
18902 this.horiz = horiz;
18903 this.zoomEnabled = false;
18904
18905 // Add default values
18906 this.defaultLegendLength = 200;
18907 },
18908
18909 /*
18910 * Return an intermediate color between two colors, according to pos where 0
18911 * is the from color and 1 is the to color.
18912 * NOTE: Changes here should be copied
18913 * to the same function in drilldown.src.js and solid-gauge-src.js.
18914 */
18915 tweenColors: function(from, to, pos) {
18916 // Check for has alpha, because rgba colors perform worse due to lack of
18917 // support in WebKit.
18918 var hasAlpha,
18919 ret;
18920
18921 // Unsupported color, return to-color (#3920)
18922 if (!to.rgba.length || !from.rgba.length) {
18923 ret = to.input || 'none';
18924
18925 // Interpolate
18926 } else {
18927 from = from.rgba;
18928 to = to.rgba;
18929 hasAlpha = (to[3] !== 1 || from[3] !== 1);
18930 ret = (hasAlpha ? 'rgba(' : 'rgb(') +
18931 Math.round(to[0] + (from[0] - to[0]) * (1 - pos)) + ',' +
18932 Math.round(to[1] + (from[1] - to[1]) * (1 - pos)) + ',' +
18933 Math.round(to[2] + (from[2] - to[2]) * (1 - pos)) +
18934 (hasAlpha ? (',' + (to[3] + (from[3] - to[3]) * (1 - pos))) : '') + ')';
18935 }
18936 return ret;
18937 },
18938
18939 initDataClasses: function(userOptions) {
18940 var axis = this,
18941 chart = this.chart,
18942 dataClasses,
18943 colorCounter = 0,
18944 colorCount = chart.options.chart.colorCount,
18945 options = this.options,
18946 len = userOptions.dataClasses.length;
18947 this.dataClasses = dataClasses = [];
18948 this.legendItems = [];
18949
18950 each(userOptions.dataClasses, function(dataClass, i) {
18951 var colors;
18952
18953 dataClass = merge(dataClass);
18954 dataClasses.push(dataClass);
18955 if (!dataClass.color) {
18956 if (options.dataClassColor === 'category') {
18957
18958 colors = chart.options.colors;
18959 colorCount = colors.length;
18960 dataClass.color = colors[colorCounter];
18961
18962 dataClass.colorIndex = colorCounter;
18963
18964 // increase and loop back to zero
18965 colorCounter++;
18966 if (colorCounter === colorCount) {
18967 colorCounter = 0;
18968 }
18969 } else {
18970 dataClass.color = axis.tweenColors(
18971 color(options.minColor),
18972 color(options.maxColor),
18973 len < 2 ? 0.5 : i / (len - 1) // #3219
18974 );
18975 }
18976 }
18977 });
18978 },
18979
18980 initStops: function(userOptions) {
18981 this.stops = userOptions.stops || [
18982 [0, this.options.minColor],
18983 [1, this.options.maxColor]
18984 ];
18985 each(this.stops, function(stop) {
18986 stop.color = color(stop[1]);
18987 });
18988 },
18989
18990 /**
18991 * Extend the setOptions method to process extreme colors and color
18992 * stops.
18993 */
18994 setOptions: function(userOptions) {
18995 Axis.prototype.setOptions.call(this, userOptions);
18996
18997 this.options.crosshair = this.options.marker;
18998 },
18999
19000 setAxisSize: function() {
19001 var symbol = this.legendSymbol,
19002 chart = this.chart,
19003 legendOptions = chart.options.legend || {},
19004 x,
19005 y,
19006 width,
19007 height;
19008
19009 if (symbol) {
19010 this.left = x = symbol.attr('x');
19011 this.top = y = symbol.attr('y');
19012 this.width = width = symbol.attr('width');
19013 this.height = height = symbol.attr('height');
19014 this.right = chart.chartWidth - x - width;
19015 this.bottom = chart.chartHeight - y - height;
19016
19017 this.len = this.horiz ? width : height;
19018 this.pos = this.horiz ? x : y;
19019 } else {
19020 // Fake length for disabled legend to avoid tick issues and such (#5205)
19021 this.len = (this.horiz ? legendOptions.symbolWidth : legendOptions.symbolHeight) || this.defaultLegendLength;
19022 }
19023 },
19024
19025 /**
19026 * Translate from a value to a color
19027 */
19028 toColor: function(value, point) {
19029 var pos,
19030 stops = this.stops,
19031 from,
19032 to,
19033 color,
19034 dataClasses = this.dataClasses,
19035 dataClass,
19036 i;
19037
19038 if (dataClasses) {
19039 i = dataClasses.length;
19040 while (i--) {
19041 dataClass = dataClasses[i];
19042 from = dataClass.from;
19043 to = dataClass.to;
19044 if ((from === undefined || value >= from) && (to === undefined || value <= to)) {
19045 color = dataClass.color;
19046 if (point) {
19047 point.dataClass = i;
19048 point.colorIndex = dataClass.colorIndex;
19049 }
19050 break;
19051 }
19052 }
19053
19054 } else {
19055
19056 if (this.isLog) {
19057 value = this.val2lin(value);
19058 }
19059 pos = 1 - ((this.max - value) / ((this.max - this.min) || 1));
19060 i = stops.length;
19061 while (i--) {
19062 if (pos > stops[i][0]) {
19063 break;
19064 }
19065 }
19066 from = stops[i] || stops[i + 1];
19067 to = stops[i + 1] || from;
19068
19069 // The position within the gradient
19070 pos = 1 - (to[0] - pos) / ((to[0] - from[0]) || 1);
19071
19072 color = this.tweenColors(
19073 from.color,
19074 to.color,
19075 pos
19076 );
19077 }
19078 return color;
19079 },
19080
19081 /**
19082 * Override the getOffset method to add the whole axis groups inside the legend.
19083 */
19084 getOffset: function() {
19085 var group = this.legendGroup,
19086 sideOffset = this.chart.axisOffset[this.side];
19087
19088 if (group) {
19089
19090 // Hook for the getOffset method to add groups to this parent group
19091 this.axisParent = group;
19092
19093 // Call the base
19094 Axis.prototype.getOffset.call(this);
19095
19096 // First time only
19097 if (!this.added) {
19098
19099 this.added = true;
19100
19101 this.labelLeft = 0;
19102 this.labelRight = this.width;
19103 }
19104 // Reset it to avoid color axis reserving space
19105 this.chart.axisOffset[this.side] = sideOffset;
19106 }
19107 },
19108
19109 /**
19110 * Create the color gradient
19111 */
19112 setLegendColor: function() {
19113 var grad,
19114 horiz = this.horiz,
19115 options = this.options,
19116 reversed = this.reversed,
19117 one = reversed ? 1 : 0,
19118 zero = reversed ? 0 : 1;
19119
19120 grad = horiz ? [one, 0, zero, 0] : [0, zero, 0, one]; // #3190
19121 this.legendColor = {
19122 linearGradient: {
19123 x1: grad[0],
19124 y1: grad[1],
19125 x2: grad[2],
19126 y2: grad[3]
19127 },
19128 stops: options.stops || [
19129 [0, options.minColor],
19130 [1, options.maxColor]
19131 ]
19132 };
19133 },
19134
19135 /**
19136 * The color axis appears inside the legend and has its own legend symbol
19137 */
19138 drawLegendSymbol: function(legend, item) {
19139 var padding = legend.padding,
19140 legendOptions = legend.options,
19141 horiz = this.horiz,
19142 width = pick(legendOptions.symbolWidth, horiz ? this.defaultLegendLength : 12),
19143 height = pick(legendOptions.symbolHeight, horiz ? 12 : this.defaultLegendLength),
19144 labelPadding = pick(legendOptions.labelPadding, horiz ? 16 : 30),
19145 itemDistance = pick(legendOptions.itemDistance, 10);
19146
19147 this.setLegendColor();
19148
19149 // Create the gradient
19150 item.legendSymbol = this.chart.renderer.rect(
19151 0,
19152 legend.baseline - 11,
19153 width,
19154 height
19155 ).attr({
19156 zIndex: 1
19157 }).add(item.legendGroup);
19158
19159 // Set how much space this legend item takes up
19160 this.legendItemWidth = width + padding + (horiz ? itemDistance : labelPadding);
19161 this.legendItemHeight = height + padding + (horiz ? labelPadding : 0);
19162 },
19163 /**
19164 * Fool the legend
19165 */
19166 setState: noop,
19167 visible: true,
19168 setVisible: noop,
19169 getSeriesExtremes: function() {
19170 var series;
19171 if (this.series.length) {
19172 series = this.series[0];
19173 this.dataMin = series.valueMin;
19174 this.dataMax = series.valueMax;
19175 }
19176 },
19177 drawCrosshair: function(e, point) {
19178 var plotX = point && point.plotX,
19179 plotY = point && point.plotY,
19180 crossPos,
19181 axisPos = this.pos,
19182 axisLen = this.len;
19183
19184 if (point) {
19185 crossPos = this.toPixels(point[point.series.colorKey]);
19186 if (crossPos < axisPos) {
19187 crossPos = axisPos - 2;
19188 } else if (crossPos > axisPos + axisLen) {
19189 crossPos = axisPos + axisLen + 2;
19190 }
19191
19192 point.plotX = crossPos;
19193 point.plotY = this.len - crossPos;
19194 Axis.prototype.drawCrosshair.call(this, e, point);
19195 point.plotX = plotX;
19196 point.plotY = plotY;
19197
19198 if (this.cross) {
19199 this.cross
19200 .addClass('highcharts-coloraxis-marker')
19201 .add(this.legendGroup);
19202
19203
19204 this.cross.attr({
19205 fill: this.crosshair.color
19206 });
19207
19208
19209 }
19210 }
19211 },
19212 getPlotLinePath: function(a, b, c, d, pos) {
19213 return isNumber(pos) ? // crosshairs only // #3969 pos can be 0 !!
19214 (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']) :
19215 Axis.prototype.getPlotLinePath.call(this, a, b, c, d);
19216 },
19217
19218 update: function(newOptions, redraw) {
19219 var chart = this.chart,
19220 legend = chart.legend;
19221
19222 each(this.series, function(series) {
19223 series.isDirtyData = true; // Needed for Axis.update when choropleth colors change
19224 });
19225
19226 // When updating data classes, destroy old items and make sure new ones are created (#3207)
19227 if (newOptions.dataClasses && legend.allItems) {
19228 each(legend.allItems, function(item) {
19229 if (item.isDataClass) {
19230 item.legendGroup.destroy();
19231 }
19232 });
19233 chart.isDirtyLegend = true;
19234 }
19235
19236 // Keep the options structure updated for export. Unlike xAxis and yAxis, the colorAxis is
19237 // not an array. (#3207)
19238 chart.options[this.coll] = merge(this.userOptions, newOptions);
19239
19240 Axis.prototype.update.call(this, newOptions, redraw);
19241 if (this.legendItem) {
19242 this.setLegendColor();
19243 legend.colorizeItem(this, true);
19244 }
19245 },
19246
19247 /**
19248 * Get the legend item symbols for data classes
19249 */
19250 getDataClassLegendSymbols: function() {
19251 var axis = this,
19252 chart = this.chart,
19253 legendItems = this.legendItems,
19254 legendOptions = chart.options.legend,
19255 valueDecimals = legendOptions.valueDecimals,
19256 valueSuffix = legendOptions.valueSuffix || '',
19257 name;
19258
19259 if (!legendItems.length) {
19260 each(this.dataClasses, function(dataClass, i) {
19261 var vis = true,
19262 from = dataClass.from,
19263 to = dataClass.to;
19264
19265 // Assemble the default name. This can be overridden by legend.options.labelFormatter
19266 name = '';
19267 if (from === undefined) {
19268 name = '< ';
19269 } else if (to === undefined) {
19270 name = '> ';
19271 }
19272 if (from !== undefined) {
19273 name += H.numberFormat(from, valueDecimals) + valueSuffix;
19274 }
19275 if (from !== undefined && to !== undefined) {
19276 name += ' - ';
19277 }
19278 if (to !== undefined) {
19279 name += H.numberFormat(to, valueDecimals) + valueSuffix;
19280 }
19281 // Add a mock object to the legend items
19282 legendItems.push(extend({
19283 chart: chart,
19284 name: name,
19285 options: {},
19286 drawLegendSymbol: LegendSymbolMixin.drawRectangle,
19287 visible: true,
19288 setState: noop,
19289 isDataClass: true,
19290 setVisible: function() {
19291 vis = this.visible = !vis;
19292 each(axis.series, function(series) {
19293 each(series.points, function(point) {
19294 if (point.dataClass === i) {
19295 point.setVisible(vis);
19296 }
19297 });
19298 });
19299
19300 chart.legend.colorizeItem(this, vis);
19301 }
19302 }, dataClass));
19303 });
19304 }
19305 return legendItems;
19306 },
19307 name: '' // Prevents 'undefined' in legend in IE8
19308 });
19309
19310 /**
19311 * Handle animation of the color attributes directly
19312 */
19313 each(['fill', 'stroke'], function(prop) {
19314 H.Fx.prototype[prop + 'Setter'] = function() {
19315 this.elem.attr(prop, ColorAxis.prototype.tweenColors(color(this.start), color(this.end), this.pos));
19316 };
19317 });
19318
19319 /**
19320 * Extend the chart getAxes method to also get the color axis
19321 */
19322 wrap(Chart.prototype, 'getAxes', function(proceed) {
19323
19324 var options = this.options,
19325 colorAxisOptions = options.colorAxis;
19326
19327 proceed.call(this);
19328
19329 this.colorAxis = [];
19330 if (colorAxisOptions) {
19331 new ColorAxis(this, colorAxisOptions); // eslint-disable-line no-new
19332 }
19333 });
19334
19335
19336 /**
19337 * Wrap the legend getAllItems method to add the color axis. This also removes the
19338 * axis' own series to prevent them from showing up individually.
19339 */
19340 wrap(Legend.prototype, 'getAllItems', function(proceed) {
19341 var allItems = [],
19342 colorAxis = this.chart.colorAxis[0];
19343
19344 if (colorAxis && colorAxis.options) {
19345 if (colorAxis.options.showInLegend) {
19346 // Data classes
19347 if (colorAxis.options.dataClasses) {
19348 allItems = allItems.concat(colorAxis.getDataClassLegendSymbols());
19349 // Gradient legend
19350 } else {
19351 // Add this axis on top
19352 allItems.push(colorAxis);
19353 }
19354 }
19355
19356 // Don't add the color axis' series
19357 each(colorAxis.series, function(series) {
19358 series.options.showInLegend = false;
19359 });
19360 }
19361
19362 return allItems.concat(proceed.call(this));
19363 });
19364
19365 wrap(Legend.prototype, 'colorizeItem', function(proceed, item, visible) {
19366 proceed.call(this, item, visible);
19367 if (visible && item.legendColor) {
19368 item.legendSymbol.attr({
19369 fill: item.legendColor
19370 });
19371 }
19372 });
19373
19374 }(Highcharts));
19375 (function(H) {
19376 /**
19377 * (c) 2010-2016 Torstein Honsi
19378 *
19379 * License: www.highcharts.com/license
19380 */
19381 'use strict';
19382 var defined = H.defined,
19383 each = H.each,
19384 noop = H.noop,
19385 seriesTypes = H.seriesTypes;
19386
19387 /**
19388 * Mixin for maps and heatmaps
19389 */
19390 H.colorPointMixin = {
19391 /**
19392 * Set the visibility of a single point
19393 */
19394 setVisible: function(vis) {
19395 var point = this,
19396 method = vis ? 'show' : 'hide';
19397
19398 // Show and hide associated elements
19399 each(['graphic', 'dataLabel'], function(key) {
19400 if (point[key]) {
19401 point[key][method]();
19402 }
19403 });
19404 }
19405 };
19406
19407 H.colorSeriesMixin = {
19408 pointArrayMap: ['value'],
19409 axisTypes: ['xAxis', 'yAxis', 'colorAxis'],
19410 optionalAxis: 'colorAxis',
19411 trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
19412 getSymbol: noop,
19413 parallelArrays: ['x', 'y', 'value'],
19414 colorKey: 'value',
19415
19416
19417 pointAttribs: seriesTypes.column.prototype.pointAttribs,
19418
19419
19420 /**
19421 * In choropleth maps, the color is a result of the value, so this needs translation too
19422 */
19423 translateColors: function() {
19424 var series = this,
19425 nullColor = this.options.nullColor,
19426 colorAxis = this.colorAxis,
19427 colorKey = this.colorKey;
19428
19429 each(this.data, function(point) {
19430 var value = point[colorKey],
19431 color;
19432
19433 color = point.options.color ||
19434 (value === null ? nullColor : (colorAxis && value !== undefined) ? colorAxis.toColor(value, point) : point.color || series.color);
19435
19436 if (color) {
19437 point.color = color;
19438 }
19439 });
19440 },
19441
19442 /**
19443 * Get the color attibutes to apply on the graphic
19444 */
19445 colorAttribs: function(point) {
19446 var ret = {};
19447 if (defined(point.color)) {
19448 ret[this.colorProp || 'fill'] = point.color;
19449 }
19450 return ret;
19451 }
19452 };
19453
19454 }(Highcharts));
19455 (function(H) {
19456 /**
19457 * (c) 2010-2016 Torstein Honsi
19458 *
19459 * License: www.highcharts.com/license
19460 */
19461 'use strict';
19462 var color = H.color,
19463 ColorAxis = H.ColorAxis,
19464 colorPointMixin = H.colorPointMixin,
19465 colorSeriesMixin = H.colorSeriesMixin,
19466 doc = H.doc,
19467 each = H.each,
19468 extend = H.extend,
19469 isNumber = H.isNumber,
19470 LegendSymbolMixin = H.LegendSymbolMixin,
19471 map = H.map,
19472 merge = H.merge,
19473 noop = H.noop,
19474 pick = H.pick,
19475 isArray = H.isArray,
19476 Point = H.Point,
19477 Series = H.Series,
19478 seriesType = H.seriesType,
19479 seriesTypes = H.seriesTypes,
19480 splat = H.splat;
19481
19482 // The vector-effect attribute is not supported in IE <= 11 (at least), so we need
19483 // diffent logic (#3218)
19484 var supportsVectorEffect = doc.documentElement.style.vectorEffect !== undefined;
19485
19486
19487 /**
19488 * The MapAreaPoint object
19489 */
19490 /**
19491 * Add the map series type
19492 */
19493 seriesType('map', 'scatter', {
19494 allAreas: true,
19495
19496 animation: false, // makes the complex shapes slow
19497 nullColor: '#f7f7f7',
19498 borderColor: '#cccccc',
19499 borderWidth: 1,
19500 marker: null,
19501 stickyTracking: false,
19502 joinBy: 'hc-key',
19503 dataLabels: {
19504 formatter: function() { // #2945
19505 return this.point.value;
19506 },
19507 inside: true, // for the color
19508 verticalAlign: 'middle',
19509 crop: false,
19510 overflow: false,
19511 padding: 0
19512 },
19513 turboThreshold: 0,
19514 tooltip: {
19515 followPointer: true,
19516 pointFormat: '{point.name}: {point.value}<br/>'
19517 },
19518 states: {
19519 normal: {
19520 animation: true
19521 },
19522 hover: {
19523 brightness: 0.2,
19524 halo: null
19525 },
19526 select: {
19527 color: '#cccccc'
19528 }
19529 }
19530
19531 // Prototype members
19532 }, merge(colorSeriesMixin, {
19533 type: 'map',
19534 supportsDrilldown: true,
19535 getExtremesFromAll: true,
19536 useMapGeometry: true, // get axis extremes from paths, not values
19537 forceDL: true,
19538 searchPoint: noop,
19539 directTouch: true, // When tooltip is not shared, this series (and derivatives) requires direct touch/hover. KD-tree does not apply.
19540 preserveAspectRatio: true, // X axis and Y axis must have same translation slope
19541 pointArrayMap: ['value'],
19542 /**
19543 * Get the bounding box of all paths in the map combined.
19544 */
19545 getBox: function(paths) {
19546 var MAX_VALUE = Number.MAX_VALUE,
19547 maxX = -MAX_VALUE,
19548 minX = MAX_VALUE,
19549 maxY = -MAX_VALUE,
19550 minY = MAX_VALUE,
19551 minRange = MAX_VALUE,
19552 xAxis = this.xAxis,
19553 yAxis = this.yAxis,
19554 hasBox;
19555
19556 // Find the bounding box
19557 each(paths || [], function(point) {
19558
19559 if (point.path) {
19560 if (typeof point.path === 'string') {
19561 point.path = H.splitPath(point.path);
19562 }
19563
19564 var path = point.path || [],
19565 i = path.length,
19566 even = false, // while loop reads from the end
19567 pointMaxX = -MAX_VALUE,
19568 pointMinX = MAX_VALUE,
19569 pointMaxY = -MAX_VALUE,
19570 pointMinY = MAX_VALUE,
19571 properties = point.properties;
19572
19573 // The first time a map point is used, analyze its box
19574 if (!point._foundBox) {
19575 while (i--) {
19576 if (isNumber(path[i])) {
19577 if (even) { // even = x
19578 pointMaxX = Math.max(pointMaxX, path[i]);
19579 pointMinX = Math.min(pointMinX, path[i]);
19580 } else { // odd = Y
19581 pointMaxY = Math.max(pointMaxY, path[i]);
19582 pointMinY = Math.min(pointMinY, path[i]);
19583 }
19584 even = !even;
19585 }
19586 }
19587 // Cache point bounding box for use to position data labels, bubbles etc
19588 point._midX = pointMinX + (pointMaxX - pointMinX) *
19589 (point.middleX || (properties && properties['hc-middle-x']) || 0.5); // pick is slower and very marginally needed
19590 point._midY = pointMinY + (pointMaxY - pointMinY) *
19591 (point.middleY || (properties && properties['hc-middle-y']) || 0.5);
19592 point._maxX = pointMaxX;
19593 point._minX = pointMinX;
19594 point._maxY = pointMaxY;
19595 point._minY = pointMinY;
19596 point.labelrank = pick(point.labelrank, (pointMaxX - pointMinX) * (pointMaxY - pointMinY));
19597 point._foundBox = true;
19598 }
19599
19600 maxX = Math.max(maxX, point._maxX);
19601 minX = Math.min(minX, point._minX);
19602 maxY = Math.max(maxY, point._maxY);
19603 minY = Math.min(minY, point._minY);
19604 minRange = Math.min(point._maxX - point._minX, point._maxY - point._minY, minRange);
19605 hasBox = true;
19606 }
19607 });
19608
19609 // Set the box for the whole series
19610 if (hasBox) {
19611 this.minY = Math.min(minY, pick(this.minY, MAX_VALUE));
19612 this.maxY = Math.max(maxY, pick(this.maxY, -MAX_VALUE));
19613 this.minX = Math.min(minX, pick(this.minX, MAX_VALUE));
19614 this.maxX = Math.max(maxX, pick(this.maxX, -MAX_VALUE));
19615
19616 // If no minRange option is set, set the default minimum zooming range to 5 times the
19617 // size of the smallest element
19618 if (xAxis && xAxis.options.minRange === undefined) {
19619 xAxis.minRange = Math.min(5 * minRange, (this.maxX - this.minX) / 5, xAxis.minRange || MAX_VALUE);
19620 }
19621 if (yAxis && yAxis.options.minRange === undefined) {
19622 yAxis.minRange = Math.min(5 * minRange, (this.maxY - this.minY) / 5, yAxis.minRange || MAX_VALUE);
19623 }
19624 }
19625 },
19626
19627 getExtremes: function() {
19628 // Get the actual value extremes for colors
19629 Series.prototype.getExtremes.call(this, this.valueData);
19630
19631 // Recalculate box on updated data
19632 if (this.chart.hasRendered && this.isDirtyData) {
19633 this.getBox(this.options.data);
19634 }
19635
19636 this.valueMin = this.dataMin;
19637 this.valueMax = this.dataMax;
19638
19639 // Extremes for the mock Y axis
19640 this.dataMin = this.minY;
19641 this.dataMax = this.maxY;
19642 },
19643
19644 /**
19645 * Translate the path so that it automatically fits into the plot area box
19646 * @param {Object} path
19647 */
19648 translatePath: function(path) {
19649
19650 var series = this,
19651 even = false, // while loop reads from the end
19652 xAxis = series.xAxis,
19653 yAxis = series.yAxis,
19654 xMin = xAxis.min,
19655 xTransA = xAxis.transA,
19656 xMinPixelPadding = xAxis.minPixelPadding,
19657 yMin = yAxis.min,
19658 yTransA = yAxis.transA,
19659 yMinPixelPadding = yAxis.minPixelPadding,
19660 i,
19661 ret = []; // Preserve the original
19662
19663 // Do the translation
19664 if (path) {
19665 i = path.length;
19666 while (i--) {
19667 if (isNumber(path[i])) {
19668 ret[i] = even ?
19669 (path[i] - xMin) * xTransA + xMinPixelPadding :
19670 (path[i] - yMin) * yTransA + yMinPixelPadding;
19671 even = !even;
19672 } else {
19673 ret[i] = path[i];
19674 }
19675 }
19676 }
19677
19678 return ret;
19679 },
19680
19681 /**
19682 * Extend setData to join in mapData. If the allAreas option is true, all areas
19683 * from the mapData are used, and those that don't correspond to a data value
19684 * are given null values.
19685 */
19686 setData: function(data, redraw, animation, updatePoints) {
19687 var options = this.options,
19688 chartOptions = this.chart.options.chart,
19689 globalMapData = chartOptions && chartOptions.map,
19690 mapData = options.mapData,
19691 joinBy = options.joinBy,
19692 joinByNull = joinBy === null,
19693 pointArrayMap = options.keys || this.pointArrayMap,
19694 dataUsed = [],
19695 mapMap = {},
19696 mapPoint,
19697 transform,
19698 mapTransforms = this.chart.mapTransforms,
19699 props,
19700 i;
19701
19702 // Collect mapData from chart options if not defined on series
19703 if (!mapData && globalMapData) {
19704 mapData = typeof globalMapData === 'string' ? H.maps[globalMapData] : globalMapData;
19705 }
19706
19707 if (joinByNull) {
19708 joinBy = '_i';
19709 }
19710 joinBy = this.joinBy = splat(joinBy);
19711 if (!joinBy[1]) {
19712 joinBy[1] = joinBy[0];
19713 }
19714
19715 // Pick up numeric values, add index
19716 // Convert Array point definitions to objects using pointArrayMap
19717 if (data) {
19718 each(data, function(val, i) {
19719 var ix = 0;
19720 if (isNumber(val)) {
19721 data[i] = {
19722 value: val
19723 };
19724 } else if (isArray(val)) {
19725 data[i] = {};
19726 // Automatically copy first item to hc-key if there is an extra leading string
19727 if (!options.keys && val.length > pointArrayMap.length && typeof val[0] === 'string') {
19728 data[i]['hc-key'] = val[0];
19729 ++ix;
19730 }
19731 // Run through pointArrayMap and what's left of the point data array in parallel, copying over the values
19732 for (var j = 0; j < pointArrayMap.length; ++j, ++ix) {
19733 if (pointArrayMap[j]) {
19734 data[i][pointArrayMap[j]] = val[ix];
19735 }
19736 }
19737 }
19738 if (joinByNull) {
19739 data[i]._i = i;
19740 }
19741 });
19742 }
19743
19744 this.getBox(data);
19745
19746 // Pick up transform definitions for chart
19747 this.chart.mapTransforms = mapTransforms = chartOptions && chartOptions.mapTransforms || mapData && mapData['hc-transform'] || mapTransforms;
19748
19749 // Cache cos/sin of transform rotation angle
19750 if (mapTransforms) {
19751 for (transform in mapTransforms) {
19752 if (mapTransforms.hasOwnProperty(transform) && transform.rotation) {
19753 transform.cosAngle = Math.cos(transform.rotation);
19754 transform.sinAngle = Math.sin(transform.rotation);
19755 }
19756 }
19757 }
19758
19759 if (mapData) {
19760 if (mapData.type === 'FeatureCollection') {
19761 this.mapTitle = mapData.title;
19762 mapData = H.geojson(mapData, this.type, this);
19763 }
19764
19765 this.mapData = mapData;
19766 this.mapMap = {};
19767
19768 for (i = 0; i < mapData.length; i++) {
19769 mapPoint = mapData[i];
19770 props = mapPoint.properties;
19771
19772 mapPoint._i = i;
19773 // Copy the property over to root for faster access
19774 if (joinBy[0] && props && props[joinBy[0]]) {
19775 mapPoint[joinBy[0]] = props[joinBy[0]];
19776 }
19777 mapMap[mapPoint[joinBy[0]]] = mapPoint;
19778 }
19779 this.mapMap = mapMap;
19780
19781 // Registered the point codes that actually hold data
19782 if (data && joinBy[1]) {
19783 each(data, function(point) {
19784 if (mapMap[point[joinBy[1]]]) {
19785 dataUsed.push(mapMap[point[joinBy[1]]]);
19786 }
19787 });
19788 }
19789
19790 if (options.allAreas) {
19791 this.getBox(mapData);
19792 data = data || [];
19793
19794 // Registered the point codes that actually hold data
19795 if (joinBy[1]) {
19796 each(data, function(point) {
19797 dataUsed.push(point[joinBy[1]]);
19798 });
19799 }
19800
19801 // Add those map points that don't correspond to data, which will be drawn as null points
19802 dataUsed = '|' + map(dataUsed, function(point) {
19803 return point && point[joinBy[0]];
19804 }).join('|') + '|'; // String search is faster than array.indexOf
19805
19806 each(mapData, function(mapPoint) {
19807 if (!joinBy[0] || dataUsed.indexOf('|' + mapPoint[joinBy[0]] + '|') === -1) {
19808 data.push(merge(mapPoint, {
19809 value: null
19810 }));
19811 updatePoints = false; // #5050 - adding all areas causes the update optimization of setData to kick in, even though the point order has changed
19812 }
19813 });
19814 } else {
19815 this.getBox(dataUsed); // Issue #4784
19816 }
19817 }
19818 Series.prototype.setData.call(this, data, redraw, animation, updatePoints);
19819 },
19820
19821
19822 /**
19823 * No graph for the map series
19824 */
19825 drawGraph: noop,
19826
19827 /**
19828 * We need the points' bounding boxes in order to draw the data labels, so
19829 * we skip it now and call it from drawPoints instead.
19830 */
19831 drawDataLabels: noop,
19832
19833 /**
19834 * Allow a quick redraw by just translating the area group. Used for zooming and panning
19835 * in capable browsers.
19836 */
19837 doFullTranslate: function() {
19838 return this.isDirtyData || this.chart.isResizing || this.chart.renderer.isVML || !this.baseTrans;
19839 },
19840
19841 /**
19842 * Add the path option for data points. Find the max value for color calculation.
19843 */
19844 translate: function() {
19845 var series = this,
19846 xAxis = series.xAxis,
19847 yAxis = series.yAxis,
19848 doFullTranslate = series.doFullTranslate();
19849
19850 series.generatePoints();
19851
19852 each(series.data, function(point) {
19853
19854 // Record the middle point (loosely based on centroid), determined
19855 // by the middleX and middleY options.
19856 point.plotX = xAxis.toPixels(point._midX, true);
19857 point.plotY = yAxis.toPixels(point._midY, true);
19858
19859 if (doFullTranslate) {
19860
19861 point.shapeType = 'path';
19862 point.shapeArgs = {
19863 d: series.translatePath(point.path)
19864 };
19865 }
19866 });
19867
19868 series.translateColors();
19869 },
19870
19871 /**
19872 * Get presentational attributes
19873 */
19874 pointAttribs: function(point, state) {
19875 var attr = seriesTypes.column.prototype.pointAttribs.call(this, point, state);
19876
19877 // Prevent flickering whan called from setState
19878 if (point.isFading) {
19879 delete attr.fill;
19880 }
19881
19882 // If vector-effect is not supported, we set the stroke-width on the group element
19883 // and let all point graphics inherit. That way we don't have to iterate over all
19884 // points to update the stroke-width on zooming. TODO: Check unstyled
19885 if (supportsVectorEffect) {
19886 attr['vector-effect'] = 'non-scaling-stroke';
19887 } else {
19888 attr['stroke-width'] = 'inherit';
19889 }
19890
19891 return attr;
19892 },
19893
19894 /**
19895 * Use the drawPoints method of column, that is able to handle simple shapeArgs.
19896 * Extend it by assigning the tooltip position.
19897 */
19898 drawPoints: function() {
19899 var series = this,
19900 xAxis = series.xAxis,
19901 yAxis = series.yAxis,
19902 group = series.group,
19903 chart = series.chart,
19904 renderer = chart.renderer,
19905 scaleX,
19906 scaleY,
19907 translateX,
19908 translateY,
19909 baseTrans = this.baseTrans;
19910
19911 // Set a group that handles transform during zooming and panning in order to preserve clipping
19912 // on series.group
19913 if (!series.transformGroup) {
19914 series.transformGroup = renderer.g()
19915 .attr({
19916 scaleX: 1,
19917 scaleY: 1
19918 })
19919 .add(group);
19920 series.transformGroup.survive = true;
19921 }
19922
19923 // Draw the shapes again
19924 if (series.doFullTranslate()) {
19925
19926 // Individual point actions. TODO: Check unstyled.
19927
19928 if (chart.hasRendered) {
19929 each(series.points, function(point) {
19930
19931 // Restore state color on update/redraw (#3529)
19932 if (point.shapeArgs) {
19933 point.shapeArgs.fill = series.pointAttribs(point, point.state).fill;
19934 }
19935 });
19936 }
19937
19938
19939 // Draw them in transformGroup
19940 series.group = series.transformGroup;
19941 seriesTypes.column.prototype.drawPoints.apply(series);
19942 series.group = group; // Reset
19943
19944 // Add class names
19945 each(series.points, function(point) {
19946 if (point.graphic) {
19947 if (point.name) {
19948 point.graphic.addClass('highcharts-name-' + point.name.replace(' ', '-').toLowerCase());
19949 }
19950 if (point.properties && point.properties['hc-key']) {
19951 point.graphic.addClass('highcharts-key-' + point.properties['hc-key'].toLowerCase());
19952 }
19953 }
19954 });
19955
19956 // Set the base for later scale-zooming. The originX and originY properties are the
19957 // axis values in the plot area's upper left corner.
19958 this.baseTrans = {
19959 originX: xAxis.min - xAxis.minPixelPadding / xAxis.transA,
19960 originY: yAxis.min - yAxis.minPixelPadding / yAxis.transA + (yAxis.reversed ? 0 : yAxis.len / yAxis.transA),
19961 transAX: xAxis.transA,
19962 transAY: yAxis.transA
19963 };
19964
19965 // Reset transformation in case we're doing a full translate (#3789)
19966 this.transformGroup.animate({
19967 translateX: 0,
19968 translateY: 0,
19969 scaleX: 1,
19970 scaleY: 1
19971 });
19972
19973 // Just update the scale and transform for better performance
19974 } else {
19975 scaleX = xAxis.transA / baseTrans.transAX;
19976 scaleY = yAxis.transA / baseTrans.transAY;
19977 translateX = xAxis.toPixels(baseTrans.originX, true);
19978 translateY = yAxis.toPixels(baseTrans.originY, true);
19979
19980 // Handle rounding errors in normal view (#3789)
19981 if (scaleX > 0.99 && scaleX < 1.01 && scaleY > 0.99 && scaleY < 1.01) {
19982 scaleX = 1;
19983 scaleY = 1;
19984 translateX = Math.round(translateX);
19985 translateY = Math.round(translateY);
19986 }
19987
19988 this.transformGroup.animate({
19989 translateX: translateX,
19990 translateY: translateY,
19991 scaleX: scaleX,
19992 scaleY: scaleY
19993 });
19994
19995 }
19996
19997 // Set the stroke-width directly on the group element so the children inherit it. We need to use
19998 // setAttribute directly, because the stroke-widthSetter method expects a stroke color also to be
19999 // set.
20000 if (!supportsVectorEffect) {
20001 series.group.element.setAttribute(
20002 'stroke-width',
20003 series.options[
20004 (series.pointAttrToOptions && series.pointAttrToOptions['stroke-width']) || 'borderWidth'
20005 ] / (scaleX || 1)
20006 );
20007 }
20008
20009 this.drawMapDataLabels();
20010
20011
20012 },
20013
20014 /**
20015 * Draw the data labels. Special for maps is the time that the data labels are drawn (after points),
20016 * and the clipping of the dataLabelsGroup.
20017 */
20018 drawMapDataLabels: function() {
20019
20020 Series.prototype.drawDataLabels.call(this);
20021 if (this.dataLabelsGroup) {
20022 this.dataLabelsGroup.clip(this.chart.clipRect);
20023 }
20024 },
20025
20026 /**
20027 * Override render to throw in an async call in IE8. Otherwise it chokes on the US counties demo.
20028 */
20029 render: function() {
20030 var series = this,
20031 render = Series.prototype.render;
20032
20033 // Give IE8 some time to breathe.
20034 if (series.chart.renderer.isVML && series.data.length > 3000) {
20035 setTimeout(function() {
20036 render.call(series);
20037 });
20038 } else {
20039 render.call(series);
20040 }
20041 },
20042
20043 /**
20044 * The initial animation for the map series. By default, animation is disabled.
20045 * Animation of map shapes is not at all supported in VML browsers.
20046 */
20047 animate: function(init) {
20048 var chart = this.chart,
20049 animation = this.options.animation,
20050 group = this.group,
20051 xAxis = this.xAxis,
20052 yAxis = this.yAxis,
20053 left = xAxis.pos,
20054 top = yAxis.pos;
20055
20056 if (chart.renderer.isSVG) {
20057
20058 if (animation === true) {
20059 animation = {
20060 duration: 1000
20061 };
20062 }
20063
20064 // Initialize the animation
20065 if (init) {
20066
20067 // Scale down the group and place it in the center
20068 group.attr({
20069 translateX: left + xAxis.len / 2,
20070 translateY: top + yAxis.len / 2,
20071 scaleX: 0.001, // #1499
20072 scaleY: 0.001
20073 });
20074
20075 // Run the animation
20076 } else {
20077 group.animate({
20078 translateX: left,
20079 translateY: top,
20080 scaleX: 1,
20081 scaleY: 1
20082 }, animation);
20083
20084 // Delete this function to allow it only once
20085 this.animate = null;
20086 }
20087 }
20088 },
20089
20090 /**
20091 * Animate in the new series from the clicked point in the old series.
20092 * Depends on the drilldown.js module
20093 */
20094 animateDrilldown: function(init) {
20095 var toBox = this.chart.plotBox,
20096 level = this.chart.drilldownLevels[this.chart.drilldownLevels.length - 1],
20097 fromBox = level.bBox,
20098 animationOptions = this.chart.options.drilldown.animation,
20099 scale;
20100
20101 if (!init) {
20102
20103 scale = Math.min(fromBox.width / toBox.width, fromBox.height / toBox.height);
20104 level.shapeArgs = {
20105 scaleX: scale,
20106 scaleY: scale,
20107 translateX: fromBox.x,
20108 translateY: fromBox.y
20109 };
20110
20111 each(this.points, function(point) {
20112 if (point.graphic) {
20113 point.graphic
20114 .attr(level.shapeArgs)
20115 .animate({
20116 scaleX: 1,
20117 scaleY: 1,
20118 translateX: 0,
20119 translateY: 0
20120 }, animationOptions);
20121 }
20122 });
20123
20124 this.animate = null;
20125 }
20126
20127 },
20128
20129 drawLegendSymbol: LegendSymbolMixin.drawRectangle,
20130
20131 /**
20132 * When drilling up, pull out the individual point graphics from the lower series
20133 * and animate them into the origin point in the upper series.
20134 */
20135 animateDrillupFrom: function(level) {
20136 seriesTypes.column.prototype.animateDrillupFrom.call(this, level);
20137 },
20138
20139
20140 /**
20141 * When drilling up, keep the upper series invisible until the lower series has
20142 * moved into place
20143 */
20144 animateDrillupTo: function(init) {
20145 seriesTypes.column.prototype.animateDrillupTo.call(this, init);
20146 }
20147
20148 // Point class
20149 }), extend({
20150 /**
20151 * Extend the Point object to split paths
20152 */
20153 applyOptions: function(options, x) {
20154
20155 var point = Point.prototype.applyOptions.call(this, options, x),
20156 series = this.series,
20157 joinBy = series.joinBy,
20158 mapPoint;
20159
20160 if (series.mapData) {
20161 mapPoint = point[joinBy[1]] !== undefined && series.mapMap[point[joinBy[1]]];
20162 if (mapPoint) {
20163 // This applies only to bubbles
20164 if (series.xyFromShape) {
20165 point.x = mapPoint._midX;
20166 point.y = mapPoint._midY;
20167 }
20168 extend(point, mapPoint); // copy over properties
20169 } else {
20170 point.value = point.value || null;
20171 }
20172 }
20173
20174 return point;
20175 },
20176
20177 /**
20178 * Stop the fade-out
20179 */
20180 onMouseOver: function(e) {
20181 clearTimeout(this.colorInterval);
20182 if (this.value !== null) {
20183 Point.prototype.onMouseOver.call(this, e);
20184 } else { //#3401 Tooltip doesn't hide when hovering over null points
20185 this.series.onMouseOut(e);
20186 }
20187 },
20188
20189 // Todo: check unstyled
20190 /**
20191 * Custom animation for tweening out the colors. Animation reduces blinking when hovering
20192 * over islands and coast lines. We run a custom implementation of animation becuase we
20193 * need to be able to run this independently from other animations like zoom redraw. Also,
20194 * adding color animation to the adapters would introduce almost the same amount of code.
20195 */
20196 onMouseOut: function() {
20197 var point = this,
20198 start = +new Date(),
20199 normalColor = color(this.series.pointAttribs(point).fill),
20200 hoverColor = color(this.series.pointAttribs(point, 'hover').fill),
20201 animation = point.series.options.states.normal.animation,
20202 duration = animation && (animation.duration || 500);
20203
20204 if (duration && normalColor.rgba.length === 4 && hoverColor.rgba.length === 4 && point.state !== 'select') {
20205 clearTimeout(point.colorInterval);
20206 point.colorInterval = setInterval(function() {
20207 var pos = (new Date() - start) / duration,
20208 graphic = point.graphic;
20209 if (pos > 1) {
20210 pos = 1;
20211 }
20212 if (graphic) {
20213 graphic.attr('fill', ColorAxis.prototype.tweenColors.call(0, hoverColor, normalColor, pos));
20214 }
20215 if (pos >= 1) {
20216 clearTimeout(point.colorInterval);
20217 }
20218 }, 13);
20219 }
20220 point.isFading = true;
20221 Point.prototype.onMouseOut.call(point);
20222 point.isFading = null;
20223 },
20224
20225
20226 /**
20227 * Zoom the chart to view a specific area point
20228 */
20229 zoomTo: function() {
20230 var point = this,
20231 series = point.series;
20232
20233 series.xAxis.setExtremes(
20234 point._minX,
20235 point._maxX,
20236 false
20237 );
20238 series.yAxis.setExtremes(
20239 point._minY,
20240 point._maxY,
20241 false
20242 );
20243 series.chart.redraw();
20244 }
20245 }, colorPointMixin));
20246
20247 }(Highcharts));
20248 (function(H) {
20249 /**
20250 * (c) 2010-2016 Torstein Honsi
20251 *
20252 * License: www.highcharts.com/license
20253 */
20254 'use strict';
20255 var addEvent = H.addEvent,
20256 Chart = H.Chart,
20257 doc = H.doc,
20258 each = H.each,
20259 extend = H.extend,
20260 merge = H.merge,
20261 pick = H.pick,
20262 wrap = H.wrap;
20263
20264 function stopEvent(e) {
20265 if (e) {
20266 if (e.preventDefault) {
20267 e.preventDefault();
20268 }
20269 if (e.stopPropagation) {
20270 e.stopPropagation();
20271 }
20272 e.cancelBubble = true;
20273 }
20274 }
20275
20276 // Add events to the Chart object itself
20277 extend(Chart.prototype, {
20278 renderMapNavigation: function() {
20279 var chart = this,
20280 options = this.options.mapNavigation,
20281 buttons = options.buttons,
20282 n,
20283 button,
20284 buttonOptions,
20285 attr,
20286 states,
20287 hoverStates,
20288 selectStates,
20289 outerHandler = function(e) {
20290 this.handler.call(chart, e);
20291 stopEvent(e); // Stop default click event (#4444)
20292 };
20293
20294 if (pick(options.enableButtons, options.enabled) && !chart.renderer.forExport) {
20295 chart.mapNavButtons = [];
20296 for (n in buttons) {
20297 if (buttons.hasOwnProperty(n)) {
20298 buttonOptions = merge(options.buttonOptions, buttons[n]);
20299
20300
20301 // Presentational
20302 attr = buttonOptions.theme;
20303 attr.style = merge(buttonOptions.theme.style, buttonOptions.style); // #3203
20304 states = attr.states;
20305 hoverStates = states && states.hover;
20306 selectStates = states && states.select;
20307
20308
20309 button = chart.renderer.button(
20310 buttonOptions.text,
20311 0,
20312 0,
20313 outerHandler,
20314 attr,
20315 hoverStates,
20316 selectStates,
20317 0,
20318 n === 'zoomIn' ? 'topbutton' : 'bottombutton'
20319 )
20320 .addClass('highcharts-map-navigation')
20321 .attr({
20322 width: buttonOptions.width,
20323 height: buttonOptions.height,
20324 title: chart.options.lang[n],
20325 padding: buttonOptions.padding,
20326 zIndex: 5
20327 })
20328 .add();
20329 button.handler = buttonOptions.onclick;
20330 button.align(extend(buttonOptions, {
20331 width: button.width,
20332 height: 2 * button.height
20333 }), null, buttonOptions.alignTo);
20334 addEvent(button.element, 'dblclick', stopEvent); // Stop double click event (#4444)
20335 chart.mapNavButtons.push(button);
20336 }
20337 }
20338 }
20339 },
20340
20341 /**
20342 * Fit an inner box to an outer. If the inner box overflows left or right, align it to the sides of the
20343 * outer. If it overflows both sides, fit it within the outer. This is a pattern that occurs more places
20344 * in Highcharts, perhaps it should be elevated to a common utility function.
20345 */
20346 fitToBox: function(inner, outer) {
20347 each([
20348 ['x', 'width'],
20349 ['y', 'height']
20350 ], function(dim) {
20351 var pos = dim[0],
20352 size = dim[1];
20353
20354 if (inner[pos] + inner[size] > outer[pos] + outer[size]) { // right overflow
20355 if (inner[size] > outer[size]) { // the general size is greater, fit fully to outer
20356 inner[size] = outer[size];
20357 inner[pos] = outer[pos];
20358 } else { // align right
20359 inner[pos] = outer[pos] + outer[size] - inner[size];
20360 }
20361 }
20362 if (inner[size] > outer[size]) {
20363 inner[size] = outer[size];
20364 }
20365 if (inner[pos] < outer[pos]) {
20366 inner[pos] = outer[pos];
20367 }
20368 });
20369
20370
20371 return inner;
20372 },
20373
20374 /**
20375 * Zoom the map in or out by a certain amount. Less than 1 zooms in, greater than 1 zooms out.
20376 */
20377 mapZoom: function(howMuch, centerXArg, centerYArg, mouseX, mouseY) {
20378 /*if (this.isMapZooming) {
20379 this.mapZoomQueue = arguments;
20380 return;
20381 }*/
20382
20383 var chart = this,
20384 xAxis = chart.xAxis[0],
20385 xRange = xAxis.max - xAxis.min,
20386 centerX = pick(centerXArg, xAxis.min + xRange / 2),
20387 newXRange = xRange * howMuch,
20388 yAxis = chart.yAxis[0],
20389 yRange = yAxis.max - yAxis.min,
20390 centerY = pick(centerYArg, yAxis.min + yRange / 2),
20391 newYRange = yRange * howMuch,
20392 fixToX = mouseX ? ((mouseX - xAxis.pos) / xAxis.len) : 0.5,
20393 fixToY = mouseY ? ((mouseY - yAxis.pos) / yAxis.len) : 0.5,
20394 newXMin = centerX - newXRange * fixToX,
20395 newYMin = centerY - newYRange * fixToY,
20396 newExt = chart.fitToBox({
20397 x: newXMin,
20398 y: newYMin,
20399 width: newXRange,
20400 height: newYRange
20401 }, {
20402 x: xAxis.dataMin,
20403 y: yAxis.dataMin,
20404 width: xAxis.dataMax - xAxis.dataMin,
20405 height: yAxis.dataMax - yAxis.dataMin
20406 }),
20407 zoomOut = newExt.x <= xAxis.dataMin &&
20408 newExt.width >= xAxis.dataMax - xAxis.dataMin &&
20409 newExt.y <= yAxis.dataMin &&
20410 newExt.height >= yAxis.dataMax - yAxis.dataMin;
20411
20412 // When mousewheel zooming, fix the point under the mouse
20413 if (mouseX) {
20414 xAxis.fixTo = [mouseX - xAxis.pos, centerXArg];
20415 }
20416 if (mouseY) {
20417 yAxis.fixTo = [mouseY - yAxis.pos, centerYArg];
20418 }
20419
20420 // Zoom
20421 if (howMuch !== undefined && !zoomOut) {
20422 xAxis.setExtremes(newExt.x, newExt.x + newExt.width, false);
20423 yAxis.setExtremes(newExt.y, newExt.y + newExt.height, false);
20424
20425 // Reset zoom
20426 } else {
20427 xAxis.setExtremes(undefined, undefined, false);
20428 yAxis.setExtremes(undefined, undefined, false);
20429 }
20430
20431 // Prevent zooming until this one is finished animating
20432 /*chart.holdMapZoom = true;
20433 setTimeout(function () {
20434 chart.holdMapZoom = false;
20435 }, 200);*/
20436 /*delay = animation ? animation.duration || 500 : 0;
20437 if (delay) {
20438 chart.isMapZooming = true;
20439 setTimeout(function () {
20440 chart.isMapZooming = false;
20441 if (chart.mapZoomQueue) {
20442 chart.mapZoom.apply(chart, chart.mapZoomQueue);
20443 }
20444 chart.mapZoomQueue = null;
20445 }, delay);
20446 }*/
20447
20448 chart.redraw();
20449 }
20450 });
20451
20452 /**
20453 * Extend the Chart.render method to add zooming and panning
20454 */
20455 wrap(Chart.prototype, 'render', function(proceed) {
20456 var chart = this,
20457 mapNavigation = chart.options.mapNavigation;
20458
20459 // Render the plus and minus buttons. Doing this before the shapes makes getBBox much quicker, at least in Chrome.
20460 chart.renderMapNavigation();
20461
20462 proceed.call(chart);
20463
20464 // Add the double click event
20465 if (pick(mapNavigation.enableDoubleClickZoom, mapNavigation.enabled) || mapNavigation.enableDoubleClickZoomTo) {
20466 addEvent(chart.container, 'dblclick', function(e) {
20467 chart.pointer.onContainerDblClick(e);
20468 });
20469 }
20470
20471 // Add the mousewheel event
20472 if (pick(mapNavigation.enableMouseWheelZoom, mapNavigation.enabled)) {
20473 addEvent(chart.container, doc.onmousewheel === undefined ? 'DOMMouseScroll' : 'mousewheel', function(e) {
20474 chart.pointer.onContainerMouseWheel(e);
20475 stopEvent(e); // Issue #5011, returning false from non-jQuery event does not prevent default
20476 return false;
20477 });
20478 }
20479 });
20480
20481 }(Highcharts));
20482 (function(H) {
20483 /**
20484 * (c) 2010-2016 Torstein Honsi
20485 *
20486 * License: www.highcharts.com/license
20487 */
20488 'use strict';
20489 var extend = H.extend,
20490 pick = H.pick,
20491 Pointer = H.Pointer,
20492 wrap = H.wrap;
20493
20494 // Extend the Pointer
20495 extend(Pointer.prototype, {
20496
20497 /**
20498 * The event handler for the doubleclick event
20499 */
20500 onContainerDblClick: function(e) {
20501 var chart = this.chart;
20502
20503 e = this.normalize(e);
20504
20505 if (chart.options.mapNavigation.enableDoubleClickZoomTo) {
20506 if (chart.pointer.inClass(e.target, 'highcharts-tracker') && chart.hoverPoint) {
20507 chart.hoverPoint.zoomTo();
20508 }
20509 } else if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
20510 chart.mapZoom(
20511 0.5,
20512 chart.xAxis[0].toValue(e.chartX),
20513 chart.yAxis[0].toValue(e.chartY),
20514 e.chartX,
20515 e.chartY
20516 );
20517 }
20518 },
20519
20520 /**
20521 * The event handler for the mouse scroll event
20522 */
20523 onContainerMouseWheel: function(e) {
20524 var chart = this.chart,
20525 delta;
20526
20527 e = this.normalize(e);
20528
20529 // Firefox uses e.detail, WebKit and IE uses wheelDelta
20530 delta = e.detail || -(e.wheelDelta / 120);
20531 if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
20532 chart.mapZoom(
20533 Math.pow(chart.options.mapNavigation.mouseWheelSensitivity, delta),
20534 chart.xAxis[0].toValue(e.chartX),
20535 chart.yAxis[0].toValue(e.chartY),
20536 e.chartX,
20537 e.chartY
20538 );
20539 }
20540 }
20541 });
20542
20543 // Implement the pinchType option
20544 wrap(Pointer.prototype, 'zoomOption', function(proceed) {
20545
20546 var mapNavigation = this.chart.options.mapNavigation;
20547
20548 proceed.apply(this, [].slice.call(arguments, 1));
20549
20550 // Pinch status
20551 if (pick(mapNavigation.enableTouchZoom, mapNavigation.enabled)) {
20552 this.pinchX = this.pinchHor = this.pinchY = this.pinchVert = this.hasZoom = true;
20553 }
20554 });
20555
20556 // Extend the pinchTranslate method to preserve fixed ratio when zooming
20557 wrap(Pointer.prototype, 'pinchTranslate', function(proceed, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) {
20558 var xBigger;
20559 proceed.call(this, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
20560
20561 // Keep ratio
20562 if (this.chart.options.chart.type === 'map' && this.hasZoom) {
20563 xBigger = transform.scaleX > transform.scaleY;
20564 this.pinchTranslateDirection(!xBigger,
20565 pinchDown,
20566 touches,
20567 transform,
20568 selectionMarker,
20569 clip,
20570 lastValidTouch,
20571 xBigger ? transform.scaleX : transform.scaleY
20572 );
20573 }
20574 });
20575
20576 }(Highcharts));
20577 (function(H) {
20578 /**
20579 * (c) 2010-2016 Torstein Honsi
20580 *
20581 * License: www.highcharts.com/license
20582 */
20583 'use strict';
20584 var seriesType = H.seriesType,
20585 seriesTypes = H.seriesTypes;
20586
20587 // The mapline series type
20588 seriesType('mapline', 'map', {
20589
20590 lineWidth: 1,
20591 fillColor: 'none'
20592
20593 }, {
20594 type: 'mapline',
20595 colorProp: 'stroke',
20596
20597 pointAttrToOptions: {
20598 'stroke': 'color',
20599 'stroke-width': 'lineWidth'
20600 },
20601 /**
20602 * Get presentational attributes
20603 */
20604 pointAttribs: function(point, state) {
20605 var attr = seriesTypes.map.prototype.pointAttribs.call(this, point, state);
20606
20607 // The difference from a map series is that the stroke takes the point color
20608 attr.fill = this.options.fillColor;
20609
20610 return attr;
20611 },
20612
20613 drawLegendSymbol: seriesTypes.line.prototype.drawLegendSymbol
20614 });
20615
20616 }(Highcharts));
20617 (function(H) {
20618 /**
20619 * (c) 2010-2016 Torstein Honsi
20620 *
20621 * License: www.highcharts.com/license
20622 */
20623 'use strict';
20624 var merge = H.merge,
20625 Point = H.Point,
20626 seriesType = H.seriesType;
20627
20628 // The mappoint series type
20629 seriesType('mappoint', 'scatter', {
20630 dataLabels: {
20631 enabled: true,
20632 formatter: function() { // #2945
20633 return this.point.name;
20634 },
20635 crop: false,
20636 defer: false,
20637 overflow: false,
20638 style: {
20639 color: '#000000'
20640 }
20641 }
20642
20643 // Prototype members
20644 }, {
20645 type: 'mappoint',
20646 forceDL: true
20647
20648 // Point class
20649 }, {
20650 applyOptions: function(options, x) {
20651 var mergedOptions = options.lat !== undefined && options.lon !== undefined ? merge(options, this.series.chart.fromLatLonToPoint(options)) : options;
20652 return Point.prototype.applyOptions.call(this, mergedOptions, x);
20653 }
20654 });
20655
20656 }(Highcharts));
20657 (function(H) {
20658 /**
20659 * (c) 2010-2016 Torstein Honsi
20660 *
20661 * License: www.highcharts.com/license
20662 */
20663 'use strict';
20664 var arrayMax = H.arrayMax,
20665 arrayMin = H.arrayMin,
20666 Axis = H.Axis,
20667 color = H.color,
20668 each = H.each,
20669 isNumber = H.isNumber,
20670 noop = H.noop,
20671 pick = H.pick,
20672 pInt = H.pInt,
20673 Point = H.Point,
20674 Series = H.Series,
20675 seriesType = H.seriesType,
20676 seriesTypes = H.seriesTypes;
20677
20678 /* ****************************************************************************
20679 * Start Bubble series code *
20680 *****************************************************************************/
20681
20682 seriesType('bubble', 'scatter', {
20683 dataLabels: {
20684 formatter: function() { // #2945
20685 return this.point.z;
20686 },
20687 inside: true,
20688 verticalAlign: 'middle'
20689 },
20690 // displayNegative: true,
20691 marker: {
20692
20693 // fillOpacity: 0.5,
20694 lineColor: null, // inherit from series.color
20695 lineWidth: 1,
20696
20697 // Avoid offset in Point.setState
20698 radius: null,
20699 states: {
20700 hover: {
20701 radiusPlus: 0
20702 }
20703 }
20704 },
20705 minSize: 8,
20706 maxSize: '20%',
20707 // negativeColor: null,
20708 // sizeBy: 'area'
20709 softThreshold: false,
20710 states: {
20711 hover: {
20712 halo: {
20713 size: 5
20714 }
20715 }
20716 },
20717 tooltip: {
20718 pointFormat: '({point.x}, {point.y}), Size: {point.z}'
20719 },
20720 turboThreshold: 0,
20721 zThreshold: 0,
20722 zoneAxis: 'z'
20723
20724 // Prototype members
20725 }, {
20726 pointArrayMap: ['y', 'z'],
20727 parallelArrays: ['x', 'y', 'z'],
20728 trackerGroups: ['group', 'dataLabelsGroup'],
20729 bubblePadding: true,
20730 zoneAxis: 'z',
20731
20732
20733 pointAttribs: function(point, state) {
20734 var markerOptions = this.options.marker,
20735 fillOpacity = pick(markerOptions.fillOpacity, 0.5),
20736 attr = Series.prototype.pointAttribs.call(this, point, state);
20737
20738 if (fillOpacity !== 1) {
20739 attr.fill = color(attr.fill).setOpacity(fillOpacity).get('rgba');
20740 }
20741
20742 return attr;
20743 },
20744
20745
20746 /**
20747 * Get the radius for each point based on the minSize, maxSize and each point's Z value. This
20748 * must be done prior to Series.translate because the axis needs to add padding in
20749 * accordance with the point sizes.
20750 */
20751 getRadii: function(zMin, zMax, minSize, maxSize) {
20752 var len,
20753 i,
20754 pos,
20755 zData = this.zData,
20756 radii = [],
20757 options = this.options,
20758 sizeByArea = options.sizeBy !== 'width',
20759 zThreshold = options.zThreshold,
20760 zRange = zMax - zMin,
20761 value,
20762 radius;
20763
20764 // Set the shape type and arguments to be picked up in drawPoints
20765 for (i = 0, len = zData.length; i < len; i++) {
20766
20767 value = zData[i];
20768
20769 // When sizing by threshold, the absolute value of z determines the size
20770 // of the bubble.
20771 if (options.sizeByAbsoluteValue && value !== null) {
20772 value = Math.abs(value - zThreshold);
20773 zMax = Math.max(zMax - zThreshold, Math.abs(zMin - zThreshold));
20774 zMin = 0;
20775 }
20776
20777 if (value === null) {
20778 radius = null;
20779 // Issue #4419 - if value is less than zMin, push a radius that's always smaller than the minimum size
20780 } else if (value < zMin) {
20781 radius = minSize / 2 - 1;
20782 } else {
20783 // Relative size, a number between 0 and 1
20784 pos = zRange > 0 ? (value - zMin) / zRange : 0.5;
20785
20786 if (sizeByArea && pos >= 0) {
20787 pos = Math.sqrt(pos);
20788 }
20789 radius = Math.ceil(minSize + pos * (maxSize - minSize)) / 2;
20790 }
20791 radii.push(radius);
20792 }
20793 this.radii = radii;
20794 },
20795
20796 /**
20797 * Perform animation on the bubbles
20798 */
20799 animate: function(init) {
20800 var animation = this.options.animation;
20801
20802 if (!init) { // run the animation
20803 each(this.points, function(point) {
20804 var graphic = point.graphic,
20805 shapeArgs = point.shapeArgs;
20806
20807 if (graphic && shapeArgs) {
20808 // start values
20809 graphic.attr('r', 1);
20810
20811 // animate
20812 graphic.animate({
20813 r: shapeArgs.r
20814 }, animation);
20815 }
20816 });
20817
20818 // delete this function to allow it only once
20819 this.animate = null;
20820 }
20821 },
20822
20823 /**
20824 * Extend the base translate method to handle bubble size
20825 */
20826 translate: function() {
20827
20828 var i,
20829 data = this.data,
20830 point,
20831 radius,
20832 radii = this.radii;
20833
20834 // Run the parent method
20835 seriesTypes.scatter.prototype.translate.call(this);
20836
20837 // Set the shape type and arguments to be picked up in drawPoints
20838 i = data.length;
20839
20840 while (i--) {
20841 point = data[i];
20842 radius = radii ? radii[i] : 0; // #1737
20843
20844 if (isNumber(radius) && radius >= this.minPxSize / 2) {
20845 // Shape arguments
20846 point.shapeType = 'circle';
20847 point.shapeArgs = {
20848 x: point.plotX,
20849 y: point.plotY,
20850 r: radius
20851 };
20852
20853 // Alignment box for the data label
20854 point.dlBox = {
20855 x: point.plotX - radius,
20856 y: point.plotY - radius,
20857 width: 2 * radius,
20858 height: 2 * radius
20859 };
20860 } else { // below zThreshold
20861 point.shapeArgs = point.plotY = point.dlBox = undefined; // #1691
20862 }
20863 }
20864 },
20865
20866 /**
20867 * Get the series' symbol in the legend
20868 *
20869 * @param {Object} legend The legend object
20870 * @param {Object} item The series (this) or point
20871 */
20872 drawLegendSymbol: function(legend, item) {
20873 var renderer = this.chart.renderer,
20874 radius = renderer.fontMetrics(legend.itemStyle.fontSize).f / 2;
20875
20876 item.legendSymbol = renderer.circle(
20877 radius,
20878 legend.baseline - radius,
20879 radius
20880 ).attr({
20881 zIndex: 3
20882 }).add(item.legendGroup);
20883 item.legendSymbol.isMarker = true;
20884
20885 },
20886
20887 drawPoints: seriesTypes.column.prototype.drawPoints,
20888 alignDataLabel: seriesTypes.column.prototype.alignDataLabel,
20889 buildKDTree: noop,
20890 applyZones: noop
20891
20892 // Point class
20893 }, {
20894 haloPath: function() {
20895 return Point.prototype.haloPath.call(this, this.shapeArgs.r + this.series.options.states.hover.halo.size);
20896 },
20897 ttBelow: false
20898 });
20899
20900 /**
20901 * Add logic to pad each axis with the amount of pixels
20902 * necessary to avoid the bubbles to overflow.
20903 */
20904 Axis.prototype.beforePadding = function() {
20905 var axis = this,
20906 axisLength = this.len,
20907 chart = this.chart,
20908 pxMin = 0,
20909 pxMax = axisLength,
20910 isXAxis = this.isXAxis,
20911 dataKey = isXAxis ? 'xData' : 'yData',
20912 min = this.min,
20913 extremes = {},
20914 smallestSize = Math.min(chart.plotWidth, chart.plotHeight),
20915 zMin = Number.MAX_VALUE,
20916 zMax = -Number.MAX_VALUE,
20917 range = this.max - min,
20918 transA = axisLength / range,
20919 activeSeries = [];
20920
20921 // Handle padding on the second pass, or on redraw
20922 each(this.series, function(series) {
20923
20924 var seriesOptions = series.options,
20925 zData;
20926
20927 if (series.bubblePadding && (series.visible || !chart.options.chart.ignoreHiddenSeries)) {
20928
20929 // Correction for #1673
20930 axis.allowZoomOutside = true;
20931
20932 // Cache it
20933 activeSeries.push(series);
20934
20935 if (isXAxis) { // because X axis is evaluated first
20936
20937 // For each series, translate the size extremes to pixel values
20938 each(['minSize', 'maxSize'], function(prop) {
20939 var length = seriesOptions[prop],
20940 isPercent = /%$/.test(length);
20941
20942 length = pInt(length);
20943 extremes[prop] = isPercent ?
20944 smallestSize * length / 100 :
20945 length;
20946
20947 });
20948 series.minPxSize = extremes.minSize;
20949 series.maxPxSize = extremes.maxSize;
20950
20951 // Find the min and max Z
20952 zData = series.zData;
20953 if (zData.length) { // #1735
20954 zMin = pick(seriesOptions.zMin, Math.min(
20955 zMin,
20956 Math.max(
20957 arrayMin(zData),
20958 seriesOptions.displayNegative === false ? seriesOptions.zThreshold : -Number.MAX_VALUE
20959 )
20960 ));
20961 zMax = pick(seriesOptions.zMax, Math.max(zMax, arrayMax(zData)));
20962 }
20963 }
20964 }
20965 });
20966
20967 each(activeSeries, function(series) {
20968
20969 var data = series[dataKey],
20970 i = data.length,
20971 radius;
20972
20973 if (isXAxis) {
20974 series.getRadii(zMin, zMax, series.minPxSize, series.maxPxSize);
20975 }
20976
20977 if (range > 0) {
20978 while (i--) {
20979 if (isNumber(data[i]) && axis.dataMin <= data[i] && data[i] <= axis.dataMax) {
20980 radius = series.radii[i];
20981 pxMin = Math.min(((data[i] - min) * transA) - radius, pxMin);
20982 pxMax = Math.max(((data[i] - min) * transA) + radius, pxMax);
20983 }
20984 }
20985 }
20986 });
20987
20988 if (activeSeries.length && range > 0 && !this.isLog) {
20989 pxMax -= axisLength;
20990 transA *= (axisLength + pxMin - pxMax) / axisLength;
20991 each([
20992 ['min', 'userMin', pxMin],
20993 ['max', 'userMax', pxMax]
20994 ], function(keys) {
20995 if (pick(axis.options[keys[0]], axis[keys[1]]) === undefined) {
20996 axis[keys[0]] += keys[2] / transA;
20997 }
20998 });
20999 }
21000 };
21001
21002 /* ****************************************************************************
21003 * End Bubble series code *
21004 *****************************************************************************/
21005
21006 }(Highcharts));
21007 (function(H) {
21008 /**
21009 * (c) 2010-2016 Torstein Honsi
21010 *
21011 * License: www.highcharts.com/license
21012 */
21013 'use strict';
21014 var extend = H.extend,
21015 Point = H.Point,
21016 seriesType = H.seriesType,
21017 seriesTypes = H.seriesTypes;
21018
21019 // The mapbubble series type
21020 if (seriesTypes.bubble) {
21021
21022 seriesType('mapbubble', 'bubble', {
21023 animationLimit: 500,
21024 tooltip: {
21025 pointFormat: '{point.name}: {point.z}'
21026 }
21027
21028 // Prototype members
21029 }, {
21030 xyFromShape: true,
21031 type: 'mapbubble',
21032 pointArrayMap: ['z'], // If one single value is passed, it is interpreted as z
21033 /**
21034 * Return the map area identified by the dataJoinBy option
21035 */
21036 getMapData: seriesTypes.map.prototype.getMapData,
21037 getBox: seriesTypes.map.prototype.getBox,
21038 setData: seriesTypes.map.prototype.setData
21039
21040 // Point class
21041 }, {
21042 applyOptions: function(options, x) {
21043 var point;
21044 if (options && options.lat !== undefined && options.lon !== undefined) {
21045 point = Point.prototype.applyOptions.call(this, options, x);
21046 point = extend(point, this.series.chart.fromLatLonToPoint(point));
21047 } else {
21048 point = seriesTypes.map.prototype.pointClass.prototype.applyOptions.call(this, options, x);
21049 }
21050 return point;
21051 },
21052 ttBelow: false
21053 });
21054 }
21055
21056 }(Highcharts));
21057 (function(H) {
21058 /**
21059 * (c) 2010-2016 Torstein Honsi
21060 *
21061 * License: www.highcharts.com/license
21062 */
21063 'use strict';
21064 var Chart = H.Chart,
21065 each = H.each,
21066 extend = H.extend,
21067 error = H.error,
21068 format = H.format,
21069 merge = H.merge,
21070 win = H.win,
21071 wrap = H.wrap;
21072 /**
21073 * Test for point in polygon. Polygon defined as array of [x,y] points.
21074 */
21075 function pointInPolygon(point, polygon) {
21076 var i,
21077 j,
21078 rel1,
21079 rel2,
21080 c = false,
21081 x = point.x,
21082 y = point.y;
21083
21084 for (i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
21085 rel1 = polygon[i][1] > y;
21086 rel2 = polygon[j][1] > y;
21087 if (rel1 !== rel2 && (x < (polygon[j][0] - polygon[i][0]) * (y - polygon[i][1]) / (polygon[j][1] - polygon[i][1]) + polygon[i][0])) {
21088 c = !c;
21089 }
21090 }
21091
21092 return c;
21093 }
21094
21095 /**
21096 * Get point from latLon using specified transform definition
21097 */
21098 Chart.prototype.transformFromLatLon = function(latLon, transform) {
21099 if (win.proj4 === undefined) {
21100 error(21);
21101 return {
21102 x: 0,
21103 y: null
21104 };
21105 }
21106
21107 var projected = win.proj4(transform.crs, [latLon.lon, latLon.lat]),
21108 cosAngle = transform.cosAngle || (transform.rotation && Math.cos(transform.rotation)),
21109 sinAngle = transform.sinAngle || (transform.rotation && Math.sin(transform.rotation)),
21110 rotated = transform.rotation ? [projected[0] * cosAngle + projected[1] * sinAngle, -projected[0] * sinAngle + projected[1] * cosAngle] : projected;
21111
21112 return {
21113 x: ((rotated[0] - (transform.xoffset || 0)) * (transform.scale || 1) + (transform.xpan || 0)) * (transform.jsonres || 1) + (transform.jsonmarginX || 0),
21114 y: (((transform.yoffset || 0) - rotated[1]) * (transform.scale || 1) + (transform.ypan || 0)) * (transform.jsonres || 1) - (transform.jsonmarginY || 0)
21115 };
21116 };
21117
21118 /**
21119 * Get latLon from point using specified transform definition
21120 */
21121 Chart.prototype.transformToLatLon = function(point, transform) {
21122 if (win.proj4 === undefined) {
21123 error(21);
21124 return;
21125 }
21126
21127 var normalized = {
21128 x: ((point.x - (transform.jsonmarginX || 0)) / (transform.jsonres || 1) - (transform.xpan || 0)) / (transform.scale || 1) + (transform.xoffset || 0),
21129 y: ((-point.y - (transform.jsonmarginY || 0)) / (transform.jsonres || 1) + (transform.ypan || 0)) / (transform.scale || 1) + (transform.yoffset || 0)
21130 },
21131 cosAngle = transform.cosAngle || (transform.rotation && Math.cos(transform.rotation)),
21132 sinAngle = transform.sinAngle || (transform.rotation && Math.sin(transform.rotation)),
21133 // Note: Inverted sinAngle to reverse rotation direction
21134 projected = win.proj4(transform.crs, 'WGS84', transform.rotation ? {
21135 x: normalized.x * cosAngle + normalized.y * -sinAngle,
21136 y: normalized.x * sinAngle + normalized.y * cosAngle
21137 } : normalized);
21138
21139 return {
21140 lat: projected.y,
21141 lon: projected.x
21142 };
21143 };
21144
21145 Chart.prototype.fromPointToLatLon = function(point) {
21146 var transforms = this.mapTransforms,
21147 transform;
21148
21149 if (!transforms) {
21150 error(22);
21151 return;
21152 }
21153
21154 for (transform in transforms) {
21155 if (transforms.hasOwnProperty(transform) && transforms[transform].hitZone &&
21156 pointInPolygon({
21157 x: point.x,
21158 y: -point.y
21159 }, transforms[transform].hitZone.coordinates[0])) {
21160 return this.transformToLatLon(point, transforms[transform]);
21161 }
21162 }
21163
21164 return this.transformToLatLon(point, transforms['default']); // eslint-disable-line dot-notation
21165 };
21166
21167 Chart.prototype.fromLatLonToPoint = function(latLon) {
21168 var transforms = this.mapTransforms,
21169 transform,
21170 coords;
21171
21172 if (!transforms) {
21173 error(22);
21174 return {
21175 x: 0,
21176 y: null
21177 };
21178 }
21179
21180 for (transform in transforms) {
21181 if (transforms.hasOwnProperty(transform) && transforms[transform].hitZone) {
21182 coords = this.transformFromLatLon(latLon, transforms[transform]);
21183 if (pointInPolygon({
21184 x: coords.x,
21185 y: -coords.y
21186 }, transforms[transform].hitZone.coordinates[0])) {
21187 return coords;
21188 }
21189 }
21190 }
21191
21192 return this.transformFromLatLon(latLon, transforms['default']); // eslint-disable-line dot-notation
21193 };
21194
21195 /**
21196 * Convert a geojson object to map data of a given Highcharts type (map, mappoint or mapline).
21197 */
21198 H.geojson = function(geojson, hType, series) {
21199 var mapData = [],
21200 path = [],
21201 polygonToPath = function(polygon) {
21202 var i,
21203 len = polygon.length;
21204 path.push('M');
21205 for (i = 0; i < len; i++) {
21206 if (i === 1) {
21207 path.push('L');
21208 }
21209 path.push(polygon[i][0], -polygon[i][1]);
21210 }
21211 };
21212
21213 hType = hType || 'map';
21214
21215 each(geojson.features, function(feature) {
21216
21217 var geometry = feature.geometry,
21218 type = geometry.type,
21219 coordinates = geometry.coordinates,
21220 properties = feature.properties,
21221 point;
21222
21223 path = [];
21224
21225 if (hType === 'map' || hType === 'mapbubble') {
21226 if (type === 'Polygon') {
21227 each(coordinates, polygonToPath);
21228 path.push('Z');
21229
21230 } else if (type === 'MultiPolygon') {
21231 each(coordinates, function(items) {
21232 each(items, polygonToPath);
21233 });
21234 path.push('Z');
21235 }
21236
21237 if (path.length) {
21238 point = {
21239 path: path
21240 };
21241 }
21242
21243 } else if (hType === 'mapline') {
21244 if (type === 'LineString') {
21245 polygonToPath(coordinates);
21246 } else if (type === 'MultiLineString') {
21247 each(coordinates, polygonToPath);
21248 }
21249
21250 if (path.length) {
21251 point = {
21252 path: path
21253 };
21254 }
21255
21256 } else if (hType === 'mappoint') {
21257 if (type === 'Point') {
21258 point = {
21259 x: coordinates[0],
21260 y: -coordinates[1]
21261 };
21262 }
21263 }
21264 if (point) {
21265 mapData.push(extend(point, {
21266 name: properties.name || properties.NAME,
21267 properties: properties
21268 }));
21269 }
21270
21271 });
21272
21273 // Create a credits text that includes map source, to be picked up in Chart.addCredits
21274 if (series && geojson.copyrightShort) {
21275 series.chart.mapCredits = format(series.chart.options.credits.mapText, {
21276 geojson: geojson
21277 });
21278 series.chart.mapCreditsFull = format(series.chart.options.credits.mapTextFull, {
21279 geojson: geojson
21280 });
21281 }
21282
21283 return mapData;
21284 };
21285
21286 /**
21287 * Override addCredits to include map source by default
21288 */
21289 wrap(Chart.prototype, 'addCredits', function(proceed, credits) {
21290
21291 credits = merge(true, this.options.credits, credits);
21292
21293 // Disable credits link if map credits enabled. This to allow for in-text anchors.
21294 if (this.mapCredits) {
21295 credits.href = null;
21296 }
21297
21298 proceed.call(this, credits);
21299
21300 // Add full map credits to hover
21301 if (this.credits && this.mapCreditsFull) {
21302 this.credits.attr({
21303 title: this.mapCreditsFull
21304 });
21305 }
21306 });
21307
21308 }(Highcharts));
21309 (function(H) {
21310 /**
21311 * (c) 2010-2016 Torstein Honsi
21312 *
21313 * License: www.highcharts.com/license
21314 */
21315 'use strict';
21316 var Chart = H.Chart,
21317 defaultOptions = H.defaultOptions,
21318 each = H.each,
21319 extend = H.extend,
21320 merge = H.merge,
21321 pick = H.pick,
21322 Renderer = H.Renderer,
21323 SVGRenderer = H.SVGRenderer,
21324 VMLRenderer = H.VMLRenderer;
21325
21326
21327 // Add language
21328 extend(defaultOptions.lang, {
21329 zoomIn: 'Zoom in',
21330 zoomOut: 'Zoom out'
21331 });
21332
21333
21334 // Set the default map navigation options
21335 defaultOptions.mapNavigation = {
21336 buttonOptions: {
21337 alignTo: 'plotBox',
21338 align: 'left',
21339 verticalAlign: 'top',
21340 x: 0,
21341 width: 18,
21342 height: 18,
21343 padding: 5,
21344
21345 style: {
21346 fontSize: '15px',
21347 fontWeight: 'bold'
21348 },
21349 theme: {
21350 'stroke-width': 1,
21351 'text-align': 'center'
21352 }
21353
21354 },
21355 buttons: {
21356 zoomIn: {
21357 onclick: function() {
21358 this.mapZoom(0.5);
21359 },
21360 text: '+',
21361 y: 0
21362 },
21363 zoomOut: {
21364 onclick: function() {
21365 this.mapZoom(2);
21366 },
21367 text: '-',
21368 y: 28
21369 }
21370 },
21371 mouseWheelSensitivity: 1.1
21372 // enabled: false,
21373 // enableButtons: null, // inherit from enabled
21374 // enableTouchZoom: null, // inherit from enabled
21375 // enableDoubleClickZoom: null, // inherit from enabled
21376 // enableDoubleClickZoomTo: false
21377 // enableMouseWheelZoom: null, // inherit from enabled
21378 };
21379
21380 /**
21381 * Utility for reading SVG paths directly.
21382 */
21383 H.splitPath = function(path) {
21384 var i;
21385
21386 // Move letters apart
21387 path = path.replace(/([A-Za-z])/g, ' $1 ');
21388 // Trim
21389 path = path.replace(/^\s*/, '').replace(/\s*$/, '');
21390
21391 // Split on spaces and commas
21392 path = path.split(/[ ,]+/); // Extra comma to escape gulp.scripts task
21393
21394 // Parse numbers
21395 for (i = 0; i < path.length; i++) {
21396 if (!/[a-zA-Z]/.test(path[i])) {
21397 path[i] = parseFloat(path[i]);
21398 }
21399 }
21400 return path;
21401 };
21402
21403 // A placeholder for map definitions
21404 H.maps = {};
21405
21406
21407
21408
21409
21410 // Create symbols for the zoom buttons
21411 function selectiveRoundedRect(x, y, w, h, rTopLeft, rTopRight, rBottomRight, rBottomLeft) {
21412 return ['M', x + rTopLeft, y,
21413 // top side
21414 'L', x + w - rTopRight, y,
21415 // top right corner
21416 'C', x + w - rTopRight / 2, y, x + w, y + rTopRight / 2, x + w, y + rTopRight,
21417 // right side
21418 'L', x + w, y + h - rBottomRight,
21419 // bottom right corner
21420 'C', x + w, y + h - rBottomRight / 2, x + w - rBottomRight / 2, y + h, x + w - rBottomRight, y + h,
21421 // bottom side
21422 'L', x + rBottomLeft, y + h,
21423 // bottom left corner
21424 'C', x + rBottomLeft / 2, y + h, x, y + h - rBottomLeft / 2, x, y + h - rBottomLeft,
21425 // left side
21426 'L', x, y + rTopLeft,
21427 // top left corner
21428 'C', x, y + rTopLeft / 2, x + rTopLeft / 2, y, x + rTopLeft, y,
21429 'Z'
21430 ];
21431 }
21432 SVGRenderer.prototype.symbols.topbutton = function(x, y, w, h, attr) {
21433 return selectiveRoundedRect(x - 1, y - 1, w, h, attr.r, attr.r, 0, 0);
21434 };
21435 SVGRenderer.prototype.symbols.bottombutton = function(x, y, w, h, attr) {
21436 return selectiveRoundedRect(x - 1, y - 1, w, h, 0, 0, attr.r, attr.r);
21437 };
21438 // The symbol callbacks are generated on the SVGRenderer object in all browsers. Even
21439 // VML browsers need this in order to generate shapes in export. Now share
21440 // them with the VMLRenderer.
21441 if (Renderer === VMLRenderer) {
21442 each(['topbutton', 'bottombutton'], function(shape) {
21443 VMLRenderer.prototype.symbols[shape] = SVGRenderer.prototype.symbols[shape];
21444 });
21445 }
21446
21447
21448 /**
21449 * A wrapper for Chart with all the default values for a Map
21450 */
21451 H.Map = H.mapChart = function(a, b, c) {
21452
21453 var hasRenderToArg = typeof a === 'string' || a.nodeName,
21454 options = arguments[hasRenderToArg ? 1 : 0],
21455 hiddenAxis = {
21456 endOnTick: false,
21457 visible: false,
21458 minPadding: 0,
21459 maxPadding: 0,
21460 startOnTick: false
21461 },
21462 seriesOptions,
21463 defaultCreditsOptions = H.getOptions().credits;
21464
21465 /* For visual testing
21466 hiddenAxis.gridLineWidth = 1;
21467 hiddenAxis.gridZIndex = 10;
21468 hiddenAxis.tickPositions = undefined;
21469 // */
21470
21471 // Don't merge the data
21472 seriesOptions = options.series;
21473 options.series = null;
21474
21475 options = merge({
21476 chart: {
21477 panning: 'xy',
21478 type: 'map'
21479 },
21480 credits: {
21481 mapText: pick(defaultCreditsOptions.mapText, ' \u00a9 <a href="{geojson.copyrightUrl}">{geojson.copyrightShort}</a>'),
21482 mapTextFull: pick(defaultCreditsOptions.mapTextFull, '{geojson.copyright}')
21483 },
21484 xAxis: hiddenAxis,
21485 yAxis: merge(hiddenAxis, {
21486 reversed: true
21487 })
21488 },
21489 options, // user's options
21490
21491 { // forced options
21492 chart: {
21493 inverted: false,
21494 alignTicks: false
21495 }
21496 }
21497 );
21498
21499 options.series = seriesOptions;
21500
21501
21502 return hasRenderToArg ?
21503 new Chart(a, options, c) :
21504 new Chart(options, b);
21505 };
21506
21507 }(Highcharts));
21508 (function(H) {
21509 /**
21510 * (c) 2010-2016 Torstein Honsi
21511 *
21512 * License: www.highcharts.com/license
21513 */
21514 'use strict';
21515 var colorPointMixin = H.colorPointMixin,
21516 colorSeriesMixin = H.colorSeriesMixin,
21517 each = H.each,
21518 LegendSymbolMixin = H.LegendSymbolMixin,
21519 merge = H.merge,
21520 noop = H.noop,
21521 pick = H.pick,
21522 Series = H.Series,
21523 seriesType = H.seriesType,
21524 seriesTypes = H.seriesTypes;
21525
21526 // The Heatmap series type
21527 seriesType('heatmap', 'scatter', {
21528 animation: false,
21529 borderWidth: 0,
21530
21531 nullColor: '#f7f7f7',
21532
21533 dataLabels: {
21534 formatter: function() { // #2945
21535 return this.point.value;
21536 },
21537 inside: true,
21538 verticalAlign: 'middle',
21539 crop: false,
21540 overflow: false,
21541 padding: 0 // #3837
21542 },
21543 marker: null,
21544 pointRange: null, // dynamically set to colsize by default
21545 tooltip: {
21546 pointFormat: '{point.x}, {point.y}: {point.value}<br/>'
21547 },
21548 states: {
21549 normal: {
21550 animation: true
21551 },
21552 hover: {
21553 halo: false, // #3406, halo is not required on heatmaps
21554 brightness: 0.2
21555 }
21556 }
21557 }, merge(colorSeriesMixin, {
21558 pointArrayMap: ['y', 'value'],
21559 hasPointSpecificOptions: true,
21560 supportsDrilldown: true,
21561 getExtremesFromAll: true,
21562 directTouch: true,
21563
21564 /**
21565 * Override the init method to add point ranges on both axes.
21566 */
21567 init: function() {
21568 var options;
21569 seriesTypes.scatter.prototype.init.apply(this, arguments);
21570
21571 options = this.options;
21572 options.pointRange = pick(options.pointRange, options.colsize || 1); // #3758, prevent resetting in setData
21573 this.yAxis.axisPointRange = options.rowsize || 1; // general point range
21574 },
21575 translate: function() {
21576 var series = this,
21577 options = series.options,
21578 xAxis = series.xAxis,
21579 yAxis = series.yAxis,
21580 between = function(x, a, b) {
21581 return Math.min(Math.max(a, x), b);
21582 };
21583
21584 series.generatePoints();
21585
21586 each(series.points, function(point) {
21587 var xPad = (options.colsize || 1) / 2,
21588 yPad = (options.rowsize || 1) / 2,
21589 x1 = between(Math.round(xAxis.len - xAxis.translate(point.x - xPad, 0, 1, 0, 1)), -xAxis.len, 2 * xAxis.len),
21590 x2 = between(Math.round(xAxis.len - xAxis.translate(point.x + xPad, 0, 1, 0, 1)), -xAxis.len, 2 * xAxis.len),
21591 y1 = between(Math.round(yAxis.translate(point.y - yPad, 0, 1, 0, 1)), -yAxis.len, 2 * yAxis.len),
21592 y2 = between(Math.round(yAxis.translate(point.y + yPad, 0, 1, 0, 1)), -yAxis.len, 2 * yAxis.len);
21593
21594 // Set plotX and plotY for use in K-D-Tree and more
21595 point.plotX = point.clientX = (x1 + x2) / 2;
21596 point.plotY = (y1 + y2) / 2;
21597
21598 point.shapeType = 'rect';
21599 point.shapeArgs = {
21600 x: Math.min(x1, x2),
21601 y: Math.min(y1, y2),
21602 width: Math.abs(x2 - x1),
21603 height: Math.abs(y2 - y1)
21604 };
21605 });
21606
21607 series.translateColors();
21608 },
21609 drawPoints: function() {
21610 seriesTypes.column.prototype.drawPoints.call(this);
21611
21612 each(this.points, function(point) {
21613 point.graphic.attr(this.colorAttribs(point, point.state));
21614 }, this);
21615 },
21616 animate: noop,
21617 getBox: noop,
21618 drawLegendSymbol: LegendSymbolMixin.drawRectangle,
21619 alignDataLabel: seriesTypes.column.prototype.alignDataLabel,
21620 getExtremes: function() {
21621 // Get the extremes from the value data
21622 Series.prototype.getExtremes.call(this, this.valueData);
21623 this.valueMin = this.dataMin;
21624 this.valueMax = this.dataMax;
21625
21626 // Get the extremes from the y data
21627 Series.prototype.getExtremes.call(this);
21628 }
21629
21630 }), colorPointMixin);
21631
21632 }(Highcharts));
21633 (function(H) {
21634 /**
21635 * (c) 2010-2016 Torstein Honsi
21636 *
21637 * License: www.highcharts.com/license
21638 */
21639 'use strict';
21640 var addEvent = H.addEvent,
21641 Chart = H.Chart,
21642 createElement = H.createElement,
21643 css = H.css,
21644 defaultOptions = H.defaultOptions,
21645 defaultPlotOptions = H.defaultPlotOptions,
21646 each = H.each,
21647 extend = H.extend,
21648 fireEvent = H.fireEvent,
21649 hasTouch = H.hasTouch,
21650 inArray = H.inArray,
21651 isObject = H.isObject,
21652 Legend = H.Legend,
21653 merge = H.merge,
21654 pick = H.pick,
21655 Point = H.Point,
21656 Series = H.Series,
21657 seriesTypes = H.seriesTypes,
21658 svg = H.svg,
21659 TrackerMixin;
21660 /**
21661 * TrackerMixin for points and graphs
21662 */
21663 TrackerMixin = H.TrackerMixin = {
21664
21665 drawTrackerPoint: function() {
21666 var series = this,
21667 chart = series.chart,
21668 pointer = chart.pointer,
21669 onMouseOver = function(e) {
21670 var target = e.target,
21671 point;
21672
21673 while (target && !point) {
21674 point = target.point;
21675 target = target.parentNode;
21676 }
21677
21678 if (point !== undefined && point !== chart.hoverPoint) { // undefined on graph in scatterchart
21679 point.onMouseOver(e);
21680 }
21681 };
21682
21683 // Add reference to the point
21684 each(series.points, function(point) {
21685 if (point.graphic) {
21686 point.graphic.element.point = point;
21687 }
21688 if (point.dataLabel) {
21689 point.dataLabel.element.point = point;
21690 }
21691 });
21692
21693 // Add the event listeners, we need to do this only once
21694 if (!series._hasTracking) {
21695 each(series.trackerGroups, function(key) {
21696 if (series[key]) { // we don't always have dataLabelsGroup
21697 series[key]
21698 .addClass('highcharts-tracker')
21699 .on('mouseover', onMouseOver)
21700 .on('mouseout', function(e) {
21701 pointer.onTrackerMouseOut(e);
21702 });
21703 if (hasTouch) {
21704 series[key].on('touchstart', onMouseOver);
21705 }
21706
21707
21708 if (series.options.cursor) {
21709 series[key]
21710 .css(css)
21711 .css({
21712 cursor: series.options.cursor
21713 });
21714 }
21715
21716 }
21717 });
21718 series._hasTracking = true;
21719 }
21720 },
21721
21722 /**
21723 * Draw the tracker object that sits above all data labels and markers to
21724 * track mouse events on the graph or points. For the line type charts
21725 * the tracker uses the same graphPath, but with a greater stroke width
21726 * for better control.
21727 */
21728 drawTrackerGraph: function() {
21729 var series = this,
21730 options = series.options,
21731 trackByArea = options.trackByArea,
21732 trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath),
21733 trackerPathLength = trackerPath.length,
21734 chart = series.chart,
21735 pointer = chart.pointer,
21736 renderer = chart.renderer,
21737 snap = chart.options.tooltip.snap,
21738 tracker = series.tracker,
21739 i,
21740 onMouseOver = function() {
21741 if (chart.hoverSeries !== series) {
21742 series.onMouseOver();
21743 }
21744 },
21745 /*
21746 * Empirical lowest possible opacities for TRACKER_FILL for an element to stay invisible but clickable
21747 * IE6: 0.002
21748 * IE7: 0.002
21749 * IE8: 0.002
21750 * IE9: 0.00000000001 (unlimited)
21751 * IE10: 0.0001 (exporting only)
21752 * FF: 0.00000000001 (unlimited)
21753 * Chrome: 0.000001
21754 * Safari: 0.000001
21755 * Opera: 0.00000000001 (unlimited)
21756 */
21757 TRACKER_FILL = 'rgba(192,192,192,' + (svg ? 0.0001 : 0.002) + ')';
21758
21759 // Extend end points. A better way would be to use round linecaps,
21760 // but those are not clickable in VML.
21761 if (trackerPathLength && !trackByArea) {
21762 i = trackerPathLength + 1;
21763 while (i--) {
21764 if (trackerPath[i] === 'M') { // extend left side
21765 trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], 'L');
21766 }
21767 if ((i && trackerPath[i] === 'M') || i === trackerPathLength) { // extend right side
21768 trackerPath.splice(i, 0, 'L', trackerPath[i - 2] + snap, trackerPath[i - 1]);
21769 }
21770 }
21771 }
21772
21773 // handle single points
21774 /*for (i = 0; i < singlePoints.length; i++) {
21775 singlePoint = singlePoints[i];
21776 trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY,
21777 L, singlePoint.plotX + snap, singlePoint.plotY);
21778 }*/
21779
21780 // draw the tracker
21781 if (tracker) {
21782 tracker.attr({
21783 d: trackerPath
21784 });
21785 } else if (series.graph) { // create
21786
21787 series.tracker = renderer.path(trackerPath)
21788 .attr({
21789 'stroke-linejoin': 'round', // #1225
21790 visibility: series.visible ? 'visible' : 'hidden',
21791 stroke: TRACKER_FILL,
21792 fill: trackByArea ? TRACKER_FILL : 'none',
21793 'stroke-width': series.graph.strokeWidth() + (trackByArea ? 0 : 2 * snap),
21794 zIndex: 2
21795 })
21796 .add(series.group);
21797
21798 // The tracker is added to the series group, which is clipped, but is covered
21799 // by the marker group. So the marker group also needs to capture events.
21800 each([series.tracker, series.markerGroup], function(tracker) {
21801 tracker.addClass('highcharts-tracker')
21802 .on('mouseover', onMouseOver)
21803 .on('mouseout', function(e) {
21804 pointer.onTrackerMouseOut(e);
21805 });
21806
21807
21808 if (options.cursor) {
21809 tracker.css({
21810 cursor: options.cursor
21811 });
21812 }
21813
21814
21815 if (hasTouch) {
21816 tracker.on('touchstart', onMouseOver);
21817 }
21818 });
21819 }
21820 }
21821 };
21822 /* End TrackerMixin */
21823
21824
21825 /**
21826 * Add tracking event listener to the series group, so the point graphics
21827 * themselves act as trackers
21828 */
21829
21830 if (seriesTypes.column) {
21831 seriesTypes.column.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
21832 }
21833
21834 if (seriesTypes.pie) {
21835 seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
21836 }
21837
21838 if (seriesTypes.scatter) {
21839 seriesTypes.scatter.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
21840 }
21841
21842 /*
21843 * Extend Legend for item events
21844 */
21845 extend(Legend.prototype, {
21846
21847 setItemEvents: function(item, legendItem, useHTML) {
21848 var legend = this,
21849 chart = legend.chart,
21850 activeClass = 'highcharts-legend-' + (item.series ? 'point' : 'series') + '-active';
21851
21852 // Set the events on the item group, or in case of useHTML, the item itself (#1249)
21853 (useHTML ? legendItem : item.legendGroup).on('mouseover', function() {
21854 item.setState('hover');
21855
21856 // A CSS class to dim or hide other than the hovered series
21857 chart.seriesGroup.addClass(activeClass);
21858
21859
21860 legendItem.css(legend.options.itemHoverStyle);
21861
21862 })
21863 .on('mouseout', function() {
21864
21865 legendItem.css(item.visible ? legend.itemStyle : legend.itemHiddenStyle);
21866
21867
21868 // A CSS class to dim or hide other than the hovered series
21869 chart.seriesGroup.removeClass(activeClass);
21870
21871 item.setState();
21872 })
21873 .on('click', function(event) {
21874 var strLegendItemClick = 'legendItemClick',
21875 fnLegendItemClick = function() {
21876 if (item.setVisible) {
21877 item.setVisible();
21878 }
21879 };
21880
21881 // Pass over the click/touch event. #4.
21882 event = {
21883 browserEvent: event
21884 };
21885
21886 // click the name or symbol
21887 if (item.firePointEvent) { // point
21888 item.firePointEvent(strLegendItemClick, event, fnLegendItemClick);
21889 } else {
21890 fireEvent(item, strLegendItemClick, event, fnLegendItemClick);
21891 }
21892 });
21893 },
21894
21895 createCheckboxForItem: function(item) {
21896 var legend = this;
21897
21898 item.checkbox = createElement('input', {
21899 type: 'checkbox',
21900 checked: item.selected,
21901 defaultChecked: item.selected // required by IE7
21902 }, legend.options.itemCheckboxStyle, legend.chart.container);
21903
21904 addEvent(item.checkbox, 'click', function(event) {
21905 var target = event.target;
21906 fireEvent(
21907 item.series || item,
21908 'checkboxClick', { // #3712
21909 checked: target.checked,
21910 item: item
21911 },
21912 function() {
21913 item.select();
21914 }
21915 );
21916 });
21917 }
21918 });
21919
21920
21921
21922 // Add pointer cursor to legend itemstyle in defaultOptions
21923 defaultOptions.legend.itemStyle.cursor = 'pointer';
21924
21925
21926
21927 /*
21928 * Extend the Chart object with interaction
21929 */
21930
21931 extend(Chart.prototype, {
21932 /**
21933 * Display the zoom button
21934 */
21935 showResetZoom: function() {
21936 var chart = this,
21937 lang = defaultOptions.lang,
21938 btnOptions = chart.options.chart.resetZoomButton,
21939 theme = btnOptions.theme,
21940 states = theme.states,
21941 alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox';
21942
21943 function zoomOut() {
21944 chart.zoomOut();
21945 }
21946
21947 this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, zoomOut, theme, states && states.hover)
21948 .attr({
21949 align: btnOptions.position.align,
21950 title: lang.resetZoomTitle
21951 })
21952 .addClass('highcharts-reset-zoom')
21953 .add()
21954 .align(btnOptions.position, false, alignTo);
21955
21956 },
21957
21958 /**
21959 * Zoom out to 1:1
21960 */
21961 zoomOut: function() {
21962 var chart = this;
21963 fireEvent(chart, 'selection', {
21964 resetSelection: true
21965 }, function() {
21966 chart.zoom();
21967 });
21968 },
21969
21970 /**
21971 * Zoom into a given portion of the chart given by axis coordinates
21972 * @param {Object} event
21973 */
21974 zoom: function(event) {
21975 var chart = this,
21976 hasZoomed,
21977 pointer = chart.pointer,
21978 displayButton = false,
21979 resetZoomButton;
21980
21981 // If zoom is called with no arguments, reset the axes
21982 if (!event || event.resetSelection) {
21983 each(chart.axes, function(axis) {
21984 hasZoomed = axis.zoom();
21985 });
21986 } else { // else, zoom in on all axes
21987 each(event.xAxis.concat(event.yAxis), function(axisData) {
21988 var axis = axisData.axis,
21989 isXAxis = axis.isXAxis;
21990
21991 // don't zoom more than minRange
21992 if (pointer[isXAxis ? 'zoomX' : 'zoomY'] || pointer[isXAxis ? 'pinchX' : 'pinchY']) {
21993 hasZoomed = axis.zoom(axisData.min, axisData.max);
21994 if (axis.displayBtn) {
21995 displayButton = true;
21996 }
21997 }
21998 });
21999 }
22000
22001 // Show or hide the Reset zoom button
22002 resetZoomButton = chart.resetZoomButton;
22003 if (displayButton && !resetZoomButton) {
22004 chart.showResetZoom();
22005 } else if (!displayButton && isObject(resetZoomButton)) {
22006 chart.resetZoomButton = resetZoomButton.destroy();
22007 }
22008
22009
22010 // Redraw
22011 if (hasZoomed) {
22012 chart.redraw(
22013 pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation
22014 );
22015 }
22016 },
22017
22018 /**
22019 * Pan the chart by dragging the mouse across the pane. This function is called
22020 * on mouse move, and the distance to pan is computed from chartX compared to
22021 * the first chartX position in the dragging operation.
22022 */
22023 pan: function(e, panning) {
22024
22025 var chart = this,
22026 hoverPoints = chart.hoverPoints,
22027 doRedraw;
22028
22029 // remove active points for shared tooltip
22030 if (hoverPoints) {
22031 each(hoverPoints, function(point) {
22032 point.setState();
22033 });
22034 }
22035
22036 each(panning === 'xy' ? [1, 0] : [1], function(isX) { // xy is used in maps
22037 var axis = chart[isX ? 'xAxis' : 'yAxis'][0],
22038 horiz = axis.horiz,
22039 mousePos = e[horiz ? 'chartX' : 'chartY'],
22040 mouseDown = horiz ? 'mouseDownX' : 'mouseDownY',
22041 startPos = chart[mouseDown],
22042 halfPointRange = (axis.pointRange || 0) / 2,
22043 extremes = axis.getExtremes(),
22044 newMin = axis.toValue(startPos - mousePos, true) + halfPointRange,
22045 newMax = axis.toValue(startPos + axis.len - mousePos, true) - halfPointRange,
22046 goingLeft = startPos > mousePos; // #3613
22047
22048 if (axis.series.length &&
22049 (goingLeft || newMin > Math.min(extremes.dataMin, extremes.min)) &&
22050 (!goingLeft || newMax < Math.max(extremes.dataMax, extremes.max))) {
22051 axis.setExtremes(newMin, newMax, false, false, {
22052 trigger: 'pan'
22053 });
22054 doRedraw = true;
22055 }
22056
22057 chart[mouseDown] = mousePos; // set new reference for next run
22058 });
22059
22060 if (doRedraw) {
22061 chart.redraw(false);
22062 }
22063 css(chart.container, {
22064 cursor: 'move'
22065 });
22066 }
22067 });
22068
22069 /*
22070 * Extend the Point object with interaction
22071 */
22072 extend(Point.prototype, {
22073 /**
22074 * Toggle the selection status of a point
22075 * @param {Boolean} selected Whether to select or unselect the point.
22076 * @param {Boolean} accumulate Whether to add to the previous selection. By default,
22077 * this happens if the control key (Cmd on Mac) was pressed during clicking.
22078 */
22079 select: function(selected, accumulate) {
22080 var point = this,
22081 series = point.series,
22082 chart = series.chart;
22083
22084 selected = pick(selected, !point.selected);
22085
22086 // fire the event with the default handler
22087 point.firePointEvent(selected ? 'select' : 'unselect', {
22088 accumulate: accumulate
22089 }, function() {
22090 point.selected = point.options.selected = selected;
22091 series.options.data[inArray(point, series.data)] = point.options;
22092
22093 point.setState(selected && 'select');
22094
22095 // unselect all other points unless Ctrl or Cmd + click
22096 if (!accumulate) {
22097 each(chart.getSelectedPoints(), function(loopPoint) {
22098 if (loopPoint.selected && loopPoint !== point) {
22099 loopPoint.selected = loopPoint.options.selected = false;
22100 series.options.data[inArray(loopPoint, series.data)] = loopPoint.options;
22101 loopPoint.setState('');
22102 loopPoint.firePointEvent('unselect');
22103 }
22104 });
22105 }
22106 });
22107 },
22108
22109 /**
22110 * Runs on mouse over the point
22111 *
22112 * @param {Object} e The event arguments
22113 * @param {Boolean} byProximity Falsy for kd points that are closest to the mouse, or to
22114 * actually hovered points. True for other points in shared tooltip.
22115 */
22116 onMouseOver: function(e, byProximity) {
22117 var point = this,
22118 series = point.series,
22119 chart = series.chart,
22120 tooltip = chart.tooltip,
22121 hoverPoint = chart.hoverPoint;
22122
22123 if (chart.hoverSeries !== series) {
22124 series.onMouseOver();
22125 }
22126
22127 // set normal state to previous series
22128 if (hoverPoint && hoverPoint !== point) {
22129 hoverPoint.onMouseOut();
22130 }
22131
22132 if (point.series) { // It may have been destroyed, #4130
22133
22134 // trigger the event
22135 point.firePointEvent('mouseOver');
22136
22137 // update the tooltip
22138 if (tooltip && (!tooltip.shared || series.noSharedTooltip)) {
22139 tooltip.refresh(point, e);
22140 }
22141
22142 // hover this
22143 point.setState('hover');
22144 if (!byProximity) {
22145 chart.hoverPoint = point;
22146 }
22147 }
22148 },
22149
22150 /**
22151 * Runs on mouse out from the point
22152 */
22153 onMouseOut: function() {
22154 var chart = this.series.chart,
22155 hoverPoints = chart.hoverPoints;
22156
22157 this.firePointEvent('mouseOut');
22158
22159 if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887, #2240
22160 this.setState();
22161 chart.hoverPoint = null;
22162 }
22163 },
22164
22165 /**
22166 * Import events from the series' and point's options. Only do it on
22167 * demand, to save processing time on hovering.
22168 */
22169 importEvents: function() {
22170 if (!this.hasImportedEvents) {
22171 var point = this,
22172 options = merge(point.series.options.point, point.options),
22173 events = options.events,
22174 eventType;
22175
22176 point.events = events;
22177
22178 for (eventType in events) {
22179 addEvent(point, eventType, events[eventType]);
22180 }
22181 this.hasImportedEvents = true;
22182
22183 }
22184 },
22185
22186 /**
22187 * Set the point's state
22188 * @param {String} state
22189 */
22190 setState: function(state, move) {
22191 var point = this,
22192 plotX = Math.floor(point.plotX), // #4586
22193 plotY = point.plotY,
22194 series = point.series,
22195 stateOptions = series.options.states[state] || {},
22196 markerOptions = (defaultPlotOptions[series.type].marker && series.options.marker) || {},
22197 normalDisabled = markerOptions.enabled === false,
22198 markerStateOptions = (markerOptions.states && markerOptions.states[state]) || {},
22199 stateDisabled = markerStateOptions.enabled === false,
22200 stateMarkerGraphic = series.stateMarkerGraphic,
22201 pointMarker = point.marker || {},
22202 chart = series.chart,
22203 radius,
22204 halo = series.halo,
22205 haloOptions,
22206 attribs,
22207 newSymbol;
22208
22209 state = state || ''; // empty string
22210
22211 if (
22212 // already has this state
22213 (state === point.state && !move) ||
22214 // selected points don't respond to hover
22215 (point.selected && state !== 'select') ||
22216 // series' state options is disabled
22217 (stateOptions.enabled === false) ||
22218 // general point marker's state options is disabled
22219 (state && (stateDisabled || (normalDisabled && markerStateOptions.enabled === false))) ||
22220 // individual point marker's state options is disabled
22221 (state && pointMarker.states && pointMarker.states[state] && pointMarker.states[state].enabled === false) // #1610
22222
22223 ) {
22224 return;
22225 }
22226
22227 radius = markerStateOptions.radius || (markerOptions.radius + (markerStateOptions.radiusPlus || 0));
22228
22229 // Apply hover styles to the existing point
22230 if (point.graphic) {
22231
22232 if (point.state) {
22233 point.graphic.removeClass('highcharts-point-' + point.state);
22234 }
22235 if (state) {
22236 point.graphic.addClass('highcharts-point-' + state);
22237 }
22238
22239 attribs = radius ? { // new symbol attributes (#507, #612)
22240 x: plotX - radius,
22241 y: plotY - radius,
22242 width: 2 * radius,
22243 height: 2 * radius
22244 } : {};
22245
22246
22247 attribs = merge(series.pointAttribs(point, state), attribs);
22248
22249
22250 point.graphic.attr(attribs);
22251
22252 // Zooming in from a range with no markers to a range with markers
22253 if (stateMarkerGraphic) {
22254 stateMarkerGraphic.hide();
22255 }
22256 } else {
22257 // if a graphic is not applied to each point in the normal state, create a shared
22258 // graphic for the hover state
22259 if (state && markerStateOptions) {
22260 newSymbol = pointMarker.symbol || series.symbol;
22261
22262 // If the point has another symbol than the previous one, throw away the
22263 // state marker graphic and force a new one (#1459)
22264 if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) {
22265 stateMarkerGraphic = stateMarkerGraphic.destroy();
22266 }
22267
22268 // Add a new state marker graphic
22269 if (!stateMarkerGraphic) {
22270 if (newSymbol) {
22271 series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol(
22272 newSymbol,
22273 plotX - radius,
22274 plotY - radius,
22275 2 * radius,
22276 2 * radius
22277 )
22278 .add(series.markerGroup);
22279 stateMarkerGraphic.currentSymbol = newSymbol;
22280 }
22281
22282 // Move the existing graphic
22283 } else {
22284 stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054
22285 x: plotX - radius,
22286 y: plotY - radius
22287 });
22288 }
22289
22290 if (stateMarkerGraphic) {
22291 stateMarkerGraphic.attr(series.pointAttribs(point, state));
22292 }
22293
22294 }
22295
22296 if (stateMarkerGraphic) {
22297 stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? 'show' : 'hide'](); // #2450
22298 stateMarkerGraphic.element.point = point; // #4310
22299 }
22300 }
22301
22302 // Show me your halo
22303 haloOptions = stateOptions.halo;
22304 if (haloOptions && haloOptions.size) {
22305 if (!halo) {
22306 series.halo = halo = chart.renderer.path()
22307 .add(chart.seriesGroup);
22308 }
22309 halo[move ? 'animate' : 'attr']({
22310 d: point.haloPath(haloOptions.size)
22311 });
22312 halo.attr({
22313 'class': 'highcharts-halo highcharts-color-' + pick(point.colorIndex, series.colorIndex)
22314 });
22315
22316
22317 halo.attr(extend({
22318 'fill': point.color || series.color,
22319 'fill-opacity': haloOptions.opacity,
22320 'zIndex': -1 // #4929, IE8 added halo above everything
22321 },
22322 haloOptions.attributes))[move ? 'animate' : 'attr']({
22323 d: point.haloPath(haloOptions.size)
22324 });
22325
22326 } else if (halo) {
22327 halo.attr({
22328 d: []
22329 });
22330 }
22331
22332 point.state = state;
22333 },
22334
22335 /**
22336 * Get the circular path definition for the halo
22337 * @param {Number} size The radius of the circular halo
22338 * @returns {Array} The path definition
22339 */
22340 haloPath: function(size) {
22341 var series = this.series,
22342 chart = series.chart,
22343 plotBox = series.getPlotBox(),
22344 inverted = chart.inverted,
22345 plotX = Math.floor(this.plotX);
22346
22347 return chart.renderer.symbols.circle(
22348 plotBox.translateX + (inverted ? series.yAxis.len - this.plotY : plotX) - size,
22349 plotBox.translateY + (inverted ? series.xAxis.len - plotX : this.plotY) - size,
22350 size * 2,
22351 size * 2
22352 );
22353 }
22354 });
22355
22356 /*
22357 * Extend the Series object with interaction
22358 */
22359
22360 extend(Series.prototype, {
22361 /**
22362 * Series mouse over handler
22363 */
22364 onMouseOver: function() {
22365 var series = this,
22366 chart = series.chart,
22367 hoverSeries = chart.hoverSeries;
22368
22369 // set normal state to previous series
22370 if (hoverSeries && hoverSeries !== series) {
22371 hoverSeries.onMouseOut();
22372 }
22373
22374 // trigger the event, but to save processing time,
22375 // only if defined
22376 if (series.options.events.mouseOver) {
22377 fireEvent(series, 'mouseOver');
22378 }
22379
22380 // hover this
22381 series.setState('hover');
22382 chart.hoverSeries = series;
22383 },
22384
22385 /**
22386 * Series mouse out handler
22387 */
22388 onMouseOut: function() {
22389 // trigger the event only if listeners exist
22390 var series = this,
22391 options = series.options,
22392 chart = series.chart,
22393 tooltip = chart.tooltip,
22394 hoverPoint = chart.hoverPoint;
22395
22396 chart.hoverSeries = null; // #182, set to null before the mouseOut event fires
22397
22398 // trigger mouse out on the point, which must be in this series
22399 if (hoverPoint) {
22400 hoverPoint.onMouseOut();
22401 }
22402
22403 // fire the mouse out event
22404 if (series && options.events.mouseOut) {
22405 fireEvent(series, 'mouseOut');
22406 }
22407
22408
22409 // hide the tooltip
22410 if (tooltip && !options.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) {
22411 tooltip.hide();
22412 }
22413
22414 // set normal state
22415 series.setState();
22416 },
22417
22418 /**
22419 * Set the state of the graph
22420 */
22421 setState: function(state) {
22422 var series = this,
22423 options = series.options,
22424 graph = series.graph,
22425 stateOptions = options.states,
22426 lineWidth = options.lineWidth,
22427 attribs,
22428 i = 0;
22429
22430 state = state || '';
22431
22432 if (series.state !== state) {
22433
22434 // Toggle class names
22435 each([series.group, series.markerGroup], function(group) {
22436 if (group) {
22437 // Old state
22438 if (series.state) {
22439 group.removeClass('highcharts-series-' + series.state);
22440 }
22441 // New state
22442 if (state) {
22443 group.addClass('highcharts-series-' + state);
22444 }
22445 }
22446 });
22447
22448 series.state = state;
22449
22450
22451
22452 if (stateOptions[state] && stateOptions[state].enabled === false) {
22453 return;
22454 }
22455
22456 if (state) {
22457 lineWidth = stateOptions[state].lineWidth || lineWidth + (stateOptions[state].lineWidthPlus || 0); // #4035
22458 }
22459
22460 if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML
22461 attribs = {
22462 'stroke-width': lineWidth
22463 };
22464 // use attr because animate will cause any other animation on the graph to stop
22465 graph.attr(attribs);
22466 while (series['zone-graph-' + i]) {
22467 series['zone-graph-' + i].attr(attribs);
22468 i = i + 1;
22469 }
22470 }
22471
22472 }
22473 },
22474
22475 /**
22476 * Set the visibility of the graph
22477 *
22478 * @param vis {Boolean} True to show the series, false to hide. If undefined,
22479 * the visibility is toggled.
22480 */
22481 setVisible: function(vis, redraw) {
22482 var series = this,
22483 chart = series.chart,
22484 legendItem = series.legendItem,
22485 showOrHide,
22486 ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries,
22487 oldVisibility = series.visible;
22488
22489 // if called without an argument, toggle visibility
22490 series.visible = vis = series.options.visible = series.userOptions.visible = vis === undefined ? !oldVisibility : vis; // #5618
22491 showOrHide = vis ? 'show' : 'hide';
22492
22493 // show or hide elements
22494 each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker'], function(key) {
22495 if (series[key]) {
22496 series[key][showOrHide]();
22497 }
22498 });
22499
22500
22501 // hide tooltip (#1361)
22502 if (chart.hoverSeries === series || (chart.hoverPoint && chart.hoverPoint.series) === series) {
22503 series.onMouseOut();
22504 }
22505
22506
22507 if (legendItem) {
22508 chart.legend.colorizeItem(series, vis);
22509 }
22510
22511
22512 // rescale or adapt to resized chart
22513 series.isDirty = true;
22514 // in a stack, all other series are affected
22515 if (series.options.stacking) {
22516 each(chart.series, function(otherSeries) {
22517 if (otherSeries.options.stacking && otherSeries.visible) {
22518 otherSeries.isDirty = true;
22519 }
22520 });
22521 }
22522
22523 // show or hide linked series
22524 each(series.linkedSeries, function(otherSeries) {
22525 otherSeries.setVisible(vis, false);
22526 });
22527
22528 if (ignoreHiddenSeries) {
22529 chart.isDirtyBox = true;
22530 }
22531 if (redraw !== false) {
22532 chart.redraw();
22533 }
22534
22535 fireEvent(series, showOrHide);
22536 },
22537
22538 /**
22539 * Show the graph
22540 */
22541 show: function() {
22542 this.setVisible(true);
22543 },
22544
22545 /**
22546 * Hide the graph
22547 */
22548 hide: function() {
22549 this.setVisible(false);
22550 },
22551
22552
22553 /**
22554 * Set the selected state of the graph
22555 *
22556 * @param selected {Boolean} True to select the series, false to unselect. If
22557 * undefined, the selection state is toggled.
22558 */
22559 select: function(selected) {
22560 var series = this;
22561 // if called without an argument, toggle
22562 series.selected = selected = (selected === undefined) ? !series.selected : selected;
22563
22564 if (series.checkbox) {
22565 series.checkbox.checked = selected;
22566 }
22567
22568 fireEvent(series, selected ? 'select' : 'unselect');
22569 },
22570
22571 drawTracker: TrackerMixin.drawTrackerGraph
22572 });
22573
22574 }(Highcharts));
22575 (function(H) {
22576 /**
22577 * (c) 2010-2016 Torstein Honsi
22578 *
22579 * License: www.highcharts.com/license
22580 */
22581 'use strict';
22582 var Chart = H.Chart,
22583 each = H.each,
22584 inArray = H.inArray,
22585 isObject = H.isObject,
22586 pick = H.pick,
22587 splat = H.splat;
22588
22589 /**
22590 * Update the chart based on the current chart/document size and options for responsiveness
22591 */
22592 Chart.prototype.setResponsive = function(redraw) {
22593 var options = this.options.responsive;
22594
22595 if (options && options.rules) {
22596 each(options.rules, function(rule) {
22597 this.matchResponsiveRule(rule, redraw);
22598 }, this);
22599 }
22600 };
22601
22602 /**
22603 * Handle a single responsiveness rule
22604 */
22605 Chart.prototype.matchResponsiveRule = function(rule, redraw) {
22606 var respRules = this.respRules,
22607 condition = rule.condition,
22608 matches,
22609 fn = rule.callback || function() {
22610 return this.chartWidth <= pick(condition.maxWidth, Number.MAX_VALUE) &&
22611 this.chartHeight <= pick(condition.maxHeight, Number.MAX_VALUE) &&
22612 this.chartWidth >= pick(condition.minWidth, 0) &&
22613 this.chartHeight >= pick(condition.minHeight, 0);
22614 };
22615
22616
22617 if (rule._id === undefined) {
22618 rule._id = H.idCounter++;
22619 }
22620 matches = fn.call(this);
22621
22622 // Apply a rule
22623 if (!respRules[rule._id] && matches) {
22624
22625 // Store the current state of the options
22626 if (rule.chartOptions) {
22627 respRules[rule._id] = this.currentOptions(rule.chartOptions);
22628 this.update(rule.chartOptions, redraw);
22629 }
22630
22631 // Unapply a rule based on the previous options before the rule
22632 // was applied
22633 } else if (respRules[rule._id] && !matches) {
22634 this.update(respRules[rule._id], redraw);
22635 delete respRules[rule._id];
22636 }
22637 };
22638
22639 /**
22640 * Get the current values for a given set of options. Used before we update
22641 * the chart with a new responsiveness rule.
22642 * TODO: Restore axis options (by id?)
22643 */
22644 Chart.prototype.currentOptions = function(options) {
22645
22646 var ret = {};
22647
22648 /**
22649 * Recurse over a set of options and its current values,
22650 * and store the current values in the ret object.
22651 */
22652 function getCurrent(options, curr, ret) {
22653 var key, i;
22654 for (key in options) {
22655 if (inArray(key, ['series', 'xAxis', 'yAxis']) > -1) {
22656 options[key] = splat(options[key]);
22657
22658 ret[key] = [];
22659 for (i = 0; i < options[key].length; i++) {
22660 ret[key][i] = {};
22661 getCurrent(options[key][i], curr[key][i], ret[key][i]);
22662 }
22663 } else if (isObject(options[key])) {
22664 ret[key] = {};
22665 getCurrent(options[key], curr[key] || {}, ret[key]);
22666 } else {
22667 ret[key] = curr[key] || null;
22668 }
22669 }
22670 }
22671
22672 getCurrent(options, this.options, ret);
22673 return ret;
22674 };
22675
22676 }(Highcharts));
22677 return Highcharts
22678}));