UNPKG

856 kBJavaScriptView Raw
1/**
2 * @license Highcharts JS v5.0.0 (2016-09-29)
3 *
4 * (c) 2009-2016 Torstein Honsi
5 *
6 * License: www.highcharts.com/license
7 */
8'use strict';
9(function(root, factory) {
10 if (typeof module === 'object' && module.exports) {
11 module.exports = root.document ?
12 factory(root) :
13 factory;
14 } else {
15 root.Highcharts = factory(root);
16 }
17}(typeof window !== 'undefined' ? window : this, function(win) {
18 var Highcharts = (function() {
19 /**
20 * (c) 2010-2016 Torstein Honsi
21 *
22 * License: www.highcharts.com/license
23 */
24 'use strict';
25 /* global window */
26 var win = window,
27 doc = win.document;
28
29 var SVG_NS = 'http://www.w3.org/2000/svg',
30 userAgent = (win.navigator && win.navigator.userAgent) || '',
31 svg = doc && doc.createElementNS && !!doc.createElementNS(SVG_NS, 'svg').createSVGRect,
32 isMS = /(edge|msie|trident)/i.test(userAgent) && !window.opera,
33 vml = !svg,
34 isFirefox = /Firefox/.test(userAgent),
35 hasBidiBug = isFirefox && parseInt(userAgent.split('Firefox/')[1], 10) < 4; // issue #38
36
37 var Highcharts = win.Highcharts ? win.Highcharts.error(16, true) : {
38 product: 'Highcharts',
39 version: '5.0.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 SVGElement,
1727 SVGRenderer,
1728
1729 addEvent = H.addEvent,
1730 animate = H.animate,
1731 attr = H.attr,
1732 charts = H.charts,
1733 color = H.color,
1734 css = H.css,
1735 createElement = H.createElement,
1736 defined = H.defined,
1737 deg2rad = H.deg2rad,
1738 destroyObjectProperties = H.destroyObjectProperties,
1739 doc = H.doc,
1740 each = H.each,
1741 extend = H.extend,
1742 erase = H.erase,
1743 grep = H.grep,
1744 hasTouch = H.hasTouch,
1745 isArray = H.isArray,
1746 isFirefox = H.isFirefox,
1747 isMS = H.isMS,
1748 isObject = H.isObject,
1749 isString = H.isString,
1750 isWebKit = H.isWebKit,
1751 merge = H.merge,
1752 noop = H.noop,
1753 pick = H.pick,
1754 pInt = H.pInt,
1755 removeEvent = H.removeEvent,
1756 splat = H.splat,
1757 stop = H.stop,
1758 svg = H.svg,
1759 SVG_NS = H.SVG_NS,
1760 win = H.win;
1761
1762 /**
1763 * A wrapper object for SVG elements
1764 */
1765 SVGElement = H.SVGElement = function() {
1766 return this;
1767 };
1768 SVGElement.prototype = {
1769
1770 // Default base for animation
1771 opacity: 1,
1772 SVG_NS: SVG_NS,
1773 // For labels, these CSS properties are applied to the <text> node directly
1774 textProps: ['direction', 'fontSize', 'fontWeight', 'fontFamily', 'fontStyle', 'color',
1775 'lineHeight', 'width', 'textDecoration', 'textOverflow', 'textShadow'
1776 ],
1777
1778 /**
1779 * Initialize the SVG renderer
1780 * @param {Object} renderer
1781 * @param {String} nodeName
1782 */
1783 init: function(renderer, nodeName) {
1784 var wrapper = this;
1785 wrapper.element = nodeName === 'span' ?
1786 createElement(nodeName) :
1787 doc.createElementNS(wrapper.SVG_NS, nodeName);
1788 wrapper.renderer = renderer;
1789 },
1790
1791 /**
1792 * Animate a given attribute
1793 * @param {Object} params
1794 * @param {Number} options Options include duration, easing, step and complete
1795 * @param {Function} complete Function to perform at the end of animation
1796 */
1797 animate: function(params, options, complete) {
1798 var animOptions = pick(options, this.renderer.globalAnimation, true);
1799 stop(this); // stop regardless of animation actually running, or reverting to .attr (#607)
1800 if (animOptions) {
1801 if (complete) { // allows using a callback with the global animation without overwriting it
1802 animOptions.complete = complete;
1803 }
1804 animate(this, params, animOptions);
1805 } else {
1806 this.attr(params, null, complete);
1807 }
1808 return this;
1809 },
1810
1811 /**
1812 * Build an SVG gradient out of a common JavaScript configuration object
1813 */
1814 colorGradient: function(color, prop, elem) {
1815 var renderer = this.renderer,
1816 colorObject,
1817 gradName,
1818 gradAttr,
1819 radAttr,
1820 gradients,
1821 gradientObject,
1822 stops,
1823 stopColor,
1824 stopOpacity,
1825 radialReference,
1826 n,
1827 id,
1828 key = [],
1829 value;
1830
1831 // Apply linear or radial gradients
1832 if (color.linearGradient) {
1833 gradName = 'linearGradient';
1834 } else if (color.radialGradient) {
1835 gradName = 'radialGradient';
1836 }
1837
1838 if (gradName) {
1839 gradAttr = color[gradName];
1840 gradients = renderer.gradients;
1841 stops = color.stops;
1842 radialReference = elem.radialReference;
1843
1844 // Keep < 2.2 kompatibility
1845 if (isArray(gradAttr)) {
1846 color[gradName] = gradAttr = {
1847 x1: gradAttr[0],
1848 y1: gradAttr[1],
1849 x2: gradAttr[2],
1850 y2: gradAttr[3],
1851 gradientUnits: 'userSpaceOnUse'
1852 };
1853 }
1854
1855 // Correct the radial gradient for the radial reference system
1856 if (gradName === 'radialGradient' && radialReference && !defined(gradAttr.gradientUnits)) {
1857 radAttr = gradAttr; // Save the radial attributes for updating
1858 gradAttr = merge(gradAttr,
1859 renderer.getRadialAttr(radialReference, radAttr), {
1860 gradientUnits: 'userSpaceOnUse'
1861 }
1862 );
1863 }
1864
1865 // Build the unique key to detect whether we need to create a new element (#1282)
1866 for (n in gradAttr) {
1867 if (n !== 'id') {
1868 key.push(n, gradAttr[n]);
1869 }
1870 }
1871 for (n in stops) {
1872 key.push(stops[n]);
1873 }
1874 key = key.join(',');
1875
1876 // Check if a gradient object with the same config object is created within this renderer
1877 if (gradients[key]) {
1878 id = gradients[key].attr('id');
1879
1880 } else {
1881
1882 // Set the id and create the element
1883 gradAttr.id = id = 'highcharts-' + H.idCounter++;
1884 gradients[key] = gradientObject = renderer.createElement(gradName)
1885 .attr(gradAttr)
1886 .add(renderer.defs);
1887
1888 gradientObject.radAttr = radAttr;
1889
1890 // The gradient needs to keep a list of stops to be able to destroy them
1891 gradientObject.stops = [];
1892 each(stops, function(stop) {
1893 var stopObject;
1894 if (stop[1].indexOf('rgba') === 0) {
1895 colorObject = H.color(stop[1]);
1896 stopColor = colorObject.get('rgb');
1897 stopOpacity = colorObject.get('a');
1898 } else {
1899 stopColor = stop[1];
1900 stopOpacity = 1;
1901 }
1902 stopObject = renderer.createElement('stop').attr({
1903 offset: stop[0],
1904 'stop-color': stopColor,
1905 'stop-opacity': stopOpacity
1906 }).add(gradientObject);
1907
1908 // Add the stop element to the gradient
1909 gradientObject.stops.push(stopObject);
1910 });
1911 }
1912
1913 // Set the reference to the gradient object
1914 value = 'url(' + renderer.url + '#' + id + ')';
1915 elem.setAttribute(prop, value);
1916 elem.gradient = key;
1917
1918 // Allow the color to be concatenated into tooltips formatters etc. (#2995)
1919 color.toString = function() {
1920 return value;
1921 };
1922 }
1923 },
1924
1925 /**
1926 * Apply a polyfill to the text-stroke CSS property, by copying the text element
1927 * and apply strokes to the copy.
1928 *
1929 * Contrast checks at http://jsfiddle.net/highcharts/43soe9m1/2/
1930 */
1931 applyTextShadow: function(textShadow) {
1932 var elem = this.element,
1933 tspans,
1934 hasContrast = textShadow.indexOf('contrast') !== -1,
1935 styles = {},
1936 forExport = this.renderer.forExport,
1937 // IE10 and IE11 report textShadow in elem.style even though it doesn't work. Check
1938 // this again with new IE release. In exports, the rendering is passed to PhantomJS.
1939 supports = this.renderer.forExport || (elem.style.textShadow !== undefined && !isMS);
1940
1941 // When the text shadow is set to contrast, use dark stroke for light text and vice versa
1942 if (hasContrast) {
1943 styles.textShadow = textShadow = textShadow.replace(/contrast/g, this.renderer.getContrast(elem.style.fill));
1944 }
1945
1946 // Safari with retina displays as well as PhantomJS bug (#3974). Firefox does not tolerate this,
1947 // it removes the text shadows.
1948 if (isWebKit || forExport) {
1949 styles.textRendering = 'geometricPrecision';
1950 }
1951
1952 /* Selective side-by-side testing in supported browser (http://jsfiddle.net/highcharts/73L1ptrh/)
1953 if (elem.textContent.indexOf('2.') === 0) {
1954 elem.style['text-shadow'] = 'none';
1955 supports = false;
1956 }
1957 // */
1958
1959 // No reason to polyfill, we've got native support
1960 if (supports) {
1961 this.css(styles); // Apply altered textShadow or textRendering workaround
1962 } else {
1963
1964 this.fakeTS = true; // Fake text shadow
1965
1966 // In order to get the right y position of the clones,
1967 // copy over the y setter
1968 this.ySetter = this.xSetter;
1969
1970 tspans = [].slice.call(elem.getElementsByTagName('tspan'));
1971 each(textShadow.split(/\s?,\s?/g), function(textShadow) {
1972 var firstChild = elem.firstChild,
1973 color,
1974 strokeWidth;
1975
1976 textShadow = textShadow.split(' ');
1977 color = textShadow[textShadow.length - 1];
1978
1979 // Approximately tune the settings to the text-shadow behaviour
1980 strokeWidth = textShadow[textShadow.length - 2];
1981
1982 if (strokeWidth) {
1983 each(tspans, function(tspan, y) {
1984 var clone;
1985
1986 // Let the first line start at the correct X position
1987 if (y === 0) {
1988 tspan.setAttribute('x', elem.getAttribute('x'));
1989 y = elem.getAttribute('y');
1990 tspan.setAttribute('y', y || 0);
1991 if (y === null) {
1992 elem.setAttribute('y', 0);
1993 }
1994 }
1995
1996 // Create the clone and apply shadow properties
1997 clone = tspan.cloneNode(1);
1998 attr(clone, {
1999 'class': 'highcharts-text-shadow',
2000 'fill': color,
2001 'stroke': color,
2002 'stroke-opacity': 1 / Math.max(pInt(strokeWidth), 3),
2003 'stroke-width': strokeWidth,
2004 'stroke-linejoin': 'round'
2005 });
2006 elem.insertBefore(clone, firstChild);
2007 });
2008 }
2009 });
2010 }
2011 },
2012
2013 /**
2014 * Set or get a given attribute
2015 * @param {Object|String} hash
2016 * @param {Mixed|Undefined} val
2017 */
2018 attr: function(hash, val, complete) {
2019 var key,
2020 value,
2021 element = this.element,
2022 hasSetSymbolSize,
2023 ret = this,
2024 skipAttr,
2025 setter;
2026
2027 // single key-value pair
2028 if (typeof hash === 'string' && val !== undefined) {
2029 key = hash;
2030 hash = {};
2031 hash[key] = val;
2032 }
2033
2034 // used as a getter: first argument is a string, second is undefined
2035 if (typeof hash === 'string') {
2036 ret = (this[hash + 'Getter'] || this._defaultGetter).call(this, hash, element);
2037
2038 // setter
2039 } else {
2040
2041 for (key in hash) {
2042 value = hash[key];
2043 skipAttr = false;
2044
2045
2046
2047 if (this.symbolName && /^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)/.test(key)) {
2048 if (!hasSetSymbolSize) {
2049 this.symbolAttr(hash);
2050 hasSetSymbolSize = true;
2051 }
2052 skipAttr = true;
2053 }
2054
2055 if (this.rotation && (key === 'x' || key === 'y')) {
2056 this.doTransform = true;
2057 }
2058
2059 if (!skipAttr) {
2060 setter = this[key + 'Setter'] || this._defaultSetter;
2061 setter.call(this, value, key, element);
2062
2063
2064 // Let the shadow follow the main element
2065 if (this.shadows && /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(key)) {
2066 this.updateShadows(key, value, setter);
2067 }
2068
2069 }
2070 }
2071
2072 // Update transform. Do this outside the loop to prevent redundant updating for batch setting
2073 // of attributes.
2074 if (this.doTransform) {
2075 this.updateTransform();
2076 this.doTransform = false;
2077 }
2078
2079 }
2080
2081 // In accordance with animate, run a complete callback
2082 if (complete) {
2083 complete();
2084 }
2085
2086 return ret;
2087 },
2088
2089
2090 /**
2091 * Update the shadow elements with new attributes
2092 * @param {String} key The attribute name
2093 * @param {String|Number} value The value of the attribute
2094 * @param {Function} setter The setter function, inherited from the parent wrapper
2095 * @returns {undefined}
2096 */
2097 updateShadows: function(key, value, setter) {
2098 var shadows = this.shadows,
2099 i = shadows.length;
2100
2101 while (i--) {
2102 setter.call(
2103 shadows[i],
2104 key === 'height' ?
2105 Math.max(value - (shadows[i].cutHeight || 0), 0) :
2106 key === 'd' ? this.d : value,
2107 key,
2108 shadows[i]
2109 );
2110 }
2111 },
2112
2113
2114 /**
2115 * Add a class name to an element
2116 */
2117 addClass: function(className, replace) {
2118 var currentClassName = this.attr('class') || '';
2119
2120 if (currentClassName.indexOf(className) === -1) {
2121 if (!replace) {
2122 className = (currentClassName + (currentClassName ? ' ' : '') + className).replace(' ', ' ');
2123 }
2124 this.attr('class', className);
2125 }
2126 return this;
2127 },
2128 hasClass: function(className) {
2129 return attr(this.element, 'class').indexOf(className) !== -1;
2130 },
2131 removeClass: function(className) {
2132 attr(this.element, 'class', (attr(this.element, 'class') || '').replace(className, ''));
2133 return this;
2134 },
2135
2136 /**
2137 * If one of the symbol size affecting parameters are changed,
2138 * check all the others only once for each call to an element's
2139 * .attr() method
2140 * @param {Object} hash
2141 */
2142 symbolAttr: function(hash) {
2143 var wrapper = this;
2144
2145 each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR', 'anchorX', 'anchorY'], function(key) {
2146 wrapper[key] = pick(hash[key], wrapper[key]);
2147 });
2148
2149 wrapper.attr({
2150 d: wrapper.renderer.symbols[wrapper.symbolName](
2151 wrapper.x,
2152 wrapper.y,
2153 wrapper.width,
2154 wrapper.height,
2155 wrapper
2156 )
2157 });
2158 },
2159
2160 /**
2161 * Apply a clipping path to this object
2162 * @param {String} id
2163 */
2164 clip: function(clipRect) {
2165 return this.attr('clip-path', clipRect ? 'url(' + this.renderer.url + '#' + clipRect.id + ')' : 'none');
2166 },
2167
2168 /**
2169 * Calculate the coordinates needed for drawing a rectangle crisply and return the
2170 * calculated attributes
2171 * @param {Number} strokeWidth
2172 * @param {Number} x
2173 * @param {Number} y
2174 * @param {Number} width
2175 * @param {Number} height
2176 */
2177 crisp: function(rect, strokeWidth) {
2178
2179 var wrapper = this,
2180 key,
2181 attribs = {},
2182 normalizer;
2183
2184 strokeWidth = strokeWidth || rect.strokeWidth || 0;
2185 normalizer = Math.round(strokeWidth) % 2 / 2; // Math.round because strokeWidth can sometimes have roundoff errors
2186
2187 // normalize for crisp edges
2188 rect.x = Math.floor(rect.x || wrapper.x || 0) + normalizer;
2189 rect.y = Math.floor(rect.y || wrapper.y || 0) + normalizer;
2190 rect.width = Math.floor((rect.width || wrapper.width || 0) - 2 * normalizer);
2191 rect.height = Math.floor((rect.height || wrapper.height || 0) - 2 * normalizer);
2192 if (defined(rect.strokeWidth)) {
2193 rect.strokeWidth = strokeWidth;
2194 }
2195
2196 for (key in rect) {
2197 if (wrapper[key] !== rect[key]) { // only set attribute if changed
2198 wrapper[key] = attribs[key] = rect[key];
2199 }
2200 }
2201
2202 return attribs;
2203 },
2204
2205 /**
2206 * Set styles for the element
2207 * @param {Object} styles
2208 */
2209 css: function(styles) {
2210 var elemWrapper = this,
2211 oldStyles = elemWrapper.styles,
2212 newStyles = {},
2213 elem = elemWrapper.element,
2214 textWidth,
2215 n,
2216 serializedCss = '',
2217 hyphenate,
2218 hasNew = !oldStyles;
2219
2220 // convert legacy
2221 if (styles && styles.color) {
2222 styles.fill = styles.color;
2223 }
2224
2225 // Filter out existing styles to increase performance (#2640)
2226 if (oldStyles) {
2227 for (n in styles) {
2228 if (styles[n] !== oldStyles[n]) {
2229 newStyles[n] = styles[n];
2230 hasNew = true;
2231 }
2232 }
2233 }
2234 if (hasNew) {
2235 textWidth = elemWrapper.textWidth =
2236 (styles && styles.width && elem.nodeName.toLowerCase() === 'text' && pInt(styles.width)) ||
2237 elemWrapper.textWidth; // #3501
2238
2239 // Merge the new styles with the old ones
2240 if (oldStyles) {
2241 styles = extend(
2242 oldStyles,
2243 newStyles
2244 );
2245 }
2246
2247 // store object
2248 elemWrapper.styles = styles;
2249
2250 if (textWidth && (!svg && elemWrapper.renderer.forExport)) {
2251 delete styles.width;
2252 }
2253
2254 // serialize and set style attribute
2255 if (isMS && !svg) {
2256 css(elemWrapper.element, styles);
2257 } else {
2258 hyphenate = function(a, b) {
2259 return '-' + b.toLowerCase();
2260 };
2261 for (n in styles) {
2262 serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + styles[n] + ';';
2263 }
2264 attr(elem, 'style', serializedCss); // #1881
2265 }
2266
2267
2268 // Rebuild text after added
2269 if (elemWrapper.added && textWidth) {
2270 elemWrapper.renderer.buildText(elemWrapper);
2271 }
2272 }
2273
2274 return elemWrapper;
2275 },
2276
2277
2278 strokeWidth: function() {
2279 return this['stroke-width'] || 0;
2280 },
2281
2282
2283 /**
2284 * Add an event listener
2285 * @param {String} eventType
2286 * @param {Function} handler
2287 */
2288 on: function(eventType, handler) {
2289 var svgElement = this,
2290 element = svgElement.element;
2291
2292 // touch
2293 if (hasTouch && eventType === 'click') {
2294 element.ontouchstart = function(e) {
2295 svgElement.touchEventFired = Date.now();
2296 e.preventDefault();
2297 handler.call(element, e);
2298 };
2299 element.onclick = function(e) {
2300 if (win.navigator.userAgent.indexOf('Android') === -1 || Date.now() - (svgElement.touchEventFired || 0) > 1100) { // #2269
2301 handler.call(element, e);
2302 }
2303 };
2304 } else {
2305 // simplest possible event model for internal use
2306 element['on' + eventType] = handler;
2307 }
2308 return this;
2309 },
2310
2311 /**
2312 * Set the coordinates needed to draw a consistent radial gradient across
2313 * pie slices regardless of positioning inside the chart. The format is
2314 * [centerX, centerY, diameter] in pixels.
2315 */
2316 setRadialReference: function(coordinates) {
2317 var existingGradient = this.renderer.gradients[this.element.gradient];
2318
2319 this.element.radialReference = coordinates;
2320
2321 // On redrawing objects with an existing gradient, the gradient needs
2322 // to be repositioned (#3801)
2323 if (existingGradient && existingGradient.radAttr) {
2324 existingGradient.animate(
2325 this.renderer.getRadialAttr(
2326 coordinates,
2327 existingGradient.radAttr
2328 )
2329 );
2330 }
2331
2332 return this;
2333 },
2334
2335 /**
2336 * Move an object and its children by x and y values
2337 * @param {Number} x
2338 * @param {Number} y
2339 */
2340 translate: function(x, y) {
2341 return this.attr({
2342 translateX: x,
2343 translateY: y
2344 });
2345 },
2346
2347 /**
2348 * Invert a group, rotate and flip
2349 */
2350 invert: function(inverted) {
2351 var wrapper = this;
2352 wrapper.inverted = inverted;
2353 wrapper.updateTransform();
2354 return wrapper;
2355 },
2356
2357 /**
2358 * Private method to update the transform attribute based on internal
2359 * properties
2360 */
2361 updateTransform: function() {
2362 var wrapper = this,
2363 translateX = wrapper.translateX || 0,
2364 translateY = wrapper.translateY || 0,
2365 scaleX = wrapper.scaleX,
2366 scaleY = wrapper.scaleY,
2367 inverted = wrapper.inverted,
2368 rotation = wrapper.rotation,
2369 element = wrapper.element,
2370 transform;
2371
2372 // flipping affects translate as adjustment for flipping around the group's axis
2373 if (inverted) {
2374 translateX += wrapper.attr('width');
2375 translateY += wrapper.attr('height');
2376 }
2377
2378 // Apply translate. Nearly all transformed elements have translation, so instead
2379 // of checking for translate = 0, do it always (#1767, #1846).
2380 transform = ['translate(' + translateX + ',' + translateY + ')'];
2381
2382 // apply rotation
2383 if (inverted) {
2384 transform.push('rotate(90) scale(-1,1)');
2385 } else if (rotation) { // text rotation
2386 transform.push('rotate(' + rotation + ' ' + (element.getAttribute('x') || 0) + ' ' + (element.getAttribute('y') || 0) + ')');
2387
2388 // Delete bBox memo when the rotation changes
2389 //delete wrapper.bBox;
2390 }
2391
2392 // apply scale
2393 if (defined(scaleX) || defined(scaleY)) {
2394 transform.push('scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')');
2395 }
2396
2397 if (transform.length) {
2398 element.setAttribute('transform', transform.join(' '));
2399 }
2400 },
2401 /**
2402 * Bring the element to the front
2403 */
2404 toFront: function() {
2405 var element = this.element;
2406 element.parentNode.appendChild(element);
2407 return this;
2408 },
2409
2410
2411 /**
2412 * Break down alignment options like align, verticalAlign, x and y
2413 * to x and y relative to the chart.
2414 *
2415 * @param {Object} alignOptions
2416 * @param {Boolean} alignByTranslate
2417 * @param {String[Object} box The box to align to, needs a width and height. When the
2418 * box is a string, it refers to an object in the Renderer. For example, when
2419 * box is 'spacingBox', it refers to Renderer.spacingBox which holds width, height
2420 * x and y properties.
2421 *
2422 */
2423 align: function(alignOptions, alignByTranslate, box) {
2424 var align,
2425 vAlign,
2426 x,
2427 y,
2428 attribs = {},
2429 alignTo,
2430 renderer = this.renderer,
2431 alignedObjects = renderer.alignedObjects,
2432 alignFactor,
2433 vAlignFactor;
2434
2435 // First call on instanciate
2436 if (alignOptions) {
2437 this.alignOptions = alignOptions;
2438 this.alignByTranslate = alignByTranslate;
2439 if (!box || isString(box)) { // boxes other than renderer handle this internally
2440 this.alignTo = alignTo = box || 'renderer';
2441 erase(alignedObjects, this); // prevent duplicates, like legendGroup after resize
2442 alignedObjects.push(this);
2443 box = null; // reassign it below
2444 }
2445
2446 // When called on resize, no arguments are supplied
2447 } else {
2448 alignOptions = this.alignOptions;
2449 alignByTranslate = this.alignByTranslate;
2450 alignTo = this.alignTo;
2451 }
2452
2453 box = pick(box, renderer[alignTo], renderer);
2454
2455 // Assign variables
2456 align = alignOptions.align;
2457 vAlign = alignOptions.verticalAlign;
2458 x = (box.x || 0) + (alignOptions.x || 0); // default: left align
2459 y = (box.y || 0) + (alignOptions.y || 0); // default: top align
2460
2461 // Align
2462 if (align === 'right') {
2463 alignFactor = 1;
2464 } else if (align === 'center') {
2465 alignFactor = 2;
2466 }
2467 if (alignFactor) {
2468 x += (box.width - (alignOptions.width || 0)) / alignFactor;
2469 }
2470 attribs[alignByTranslate ? 'translateX' : 'x'] = Math.round(x);
2471
2472
2473 // Vertical align
2474 if (vAlign === 'bottom') {
2475 vAlignFactor = 1;
2476 } else if (vAlign === 'middle') {
2477 vAlignFactor = 2;
2478 }
2479 if (vAlignFactor) {
2480 y += (box.height - (alignOptions.height || 0)) / vAlignFactor;
2481 }
2482 attribs[alignByTranslate ? 'translateY' : 'y'] = Math.round(y);
2483
2484 // Animate only if already placed
2485 this[this.placed ? 'animate' : 'attr'](attribs);
2486 this.placed = true;
2487 this.alignAttr = attribs;
2488
2489 return this;
2490 },
2491
2492 /**
2493 * Get the bounding box (width, height, x and y) for the element
2494 */
2495 getBBox: function(reload, rot) {
2496 var wrapper = this,
2497 bBox, // = wrapper.bBox,
2498 renderer = wrapper.renderer,
2499 width,
2500 height,
2501 rotation,
2502 rad,
2503 element = wrapper.element,
2504 styles = wrapper.styles,
2505 fontSize,
2506 textStr = wrapper.textStr,
2507 textShadow,
2508 elemStyle = element.style,
2509 toggleTextShadowShim,
2510 cache = renderer.cache,
2511 cacheKeys = renderer.cacheKeys,
2512 cacheKey;
2513
2514 rotation = pick(rot, wrapper.rotation);
2515 rad = rotation * deg2rad;
2516
2517
2518 fontSize = styles && styles.fontSize;
2519
2520
2521 if (textStr !== undefined) {
2522
2523 cacheKey =
2524
2525 // Since numbers are monospaced, and numerical labels appear a lot in a chart,
2526 // we assume that a label of n characters has the same bounding box as others
2527 // of the same length.
2528 textStr.toString().replace(/[0-9]/g, '0') +
2529
2530 // Properties that affect bounding box
2531 ['', rotation || 0, fontSize, element.style.width].join(',');
2532
2533 }
2534
2535 if (cacheKey && !reload) {
2536 bBox = cache[cacheKey];
2537 }
2538
2539 // No cache found
2540 if (!bBox) {
2541
2542 // SVG elements
2543 if (element.namespaceURI === wrapper.SVG_NS || renderer.forExport) {
2544 try { // Fails in Firefox if the container has display: none.
2545
2546 // When the text shadow shim is used, we need to hide the fake shadows
2547 // to get the correct bounding box (#3872)
2548 toggleTextShadowShim = this.fakeTS && function(display) {
2549 each(element.querySelectorAll('.highcharts-text-shadow'), function(tspan) {
2550 tspan.style.display = display;
2551 });
2552 };
2553
2554 // Workaround for #3842, Firefox reporting wrong bounding box for shadows
2555 if (isFirefox && elemStyle.textShadow) {
2556 textShadow = elemStyle.textShadow;
2557 elemStyle.textShadow = '';
2558 } else if (toggleTextShadowShim) {
2559 toggleTextShadowShim('none');
2560 }
2561
2562 bBox = element.getBBox ?
2563 // SVG: use extend because IE9 is not allowed to change width and height in case
2564 // of rotation (below)
2565 extend({}, element.getBBox()) :
2566 // Legacy IE in export mode
2567 {
2568 width: element.offsetWidth,
2569 height: element.offsetHeight
2570 };
2571
2572 // #3842
2573 if (textShadow) {
2574 elemStyle.textShadow = textShadow;
2575 } else if (toggleTextShadowShim) {
2576 toggleTextShadowShim('');
2577 }
2578 } catch (e) {}
2579
2580 // If the bBox is not set, the try-catch block above failed. The other condition
2581 // is for Opera that returns a width of -Infinity on hidden elements.
2582 if (!bBox || bBox.width < 0) {
2583 bBox = {
2584 width: 0,
2585 height: 0
2586 };
2587 }
2588
2589
2590 // VML Renderer or useHTML within SVG
2591 } else {
2592
2593 bBox = wrapper.htmlGetBBox();
2594
2595 }
2596
2597 // True SVG elements as well as HTML elements in modern browsers using the .useHTML option
2598 // need to compensated for rotation
2599 if (renderer.isSVG) {
2600 width = bBox.width;
2601 height = bBox.height;
2602
2603 // Workaround for wrong bounding box in IE9 and IE10 (#1101, #1505, #1669, #2568)
2604 if (isMS && styles && styles.fontSize === '11px' && height.toPrecision(3) === '16.9') {
2605 bBox.height = height = 14;
2606 }
2607
2608 // Adjust for rotated text
2609 if (rotation) {
2610 bBox.width = Math.abs(height * Math.sin(rad)) + Math.abs(width * Math.cos(rad));
2611 bBox.height = Math.abs(height * Math.cos(rad)) + Math.abs(width * Math.sin(rad));
2612 }
2613 }
2614
2615 // Cache it. When loading a chart in a hidden iframe in Firefox and IE/Edge, the
2616 // bounding box height is 0, so don't cache it (#5620).
2617 if (cacheKey && bBox.height > 0) {
2618
2619 // Rotate (#4681)
2620 while (cacheKeys.length > 250) {
2621 delete cache[cacheKeys.shift()];
2622 }
2623
2624 if (!cache[cacheKey]) {
2625 cacheKeys.push(cacheKey);
2626 }
2627 cache[cacheKey] = bBox;
2628 }
2629 }
2630 return bBox;
2631 },
2632
2633 /**
2634 * Show the element
2635 */
2636 show: function(inherit) {
2637 return this.attr({
2638 visibility: inherit ? 'inherit' : 'visible'
2639 });
2640 },
2641
2642 /**
2643 * Hide the element
2644 */
2645 hide: function() {
2646 return this.attr({
2647 visibility: 'hidden'
2648 });
2649 },
2650
2651 fadeOut: function(duration) {
2652 var elemWrapper = this;
2653 elemWrapper.animate({
2654 opacity: 0
2655 }, {
2656 duration: duration || 150,
2657 complete: function() {
2658 elemWrapper.attr({
2659 y: -9999
2660 }); // #3088, assuming we're only using this for tooltips
2661 }
2662 });
2663 },
2664
2665 /**
2666 * Add the element
2667 * @param {Object|Undefined} parent Can be an element, an element wrapper or undefined
2668 * to append the element to the renderer.box.
2669 */
2670 add: function(parent) {
2671
2672 var renderer = this.renderer,
2673 element = this.element,
2674 inserted;
2675
2676 if (parent) {
2677 this.parentGroup = parent;
2678 }
2679
2680 // mark as inverted
2681 this.parentInverted = parent && parent.inverted;
2682
2683 // build formatted text
2684 if (this.textStr !== undefined) {
2685 renderer.buildText(this);
2686 }
2687
2688 // Mark as added
2689 this.added = true;
2690
2691 // If we're adding to renderer root, or other elements in the group
2692 // have a z index, we need to handle it
2693 if (!parent || parent.handleZ || this.zIndex) {
2694 inserted = this.zIndexSetter();
2695 }
2696
2697 // If zIndex is not handled, append at the end
2698 if (!inserted) {
2699 (parent ? parent.element : renderer.box).appendChild(element);
2700 }
2701
2702 // fire an event for internal hooks
2703 if (this.onAdd) {
2704 this.onAdd();
2705 }
2706
2707 return this;
2708 },
2709
2710 /**
2711 * Removes a child either by removeChild or move to garbageBin.
2712 * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not.
2713 */
2714 safeRemoveChild: function(element) {
2715 var parentNode = element.parentNode;
2716 if (parentNode) {
2717 parentNode.removeChild(element);
2718 }
2719 },
2720
2721 /**
2722 * Destroy the element and element wrapper
2723 */
2724 destroy: function() {
2725 var wrapper = this,
2726 element = wrapper.element || {},
2727 parentToClean = wrapper.renderer.isSVG && element.nodeName === 'SPAN' && wrapper.parentGroup,
2728 grandParent,
2729 key,
2730 i;
2731
2732 // remove events
2733 element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = element.point = null;
2734 stop(wrapper); // stop running animations
2735
2736 if (wrapper.clipPath) {
2737 wrapper.clipPath = wrapper.clipPath.destroy();
2738 }
2739
2740 // Destroy stops in case this is a gradient object
2741 if (wrapper.stops) {
2742 for (i = 0; i < wrapper.stops.length; i++) {
2743 wrapper.stops[i] = wrapper.stops[i].destroy();
2744 }
2745 wrapper.stops = null;
2746 }
2747
2748 // remove element
2749 wrapper.safeRemoveChild(element);
2750
2751
2752 wrapper.destroyShadows();
2753
2754
2755 // In case of useHTML, clean up empty containers emulating SVG groups (#1960, #2393, #2697).
2756 while (parentToClean && parentToClean.div && parentToClean.div.childNodes.length === 0) {
2757 grandParent = parentToClean.parentGroup;
2758 wrapper.safeRemoveChild(parentToClean.div);
2759 delete parentToClean.div;
2760 parentToClean = grandParent;
2761 }
2762
2763 // remove from alignObjects
2764 if (wrapper.alignTo) {
2765 erase(wrapper.renderer.alignedObjects, wrapper);
2766 }
2767
2768 for (key in wrapper) {
2769 delete wrapper[key];
2770 }
2771
2772 return null;
2773 },
2774
2775
2776 /**
2777 * Add a shadow to the element. Must be done after the element is added to the DOM
2778 * @param {Boolean|Object} shadowOptions
2779 */
2780 shadow: function(shadowOptions, group, cutOff) {
2781 var shadows = [],
2782 i,
2783 shadow,
2784 element = this.element,
2785 strokeWidth,
2786 shadowWidth,
2787 shadowElementOpacity,
2788
2789 // compensate for inverted plot area
2790 transform;
2791
2792 if (!shadowOptions) {
2793 this.destroyShadows();
2794
2795 } else if (!this.shadows) {
2796 shadowWidth = pick(shadowOptions.width, 3);
2797 shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth;
2798 transform = this.parentInverted ?
2799 '(-1,-1)' :
2800 '(' + pick(shadowOptions.offsetX, 1) + ', ' + pick(shadowOptions.offsetY, 1) + ')';
2801 for (i = 1; i <= shadowWidth; i++) {
2802 shadow = element.cloneNode(0);
2803 strokeWidth = (shadowWidth * 2) + 1 - (2 * i);
2804 attr(shadow, {
2805 'isShadow': 'true',
2806 'stroke': shadowOptions.color || '#000000',
2807 'stroke-opacity': shadowElementOpacity * i,
2808 'stroke-width': strokeWidth,
2809 'transform': 'translate' + transform,
2810 'fill': 'none'
2811 });
2812 if (cutOff) {
2813 attr(shadow, 'height', Math.max(attr(shadow, 'height') - strokeWidth, 0));
2814 shadow.cutHeight = strokeWidth;
2815 }
2816
2817 if (group) {
2818 group.element.appendChild(shadow);
2819 } else {
2820 element.parentNode.insertBefore(shadow, element);
2821 }
2822
2823 shadows.push(shadow);
2824 }
2825
2826 this.shadows = shadows;
2827 }
2828 return this;
2829
2830 },
2831
2832 destroyShadows: function() {
2833 each(this.shadows || [], function(shadow) {
2834 this.safeRemoveChild(shadow);
2835 }, this);
2836 this.shadows = undefined;
2837 },
2838
2839
2840
2841 xGetter: function(key) {
2842 if (this.element.nodeName === 'circle') {
2843 if (key === 'x') {
2844 key = 'cx';
2845 } else if (key === 'y') {
2846 key = 'cy';
2847 }
2848 }
2849 return this._defaultGetter(key);
2850 },
2851
2852 /**
2853 * Get the current value of an attribute or pseudo attribute, used mainly
2854 * for animation.
2855 */
2856 _defaultGetter: function(key) {
2857 var ret = pick(this[key], this.element ? this.element.getAttribute(key) : null, 0);
2858
2859 if (/^[\-0-9\.]+$/.test(ret)) { // is numerical
2860 ret = parseFloat(ret);
2861 }
2862 return ret;
2863 },
2864
2865
2866 dSetter: function(value, key, element) {
2867 if (value && value.join) { // join path
2868 value = value.join(' ');
2869 }
2870 if (/(NaN| {2}|^$)/.test(value)) {
2871 value = 'M 0 0';
2872 }
2873 element.setAttribute(key, value);
2874
2875 this[key] = value;
2876 },
2877
2878 dashstyleSetter: function(value) {
2879 var i,
2880 strokeWidth = this['stroke-width'];
2881
2882 // If "inherit", like maps in IE, assume 1 (#4981). With HC5 and the new strokeWidth
2883 // function, we should be able to use that instead.
2884 if (strokeWidth === 'inherit') {
2885 strokeWidth = 1;
2886 }
2887 value = value && value.toLowerCase();
2888 if (value) {
2889 value = value
2890 .replace('shortdashdotdot', '3,1,1,1,1,1,')
2891 .replace('shortdashdot', '3,1,1,1')
2892 .replace('shortdot', '1,1,')
2893 .replace('shortdash', '3,1,')
2894 .replace('longdash', '8,3,')
2895 .replace(/dot/g, '1,3,')
2896 .replace('dash', '4,3,')
2897 .replace(/,$/, '')
2898 .split(','); // ending comma
2899
2900 i = value.length;
2901 while (i--) {
2902 value[i] = pInt(value[i]) * strokeWidth;
2903 }
2904 value = value.join(',')
2905 .replace(/NaN/g, 'none'); // #3226
2906 this.element.setAttribute('stroke-dasharray', value);
2907 }
2908 },
2909
2910 alignSetter: function(value) {
2911 var convert = {
2912 left: 'start',
2913 center: 'middle',
2914 right: 'end'
2915 };
2916 this.element.setAttribute('text-anchor', convert[value]);
2917 },
2918 titleSetter: function(value) {
2919 var titleNode = this.element.getElementsByTagName('title')[0];
2920 if (!titleNode) {
2921 titleNode = doc.createElementNS(this.SVG_NS, 'title');
2922 this.element.appendChild(titleNode);
2923 }
2924
2925 // Remove text content if it exists
2926 if (titleNode.firstChild) {
2927 titleNode.removeChild(titleNode.firstChild);
2928 }
2929
2930 titleNode.appendChild(
2931 doc.createTextNode(
2932 (String(pick(value), '')).replace(/<[^>]*>/g, '') // #3276, #3895
2933 )
2934 );
2935 },
2936 textSetter: function(value) {
2937 if (value !== this.textStr) {
2938 // Delete bBox memo when the text changes
2939 delete this.bBox;
2940
2941 this.textStr = value;
2942 if (this.added) {
2943 this.renderer.buildText(this);
2944 }
2945 }
2946 },
2947 fillSetter: function(value, key, element) {
2948 if (typeof value === 'string') {
2949 element.setAttribute(key, value);
2950 } else if (value) {
2951 this.colorGradient(value, key, element);
2952 }
2953 },
2954 visibilitySetter: function(value, key, element) {
2955 // IE9-11 doesn't handle visibilty:inherit well, so we remove the attribute instead (#2881, #3909)
2956 if (value === 'inherit') {
2957 element.removeAttribute(key);
2958 } else {
2959 element.setAttribute(key, value);
2960 }
2961 },
2962 zIndexSetter: function(value, key) {
2963 var renderer = this.renderer,
2964 parentGroup = this.parentGroup,
2965 parentWrapper = parentGroup || renderer,
2966 parentNode = parentWrapper.element || renderer.box,
2967 childNodes,
2968 otherElement,
2969 otherZIndex,
2970 element = this.element,
2971 inserted,
2972 run = this.added,
2973 i;
2974
2975 if (defined(value)) {
2976 element.zIndex = value; // So we can read it for other elements in the group
2977 value = +value;
2978 if (this[key] === value) { // Only update when needed (#3865)
2979 run = false;
2980 }
2981 this[key] = value;
2982 }
2983
2984 // Insert according to this and other elements' zIndex. Before .add() is called,
2985 // nothing is done. Then on add, or by later calls to zIndexSetter, the node
2986 // is placed on the right place in the DOM.
2987 if (run) {
2988 value = this.zIndex;
2989
2990 if (value && parentGroup) {
2991 parentGroup.handleZ = true;
2992 }
2993
2994 childNodes = parentNode.childNodes;
2995 for (i = 0; i < childNodes.length && !inserted; i++) {
2996 otherElement = childNodes[i];
2997 otherZIndex = otherElement.zIndex;
2998 if (otherElement !== element && (
2999 // Insert before the first element with a higher zIndex
3000 pInt(otherZIndex) > value ||
3001 // If no zIndex given, insert before the first element with a zIndex
3002 (!defined(value) && defined(otherZIndex))
3003
3004 )) {
3005 parentNode.insertBefore(element, otherElement);
3006 inserted = true;
3007 }
3008 }
3009 if (!inserted) {
3010 parentNode.appendChild(element);
3011 }
3012 }
3013 return inserted;
3014 },
3015 _defaultSetter: function(value, key, element) {
3016 element.setAttribute(key, value);
3017 }
3018 };
3019
3020 // Some shared setters and getters
3021 SVGElement.prototype.yGetter = SVGElement.prototype.xGetter;
3022 SVGElement.prototype.translateXSetter = SVGElement.prototype.translateYSetter =
3023 SVGElement.prototype.rotationSetter = SVGElement.prototype.verticalAlignSetter =
3024 SVGElement.prototype.scaleXSetter = SVGElement.prototype.scaleYSetter = function(value, key) {
3025 this[key] = value;
3026 this.doTransform = true;
3027 };
3028 // These setters both set the key on the instance itself plus as an attribute
3029 SVGElement.prototype.opacitySetter = SVGElement.prototype.displaySetter = function(value, key, element) {
3030 this[key] = value;
3031 element.setAttribute(key, value);
3032 };
3033
3034
3035 // WebKit and Batik have problems with a stroke-width of zero, so in this case we remove the
3036 // stroke attribute altogether. #1270, #1369, #3065, #3072.
3037 SVGElement.prototype['stroke-widthSetter'] = SVGElement.prototype.strokeSetter = function(value, key, element) {
3038 this[key] = value;
3039 // Only apply the stroke attribute if the stroke width is defined and larger than 0
3040 if (this.stroke && this['stroke-width']) {
3041 SVGElement.prototype.fillSetter.call(this, this.stroke, 'stroke', element); // use prototype as instance may be overridden
3042 element.setAttribute('stroke-width', this['stroke-width']);
3043 this.hasStroke = true;
3044 } else if (key === 'stroke-width' && value === 0 && this.hasStroke) {
3045 element.removeAttribute('stroke');
3046 this.hasStroke = false;
3047 }
3048 };
3049
3050
3051 /**
3052 * The default SVG renderer
3053 */
3054 SVGRenderer = H.SVGRenderer = function() {
3055 this.init.apply(this, arguments);
3056 };
3057 SVGRenderer.prototype = {
3058 Element: SVGElement,
3059 SVG_NS: SVG_NS,
3060 /**
3061 * Initialize the SVGRenderer
3062 * @param {Object} container
3063 * @param {Number} width
3064 * @param {Number} height
3065 * @param {Boolean} forExport
3066 */
3067 init: function(container, width, height, style, forExport, allowHTML) {
3068 var renderer = this,
3069 boxWrapper,
3070 element,
3071 desc;
3072
3073 boxWrapper = renderer.createElement('svg')
3074 .attr({
3075 'version': '1.1',
3076 'class': 'highcharts-root'
3077 })
3078
3079 .css(this.getStyle(style));
3080 element = boxWrapper.element;
3081 container.appendChild(element);
3082
3083 // For browsers other than IE, add the namespace attribute (#1978)
3084 if (container.innerHTML.indexOf('xmlns') === -1) {
3085 attr(element, 'xmlns', this.SVG_NS);
3086 }
3087
3088 // object properties
3089 renderer.isSVG = true;
3090 renderer.box = element;
3091 renderer.boxWrapper = boxWrapper;
3092 renderer.alignedObjects = [];
3093
3094 // Page url used for internal references. #24, #672, #1070
3095 renderer.url = (isFirefox || isWebKit) && doc.getElementsByTagName('base').length ?
3096 win.location.href
3097 .replace(/#.*?$/, '') // remove the hash
3098 .replace(/([\('\)])/g, '\\$1') // escape parantheses and quotes
3099 .replace(/ /g, '%20') : // replace spaces (needed for Safari only)
3100 '';
3101
3102 // Add description
3103 desc = this.createElement('desc').add();
3104 desc.element.appendChild(doc.createTextNode('Created with Highcharts 5.0.0'));
3105
3106
3107 renderer.defs = this.createElement('defs').add();
3108 renderer.allowHTML = allowHTML;
3109 renderer.forExport = forExport;
3110 renderer.gradients = {}; // Object where gradient SvgElements are stored
3111 renderer.cache = {}; // Cache for numerical bounding boxes
3112 renderer.cacheKeys = [];
3113 renderer.imgCount = 0;
3114
3115 renderer.setSize(width, height, false);
3116
3117
3118
3119 // Issue 110 workaround:
3120 // In Firefox, if a div is positioned by percentage, its pixel position may land
3121 // between pixels. The container itself doesn't display this, but an SVG element
3122 // inside this container will be drawn at subpixel precision. In order to draw
3123 // sharp lines, this must be compensated for. This doesn't seem to work inside
3124 // iframes though (like in jsFiddle).
3125 var subPixelFix, rect;
3126 if (isFirefox && container.getBoundingClientRect) {
3127 renderer.subPixelFix = subPixelFix = function() {
3128 css(container, {
3129 left: 0,
3130 top: 0
3131 });
3132 rect = container.getBoundingClientRect();
3133 css(container, {
3134 left: (Math.ceil(rect.left) - rect.left) + 'px',
3135 top: (Math.ceil(rect.top) - rect.top) + 'px'
3136 });
3137 };
3138
3139 // run the fix now
3140 subPixelFix();
3141
3142 // run it on resize
3143 addEvent(win, 'resize', subPixelFix);
3144 }
3145 },
3146
3147
3148
3149 getStyle: function(style) {
3150 this.style = extend({
3151
3152 fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif', // default font
3153 fontSize: '12px'
3154
3155 }, style);
3156 return this.style;
3157 },
3158 setStyle: function(style) {
3159 this.boxWrapper.css(this.getStyle(style));
3160 },
3161
3162
3163 /**
3164 * Detect whether the renderer is hidden. This happens when one of the parent elements
3165 * has display: none. #608.
3166 */
3167 isHidden: function() {
3168 return !this.boxWrapper.getBBox().width;
3169 },
3170
3171 /**
3172 * Destroys the renderer and its allocated members.
3173 */
3174 destroy: function() {
3175 var renderer = this,
3176 rendererDefs = renderer.defs;
3177 renderer.box = null;
3178 renderer.boxWrapper = renderer.boxWrapper.destroy();
3179
3180 // Call destroy on all gradient elements
3181 destroyObjectProperties(renderer.gradients || {});
3182 renderer.gradients = null;
3183
3184 // Defs are null in VMLRenderer
3185 // Otherwise, destroy them here.
3186 if (rendererDefs) {
3187 renderer.defs = rendererDefs.destroy();
3188 }
3189
3190 // Remove sub pixel fix handler
3191 // We need to check that there is a handler, otherwise all functions that are registered for event 'resize' are removed
3192 // See issue #982
3193 if (renderer.subPixelFix) {
3194 removeEvent(win, 'resize', renderer.subPixelFix);
3195 }
3196
3197 renderer.alignedObjects = null;
3198
3199 return null;
3200 },
3201
3202 /**
3203 * Create a wrapper for an SVG element
3204 * @param {Object} nodeName
3205 */
3206 createElement: function(nodeName) {
3207 var wrapper = new this.Element();
3208 wrapper.init(this, nodeName);
3209 return wrapper;
3210 },
3211
3212 /**
3213 * Dummy function for plugins
3214 */
3215 draw: noop,
3216
3217 /**
3218 * Get converted radial gradient attributes
3219 */
3220 getRadialAttr: function(radialReference, gradAttr) {
3221 return {
3222 cx: (radialReference[0] - radialReference[2] / 2) + gradAttr.cx * radialReference[2],
3223 cy: (radialReference[1] - radialReference[2] / 2) + gradAttr.cy * radialReference[2],
3224 r: gradAttr.r * radialReference[2]
3225 };
3226 },
3227
3228 /**
3229 * Parse a simple HTML string into SVG tspans
3230 *
3231 * @param {Object} textNode The parent text SVG node
3232 */
3233 buildText: function(wrapper) {
3234 var textNode = wrapper.element,
3235 renderer = this,
3236 forExport = renderer.forExport,
3237 textStr = pick(wrapper.textStr, '').toString(),
3238 hasMarkup = textStr.indexOf('<') !== -1,
3239 lines,
3240 childNodes = textNode.childNodes,
3241 clsRegex,
3242 styleRegex,
3243 hrefRegex,
3244 wasTooLong,
3245 parentX = attr(textNode, 'x'),
3246 textStyles = wrapper.styles,
3247 width = wrapper.textWidth,
3248 textLineHeight = textStyles && textStyles.lineHeight,
3249 textShadow = textStyles && textStyles.textShadow,
3250 ellipsis = textStyles && textStyles.textOverflow === 'ellipsis',
3251 i = childNodes.length,
3252 tempParent = width && !wrapper.added && this.box,
3253 getLineHeight = function(tspan) {
3254 var fontSizeStyle;
3255
3256 fontSizeStyle = /(px|em)$/.test(tspan && tspan.style.fontSize) ?
3257 tspan.style.fontSize :
3258 ((textStyles && textStyles.fontSize) || renderer.style.fontSize || 12);
3259
3260
3261 return textLineHeight ?
3262 pInt(textLineHeight) :
3263 renderer.fontMetrics(
3264 fontSizeStyle,
3265 tspan
3266 ).h;
3267 },
3268 unescapeAngleBrackets = function(inputStr) {
3269 return inputStr.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
3270 };
3271
3272 /// remove old text
3273 while (i--) {
3274 textNode.removeChild(childNodes[i]);
3275 }
3276
3277 // Skip tspans, add text directly to text node. The forceTSpan is a hook
3278 // used in text outline hack.
3279 if (!hasMarkup && !textShadow && !ellipsis && !width && textStr.indexOf(' ') === -1) {
3280 textNode.appendChild(doc.createTextNode(unescapeAngleBrackets(textStr)));
3281
3282 // Complex strings, add more logic
3283 } else {
3284
3285 clsRegex = /<.*class="([^"]+)".*>/;
3286 styleRegex = /<.*style="([^"]+)".*>/;
3287 hrefRegex = /<.*href="(http[^"]+)".*>/;
3288
3289 if (tempParent) {
3290 tempParent.appendChild(textNode); // attach it to the DOM to read offset width
3291 }
3292
3293 if (hasMarkup) {
3294 lines = textStr
3295 .replace(/<(b|strong)>/g, '<span style="font-weight:bold">')
3296 .replace(/<(i|em)>/g, '<span style="font-style:italic">')
3297 .replace(/<a/g, '<span')
3298 .replace(/<\/(b|strong|i|em|a)>/g, '</span>')
3299 .split(/<br.*?>/g);
3300
3301 } else {
3302 lines = [textStr];
3303 }
3304
3305
3306 // Trim empty lines (#5261)
3307 lines = grep(lines, function(line) {
3308 return line !== '';
3309 });
3310
3311
3312 // build the lines
3313 each(lines, function buildTextLines(line, lineNo) {
3314 var spans,
3315 spanNo = 0;
3316 line = line
3317 .replace(/^\s+|\s+$/g, '') // Trim to prevent useless/costly process on the spaces (#5258)
3318 .replace(/<span/g, '|||<span')
3319 .replace(/<\/span>/g, '</span>|||');
3320 spans = line.split('|||');
3321
3322 each(spans, function buildTextSpans(span) {
3323 if (span !== '' || spans.length === 1) {
3324 var attributes = {},
3325 tspan = doc.createElementNS(renderer.SVG_NS, 'tspan'),
3326 spanCls,
3327 spanStyle; // #390
3328 if (clsRegex.test(span)) {
3329 spanCls = span.match(clsRegex)[1];
3330 attr(tspan, 'class', spanCls);
3331 }
3332 if (styleRegex.test(span)) {
3333 spanStyle = span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2');
3334 attr(tspan, 'style', spanStyle);
3335 }
3336 if (hrefRegex.test(span) && !forExport) { // Not for export - #1529
3337 attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"');
3338 css(tspan, {
3339 cursor: 'pointer'
3340 });
3341 }
3342
3343 span = unescapeAngleBrackets(span.replace(/<(.|\n)*?>/g, '') || ' ');
3344
3345 // Nested tags aren't supported, and cause crash in Safari (#1596)
3346 if (span !== ' ') {
3347
3348 // add the text node
3349 tspan.appendChild(doc.createTextNode(span));
3350
3351 if (!spanNo) { // first span in a line, align it to the left
3352 if (lineNo && parentX !== null) {
3353 attributes.x = parentX;
3354 }
3355 } else {
3356 attributes.dx = 0; // #16
3357 }
3358
3359 // add attributes
3360 attr(tspan, attributes);
3361
3362 // Append it
3363 textNode.appendChild(tspan);
3364
3365 // first span on subsequent line, add the line height
3366 if (!spanNo && lineNo) {
3367
3368 // allow getting the right offset height in exporting in IE
3369 if (!svg && forExport) {
3370 css(tspan, {
3371 display: 'block'
3372 });
3373 }
3374
3375 // Set the line height based on the font size of either
3376 // the text element or the tspan element
3377 attr(
3378 tspan,
3379 'dy',
3380 getLineHeight(tspan)
3381 );
3382 }
3383
3384 /*if (width) {
3385 renderer.breakText(wrapper, width);
3386 }*/
3387
3388 // Check width and apply soft breaks or ellipsis
3389 if (width) {
3390 var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273
3391 noWrap = textStyles.whiteSpace === 'nowrap',
3392 hasWhiteSpace = spans.length > 1 || lineNo || (words.length > 1 && !noWrap),
3393 tooLong,
3394 actualWidth,
3395 rest = [],
3396 dy = getLineHeight(tspan),
3397 rotation = wrapper.rotation,
3398 wordStr = span, // for ellipsis
3399 cursor = wordStr.length, // binary search cursor
3400 bBox;
3401
3402 while ((hasWhiteSpace || ellipsis) && (words.length || rest.length)) {
3403 wrapper.rotation = 0; // discard rotation when computing box
3404 bBox = wrapper.getBBox(true);
3405 actualWidth = bBox.width;
3406
3407 // Old IE cannot measure the actualWidth for SVG elements (#2314)
3408 if (!svg && renderer.forExport) {
3409 actualWidth = renderer.measureSpanWidth(tspan.firstChild.data, wrapper.styles);
3410 }
3411
3412 tooLong = actualWidth > width;
3413
3414 // For ellipsis, do a binary search for the correct string length
3415 if (wasTooLong === undefined) {
3416 wasTooLong = tooLong; // First time
3417 }
3418 if (ellipsis && wasTooLong) {
3419 cursor /= 2;
3420
3421 if (wordStr === '' || (!tooLong && cursor < 0.5)) {
3422 words = []; // All ok, break out
3423 } else {
3424 wordStr = span.substring(0, wordStr.length + (tooLong ? -1 : 1) * Math.ceil(cursor));
3425 words = [wordStr + (width > 3 ? '\u2026' : '')];
3426 tspan.removeChild(tspan.firstChild);
3427 }
3428
3429 // Looping down, this is the first word sequence that is not too long,
3430 // so we can move on to build the next line.
3431 } else if (!tooLong || words.length === 1) {
3432 words = rest;
3433 rest = [];
3434
3435 if (words.length && !noWrap) {
3436 tspan = doc.createElementNS(SVG_NS, 'tspan');
3437 attr(tspan, {
3438 dy: dy,
3439 x: parentX
3440 });
3441 if (spanStyle) { // #390
3442 attr(tspan, 'style', spanStyle);
3443 }
3444 textNode.appendChild(tspan);
3445 }
3446 if (actualWidth > width) { // a single word is pressing it out
3447 width = actualWidth;
3448 }
3449 } else { // append to existing line tspan
3450 tspan.removeChild(tspan.firstChild);
3451 rest.unshift(words.pop());
3452 }
3453 if (words.length) {
3454 tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-')));
3455 }
3456 }
3457 wrapper.rotation = rotation;
3458 }
3459
3460 spanNo++;
3461 }
3462 }
3463 });
3464 });
3465
3466 if (wasTooLong) {
3467 wrapper.attr('title', wrapper.textStr);
3468 }
3469 if (tempParent) {
3470 tempParent.removeChild(textNode); // attach it to the DOM to read offset width
3471 }
3472
3473 // Apply the text shadow
3474 if (textShadow && wrapper.applyTextShadow) {
3475 wrapper.applyTextShadow(textShadow);
3476 }
3477 }
3478 },
3479
3480
3481
3482 /*
3483 breakText: function (wrapper, width) {
3484 var bBox = wrapper.getBBox(),
3485 node = wrapper.element,
3486 textLength = node.textContent.length,
3487 pos = Math.round(width * textLength / bBox.width), // try this position first, based on average character width
3488 increment = 0,
3489 finalPos;
3490
3491 if (bBox.width > width) {
3492 while (finalPos === undefined) {
3493 textLength = node.getSubStringLength(0, pos);
3494
3495 if (textLength <= width) {
3496 if (increment === -1) {
3497 finalPos = pos;
3498 } else {
3499 increment = 1;
3500 }
3501 } else {
3502 if (increment === 1) {
3503 finalPos = pos - 1;
3504 } else {
3505 increment = -1;
3506 }
3507 }
3508 pos += increment;
3509 }
3510 }
3511 console.log('width', width, 'stringWidth', node.getSubStringLength(0, finalPos))
3512 },
3513 */
3514
3515 /**
3516 * Returns white for dark colors and black for bright colors
3517 */
3518 getContrast: function(rgba) {
3519 rgba = color(rgba).rgba;
3520 return rgba[0] + rgba[1] + rgba[2] > 2 * 255 ? '#000000' : '#FFFFFF';
3521 },
3522
3523 /**
3524 * Create a button with preset states
3525 * @param {String} text
3526 * @param {Number} x
3527 * @param {Number} y
3528 * @param {Function} callback
3529 * @param {Object} normalState
3530 * @param {Object} hoverState
3531 * @param {Object} pressedState
3532 */
3533 button: function(text, x, y, callback, normalState, hoverState, pressedState, disabledState, shape) {
3534 var label = this.label(text, x, y, shape, null, null, null, null, 'button'),
3535 curState = 0;
3536
3537 // Default, non-stylable attributes
3538 label.attr(merge({
3539 'padding': 8,
3540 'r': 2
3541 }, normalState));
3542
3543
3544 // Presentational
3545 var normalStyle,
3546 hoverStyle,
3547 pressedStyle,
3548 disabledStyle;
3549
3550 // Normal state - prepare the attributes
3551 normalState = merge({
3552 fill: '#f7f7f7',
3553 stroke: '#cccccc',
3554 'stroke-width': 1,
3555 style: {
3556 color: '#333333',
3557 cursor: 'pointer',
3558 fontWeight: 'normal'
3559 }
3560 }, normalState);
3561 normalStyle = normalState.style;
3562 delete normalState.style;
3563
3564 // Hover state
3565 hoverState = merge(normalState, {
3566 fill: '#e6e6e6'
3567 }, hoverState);
3568 hoverStyle = hoverState.style;
3569 delete hoverState.style;
3570
3571 // Pressed state
3572 pressedState = merge(normalState, {
3573 fill: '#e6ebf5',
3574 style: {
3575 color: '#000000',
3576 fontWeight: 'bold'
3577 }
3578 }, pressedState);
3579 pressedStyle = pressedState.style;
3580 delete pressedState.style;
3581
3582 // Disabled state
3583 disabledState = merge(normalState, {
3584 style: {
3585 color: '#cccccc'
3586 }
3587 }, disabledState);
3588 disabledStyle = disabledState.style;
3589 delete disabledState.style;
3590
3591
3592 // Add the events. IE9 and IE10 need mouseover and mouseout to funciton (#667).
3593 addEvent(label.element, isMS ? 'mouseover' : 'mouseenter', function() {
3594 if (curState !== 3) {
3595 label.setState(1);
3596 }
3597 });
3598 addEvent(label.element, isMS ? 'mouseout' : 'mouseleave', function() {
3599 if (curState !== 3) {
3600 label.setState(curState);
3601 }
3602 });
3603
3604 label.setState = function(state) {
3605 // Hover state is temporary, don't record it
3606 if (state !== 1) {
3607 label.state = curState = state;
3608 }
3609 // Update visuals
3610 label.removeClass(/highcharts-button-(normal|hover|pressed|disabled)/)
3611 .addClass('highcharts-button-' + ['normal', 'hover', 'pressed', 'disabled'][state || 0]);
3612
3613
3614 label.attr([normalState, hoverState, pressedState, disabledState][state || 0])
3615 .css([normalStyle, hoverStyle, pressedStyle, disabledStyle][state || 0]);
3616
3617 };
3618
3619
3620
3621 // Presentational attributes
3622 label
3623 .attr(normalState)
3624 .css(extend({
3625 cursor: 'default'
3626 }, normalStyle));
3627
3628
3629 return label
3630 .on('click', function(e) {
3631 if (curState !== 3) {
3632 callback.call(label, e);
3633 }
3634 });
3635 },
3636
3637 /**
3638 * Make a straight line crisper by not spilling out to neighbour pixels
3639 * @param {Array} points
3640 * @param {Number} width
3641 */
3642 crispLine: function(points, width) {
3643 // points format: ['M', 0, 0, 'L', 100, 0]
3644 // normalize to a crisp line
3645 if (points[1] === points[4]) {
3646 // Substract due to #1129. Now bottom and left axis gridlines behave the same.
3647 points[1] = points[4] = Math.round(points[1]) - (width % 2 / 2);
3648 }
3649 if (points[2] === points[5]) {
3650 points[2] = points[5] = Math.round(points[2]) + (width % 2 / 2);
3651 }
3652 return points;
3653 },
3654
3655
3656 /**
3657 * Draw a path
3658 * @param {Array} path An SVG path in array form
3659 */
3660 path: function(path) {
3661 var attribs = {
3662
3663 fill: 'none'
3664
3665 };
3666 if (isArray(path)) {
3667 attribs.d = path;
3668 } else if (isObject(path)) { // attributes
3669 extend(attribs, path);
3670 }
3671 return this.createElement('path').attr(attribs);
3672 },
3673
3674 /**
3675 * Draw and return an SVG circle
3676 * @param {Number} x The x position
3677 * @param {Number} y The y position
3678 * @param {Number} r The radius
3679 */
3680 circle: function(x, y, r) {
3681 var attribs = isObject(x) ? x : {
3682 x: x,
3683 y: y,
3684 r: r
3685 },
3686 wrapper = this.createElement('circle');
3687
3688 // Setting x or y translates to cx and cy
3689 wrapper.xSetter = wrapper.ySetter = function(value, key, element) {
3690 element.setAttribute('c' + key, value);
3691 };
3692
3693 return wrapper.attr(attribs);
3694 },
3695
3696 /**
3697 * Draw and return an arc
3698 * @param {Number} x X position
3699 * @param {Number} y Y position
3700 * @param {Number} r Radius
3701 * @param {Number} innerR Inner radius like used in donut charts
3702 * @param {Number} start Starting angle
3703 * @param {Number} end Ending angle
3704 */
3705 arc: function(x, y, r, innerR, start, end) {
3706 var arc;
3707
3708 if (isObject(x)) {
3709 y = x.y;
3710 r = x.r;
3711 innerR = x.innerR;
3712 start = x.start;
3713 end = x.end;
3714 x = x.x;
3715 }
3716
3717 // Arcs are defined as symbols for the ability to set
3718 // attributes in attr and animate
3719 arc = this.symbol('arc', x || 0, y || 0, r || 0, r || 0, {
3720 innerR: innerR || 0,
3721 start: start || 0,
3722 end: end || 0
3723 });
3724 arc.r = r; // #959
3725 return arc;
3726 },
3727
3728 /**
3729 * Draw and return a rectangle
3730 * @param {Number} x Left position
3731 * @param {Number} y Top position
3732 * @param {Number} width
3733 * @param {Number} height
3734 * @param {Number} r Border corner radius
3735 * @param {Number} strokeWidth A stroke width can be supplied to allow crisp drawing
3736 */
3737 rect: function(x, y, width, height, r, strokeWidth) {
3738
3739 r = isObject(x) ? x.r : r;
3740
3741 var wrapper = this.createElement('rect'),
3742 attribs = isObject(x) ? x : x === undefined ? {} : {
3743 x: x,
3744 y: y,
3745 width: Math.max(width, 0),
3746 height: Math.max(height, 0)
3747 };
3748
3749
3750 if (strokeWidth !== undefined) {
3751 attribs.strokeWidth = strokeWidth;
3752 attribs = wrapper.crisp(attribs);
3753 }
3754 attribs.fill = 'none';
3755
3756
3757 if (r) {
3758 attribs.r = r;
3759 }
3760
3761 wrapper.rSetter = function(value, key, element) {
3762 attr(element, {
3763 rx: value,
3764 ry: value
3765 });
3766 };
3767
3768 return wrapper.attr(attribs);
3769 },
3770
3771 /**
3772 * Resize the box and re-align all aligned elements
3773 * @param {Object} width
3774 * @param {Object} height
3775 * @param {Boolean} animate
3776 *
3777 */
3778 setSize: function(width, height, animate) {
3779 var renderer = this,
3780 alignedObjects = renderer.alignedObjects,
3781 i = alignedObjects.length;
3782
3783 renderer.width = width;
3784 renderer.height = height;
3785
3786 renderer.boxWrapper.animate({
3787 width: width,
3788 height: height
3789 }, {
3790 step: function() {
3791 this.attr({
3792 viewBox: '0 0 ' + this.attr('width') + ' ' + this.attr('height')
3793 });
3794 },
3795 duration: pick(animate, true) ? undefined : 0
3796 });
3797
3798 while (i--) {
3799 alignedObjects[i].align();
3800 }
3801 },
3802
3803 /**
3804 * Create a group
3805 * @param {String} name The group will be given a class name of 'highcharts-{name}'.
3806 * This can be used for styling and scripting.
3807 */
3808 g: function(name) {
3809 var elem = this.createElement('g');
3810 return name ? elem.attr({
3811 'class': 'highcharts-' + name
3812 }) : elem;
3813 },
3814
3815 /**
3816 * Display an image
3817 * @param {String} src
3818 * @param {Number} x
3819 * @param {Number} y
3820 * @param {Number} width
3821 * @param {Number} height
3822 */
3823 image: function(src, x, y, width, height) {
3824 var attribs = {
3825 preserveAspectRatio: 'none'
3826 },
3827 elemWrapper;
3828
3829 // optional properties
3830 if (arguments.length > 1) {
3831 extend(attribs, {
3832 x: x,
3833 y: y,
3834 width: width,
3835 height: height
3836 });
3837 }
3838
3839 elemWrapper = this.createElement('image').attr(attribs);
3840
3841 // set the href in the xlink namespace
3842 if (elemWrapper.element.setAttributeNS) {
3843 elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink',
3844 'href', src);
3845 } else {
3846 // could be exporting in IE
3847 // using href throws "not supported" in ie7 and under, requries regex shim to fix later
3848 elemWrapper.element.setAttribute('hc-svg-href', src);
3849 }
3850 return elemWrapper;
3851 },
3852
3853 /**
3854 * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object.
3855 *
3856 * @param {Object} symbol
3857 * @param {Object} x
3858 * @param {Object} y
3859 * @param {Object} radius
3860 * @param {Object} options
3861 */
3862 symbol: function(symbol, x, y, width, height, options) {
3863
3864 var ren = this,
3865 obj,
3866
3867 // get the symbol definition function
3868 symbolFn = this.symbols[symbol],
3869
3870 // check if there's a path defined for this symbol
3871 path = defined(x) && symbolFn && symbolFn(
3872 Math.round(x),
3873 Math.round(y),
3874 width,
3875 height,
3876 options
3877 ),
3878 imageRegex = /^url\((.*?)\)$/,
3879 imageSrc,
3880 centerImage,
3881 symbolSizes = {};
3882
3883 if (symbolFn) {
3884 obj = this.path(path);
3885
3886
3887 obj.attr('fill', 'none');
3888
3889
3890 // expando properties for use in animate and attr
3891 extend(obj, {
3892 symbolName: symbol,
3893 x: x,
3894 y: y,
3895 width: width,
3896 height: height
3897 });
3898 if (options) {
3899 extend(obj, options);
3900 }
3901
3902
3903 // image symbols
3904 } else if (imageRegex.test(symbol)) {
3905
3906
3907 imageSrc = symbol.match(imageRegex)[1];
3908
3909 // Create the image synchronously, add attribs async
3910 obj = this.image(imageSrc);
3911
3912 // The image width is not always the same as the symbol width. The image may be centered within the symbol,
3913 // as is the case when image shapes are used as label backgrounds, for example in flags.
3914 obj.imgwidth = pick(symbolSizes[imageSrc] && symbolSizes[imageSrc].width, options && options.width);
3915 obj.imgheight = pick(symbolSizes[imageSrc] && symbolSizes[imageSrc].height, options && options.height);
3916 /**
3917 * Set the size and position
3918 */
3919 centerImage = function() {
3920 obj.attr({
3921 width: obj.width,
3922 height: obj.height
3923 });
3924 };
3925
3926 /**
3927 * Width and height setters that take both the image's physical size and the label size into
3928 * consideration, and translates the image to center within the label.
3929 */
3930 each(['width', 'height'], function(key) {
3931 obj[key + 'Setter'] = function(value, key) {
3932 var attribs = {},
3933 imgSize = this['img' + key];
3934 this[key] = value;
3935 if (defined(imgSize)) {
3936 if (this.element) {
3937 this.element.setAttribute(key, imgSize);
3938 }
3939 if (!this.alignByTranslate) {
3940 attribs[key === 'width' ? 'translateX' : 'translateY'] = (this[key] - imgSize) / 2;
3941 this.attr(attribs);
3942 }
3943 }
3944 };
3945 });
3946
3947
3948 if (defined(x)) {
3949 obj.attr({
3950 x: x,
3951 y: y
3952 });
3953 }
3954 obj.isImg = true;
3955
3956 if (defined(obj.imgwidth) && defined(obj.imgheight)) {
3957 centerImage();
3958 } else {
3959 // Initialize image to be 0 size so export will still function if there's no cached sizes.
3960 obj.attr({
3961 width: 0,
3962 height: 0
3963 });
3964
3965 // Create a dummy JavaScript image to get the width and height. Due to a bug in IE < 8,
3966 // the created element must be assigned to a variable in order to load (#292).
3967 createElement('img', {
3968 onload: function() {
3969
3970 var chart = charts[ren.chartIndex];
3971
3972 // Special case for SVGs on IE11, the width is not accessible until the image is
3973 // part of the DOM (#2854).
3974 if (this.width === 0) {
3975 css(this, {
3976 position: 'absolute',
3977 top: '-999em'
3978 });
3979 doc.body.appendChild(this);
3980 }
3981
3982 // Center the image
3983 symbolSizes[imageSrc] = { // Cache for next
3984 width: this.width,
3985 height: this.height
3986 };
3987 obj.imgwidth = this.width;
3988 obj.imgheight = this.height;
3989
3990 if (obj.element) {
3991 centerImage();
3992 }
3993
3994 // Clean up after #2854 workaround.
3995 if (this.parentNode) {
3996 this.parentNode.removeChild(this);
3997 }
3998
3999 // Fire the load event when all external images are loaded
4000 ren.imgCount--;
4001 if (!ren.imgCount && chart && chart.onload) {
4002 chart.onload();
4003 }
4004 },
4005 src: imageSrc
4006 });
4007 this.imgCount++;
4008 }
4009 }
4010
4011 return obj;
4012 },
4013
4014 /**
4015 * An extendable collection of functions for defining symbol paths.
4016 */
4017 symbols: {
4018 'circle': function(x, y, w, h) {
4019 var cpw = 0.166 * w;
4020 return [
4021 'M', x + w / 2, y,
4022 'C', x + w + cpw, y, x + w + cpw, y + h, x + w / 2, y + h,
4023 'C', x - cpw, y + h, x - cpw, y, x + w / 2, y,
4024 'Z'
4025 ];
4026 },
4027
4028 'square': function(x, y, w, h) {
4029 return [
4030 'M', x, y,
4031 'L', x + w, y,
4032 x + w, y + h,
4033 x, y + h,
4034 'Z'
4035 ];
4036 },
4037
4038 'triangle': function(x, y, w, h) {
4039 return [
4040 'M', x + w / 2, y,
4041 'L', x + w, y + h,
4042 x, y + h,
4043 'Z'
4044 ];
4045 },
4046
4047 'triangle-down': function(x, y, w, h) {
4048 return [
4049 'M', x, y,
4050 'L', x + w, y,
4051 x + w / 2, y + h,
4052 'Z'
4053 ];
4054 },
4055 'diamond': function(x, y, w, h) {
4056 return [
4057 'M', x + w / 2, y,
4058 'L', x + w, y + h / 2,
4059 x + w / 2, y + h,
4060 x, y + h / 2,
4061 'Z'
4062 ];
4063 },
4064 'arc': function(x, y, w, h, options) {
4065 var start = options.start,
4066 radius = options.r || w || h,
4067 end = options.end - 0.001, // to prevent cos and sin of start and end from becoming equal on 360 arcs (related: #1561)
4068 innerRadius = options.innerR,
4069 open = options.open,
4070 cosStart = Math.cos(start),
4071 sinStart = Math.sin(start),
4072 cosEnd = Math.cos(end),
4073 sinEnd = Math.sin(end),
4074 longArc = options.end - start < Math.PI ? 0 : 1;
4075
4076 return [
4077 'M',
4078 x + radius * cosStart,
4079 y + radius * sinStart,
4080 'A', // arcTo
4081 radius, // x radius
4082 radius, // y radius
4083 0, // slanting
4084 longArc, // long or short arc
4085 1, // clockwise
4086 x + radius * cosEnd,
4087 y + radius * sinEnd,
4088 open ? 'M' : 'L',
4089 x + innerRadius * cosEnd,
4090 y + innerRadius * sinEnd,
4091 'A', // arcTo
4092 innerRadius, // x radius
4093 innerRadius, // y radius
4094 0, // slanting
4095 longArc, // long or short arc
4096 0, // clockwise
4097 x + innerRadius * cosStart,
4098 y + innerRadius * sinStart,
4099
4100 open ? '' : 'Z' // close
4101 ];
4102 },
4103
4104 /**
4105 * Callout shape used for default tooltips, also used for rounded rectangles in VML
4106 */
4107 callout: function(x, y, w, h, options) {
4108 var arrowLength = 6,
4109 halfDistance = 6,
4110 r = Math.min((options && options.r) || 0, w, h),
4111 safeDistance = r + halfDistance,
4112 anchorX = options && options.anchorX,
4113 anchorY = options && options.anchorY,
4114 path;
4115
4116 path = [
4117 'M', x + r, y,
4118 'L', x + w - r, y, // top side
4119 'C', x + w, y, x + w, y, x + w, y + r, // top-right corner
4120 'L', x + w, y + h - r, // right side
4121 'C', x + w, y + h, x + w, y + h, x + w - r, y + h, // bottom-right corner
4122 'L', x + r, y + h, // bottom side
4123 'C', x, y + h, x, y + h, x, y + h - r, // bottom-left corner
4124 'L', x, y + r, // left side
4125 'C', x, y, x, y, x + r, y // top-right corner
4126 ];
4127
4128 if (anchorX && anchorX > w && anchorY > y + safeDistance && anchorY < y + h - safeDistance) { // replace right side
4129 path.splice(13, 3,
4130 'L', x + w, anchorY - halfDistance,
4131 x + w + arrowLength, anchorY,
4132 x + w, anchorY + halfDistance,
4133 x + w, y + h - r
4134 );
4135 } else if (anchorX && anchorX < 0 && anchorY > y + safeDistance && anchorY < y + h - safeDistance) { // replace left side
4136 path.splice(33, 3,
4137 'L', x, anchorY + halfDistance,
4138 x - arrowLength, anchorY,
4139 x, anchorY - halfDistance,
4140 x, y + r
4141 );
4142 } else if (anchorY && anchorY > h && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace bottom
4143 path.splice(23, 3,
4144 'L', anchorX + halfDistance, y + h,
4145 anchorX, y + h + arrowLength,
4146 anchorX - halfDistance, y + h,
4147 x + r, y + h
4148 );
4149 } else if (anchorY && anchorY < 0 && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace top
4150 path.splice(3, 3,
4151 'L', anchorX - halfDistance, y,
4152 anchorX, y - arrowLength,
4153 anchorX + halfDistance, y,
4154 w - r, y
4155 );
4156 }
4157 return path;
4158 }
4159 },
4160
4161 /**
4162 * Define a clipping rectangle
4163 * @param {String} id
4164 * @param {Number} x
4165 * @param {Number} y
4166 * @param {Number} width
4167 * @param {Number} height
4168 */
4169 clipRect: function(x, y, width, height) {
4170 var wrapper,
4171 id = 'highcharts-' + H.idCounter++,
4172
4173 clipPath = this.createElement('clipPath').attr({
4174 id: id
4175 }).add(this.defs);
4176
4177 wrapper = this.rect(x, y, width, height, 0).add(clipPath);
4178 wrapper.id = id;
4179 wrapper.clipPath = clipPath;
4180 wrapper.count = 0;
4181
4182 return wrapper;
4183 },
4184
4185
4186
4187
4188
4189 /**
4190 * Add text to the SVG object
4191 * @param {String} str
4192 * @param {Number} x Left position
4193 * @param {Number} y Top position
4194 * @param {Boolean} useHTML Use HTML to render the text
4195 */
4196 text: function(str, x, y, useHTML) {
4197
4198 // declare variables
4199 var renderer = this,
4200 fakeSVG = !svg && renderer.forExport,
4201 wrapper,
4202 attribs = {};
4203
4204 if (useHTML && (renderer.allowHTML || !renderer.forExport)) {
4205 return renderer.html(str, x, y);
4206 }
4207
4208 attribs.x = Math.round(x || 0); // X is always needed for line-wrap logic
4209 if (y) {
4210 attribs.y = Math.round(y);
4211 }
4212 if (str || str === 0) {
4213 attribs.text = str;
4214 }
4215
4216 wrapper = renderer.createElement('text')
4217 .attr(attribs);
4218
4219 // Prevent wrapping from creating false offsetWidths in export in legacy IE (#1079, #1063)
4220 if (fakeSVG) {
4221 wrapper.css({
4222 position: 'absolute'
4223 });
4224 }
4225
4226 if (!useHTML) {
4227 wrapper.xSetter = function(value, key, element) {
4228 var tspans = element.getElementsByTagName('tspan'),
4229 tspan,
4230 parentVal = element.getAttribute(key),
4231 i;
4232 for (i = 0; i < tspans.length; i++) {
4233 tspan = tspans[i];
4234 // If the x values are equal, the tspan represents a linebreak
4235 if (tspan.getAttribute(key) === parentVal) {
4236 tspan.setAttribute(key, value);
4237 }
4238 }
4239 element.setAttribute(key, value);
4240 };
4241 }
4242
4243 return wrapper;
4244 },
4245
4246 /**
4247 * Utility to return the baseline offset and total line height from the font size
4248 */
4249 fontMetrics: function(fontSize, elem) { // eslint-disable-line no-unused-vars
4250 var lineHeight,
4251 baseline;
4252
4253
4254 fontSize = fontSize || (this.style && this.style.fontSize);
4255
4256
4257 fontSize = /px/.test(fontSize) ? pInt(fontSize) : /em/.test(fontSize) ? parseFloat(fontSize) * 12 : 12;
4258
4259 // Empirical values found by comparing font size and bounding box height.
4260 // Applies to the default font family. http://jsfiddle.net/highcharts/7xvn7/
4261 lineHeight = fontSize < 24 ? fontSize + 3 : Math.round(fontSize * 1.2);
4262 baseline = Math.round(lineHeight * 0.8);
4263
4264 return {
4265 h: lineHeight,
4266 b: baseline,
4267 f: fontSize
4268 };
4269 },
4270
4271 /**
4272 * Correct X and Y positioning of a label for rotation (#1764)
4273 */
4274 rotCorr: function(baseline, rotation, alterY) {
4275 var y = baseline;
4276 if (rotation && alterY) {
4277 y = Math.max(y * Math.cos(rotation * deg2rad), 4);
4278 }
4279 return {
4280 x: (-baseline / 3) * Math.sin(rotation * deg2rad),
4281 y: y
4282 };
4283 },
4284
4285 /**
4286 * Add a label, a text item that can hold a colored or gradient background
4287 * as well as a border and shadow.
4288 * @param {string} str
4289 * @param {Number} x
4290 * @param {Number} y
4291 * @param {String} shape
4292 * @param {Number} anchorX In case the shape has a pointer, like a flag, this is the
4293 * coordinates it should be pinned to
4294 * @param {Number} anchorY
4295 * @param {Boolean} baseline Whether to position the label relative to the text baseline,
4296 * like renderer.text, or to the upper border of the rectangle.
4297 * @param {String} className Class name for the group
4298 */
4299 label: function(str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) {
4300
4301 var renderer = this,
4302 wrapper = renderer.g(className !== 'button' && 'label'),
4303 text = wrapper.text = renderer.text('', 0, 0, useHTML)
4304 .attr({
4305 zIndex: 1
4306 }),
4307 box,
4308 bBox,
4309 alignFactor = 0,
4310 padding = 3,
4311 paddingLeft = 0,
4312 width,
4313 height,
4314 wrapperX,
4315 wrapperY,
4316 textAlign,
4317 deferredAttr = {},
4318 strokeWidth,
4319 baselineOffset,
4320 hasBGImage = /^url\((.*?)\)$/.test(shape),
4321 needsBox = hasBGImage,
4322 getCrispAdjust,
4323 updateBoxSize,
4324 updateTextPadding,
4325 boxAttr;
4326
4327 if (className) {
4328 wrapper.addClass('highcharts-' + className);
4329 }
4330
4331
4332 needsBox = hasBGImage;
4333 getCrispAdjust = function() {
4334 return (strokeWidth || 0) % 2 / 2;
4335 };
4336
4337
4338
4339 /**
4340 * This function runs after the label is added to the DOM (when the bounding box is
4341 * available), and after the text of the label is updated to detect the new bounding
4342 * box and reflect it in the border box.
4343 */
4344 updateBoxSize = function() {
4345 var style = text.element.style,
4346 crispAdjust,
4347 attribs = {};
4348
4349 bBox = (width === undefined || height === undefined || textAlign) && defined(text.textStr) &&
4350 text.getBBox(); //#3295 && 3514 box failure when string equals 0
4351 wrapper.width = (width || bBox.width || 0) + 2 * padding + paddingLeft;
4352 wrapper.height = (height || bBox.height || 0) + 2 * padding;
4353
4354 // Update the label-scoped y offset
4355 baselineOffset = padding + renderer.fontMetrics(style && style.fontSize, text).b;
4356
4357
4358 if (needsBox) {
4359
4360 // Create the border box if it is not already present
4361 if (!box) {
4362 wrapper.box = box = renderer.symbols[shape] || hasBGImage ? // Symbol definition exists (#5324)
4363 renderer.symbol(shape) :
4364 renderer.rect();
4365
4366 box.addClass(
4367 (className === 'button' ? '' : 'highcharts-label-box') + // Don't use label className for buttons
4368 (className ? ' highcharts-' + className + '-box' : '')
4369 );
4370
4371 box.add(wrapper);
4372
4373 crispAdjust = getCrispAdjust();
4374 attribs.x = crispAdjust;
4375 attribs.y = (baseline ? -baselineOffset : 0) + crispAdjust;
4376 }
4377
4378 // Apply the box attributes
4379 attribs.width = Math.round(wrapper.width);
4380 attribs.height = Math.round(wrapper.height);
4381
4382 box.attr(extend(attribs, deferredAttr));
4383 deferredAttr = {};
4384 }
4385 };
4386
4387 /**
4388 * This function runs after setting text or padding, but only if padding is changed
4389 */
4390 updateTextPadding = function() {
4391 var textX = paddingLeft + padding,
4392 textY;
4393
4394 // determin y based on the baseline
4395 textY = baseline ? 0 : baselineOffset;
4396
4397 // compensate for alignment
4398 if (defined(width) && bBox && (textAlign === 'center' || textAlign === 'right')) {
4399 textX += {
4400 center: 0.5,
4401 right: 1
4402 }[textAlign] * (width - bBox.width);
4403 }
4404
4405 // update if anything changed
4406 if (textX !== text.x || textY !== text.y) {
4407 text.attr('x', textX);
4408 if (textY !== undefined) {
4409 text.attr('y', textY);
4410 }
4411 }
4412
4413 // record current values
4414 text.x = textX;
4415 text.y = textY;
4416 };
4417
4418 /**
4419 * Set a box attribute, or defer it if the box is not yet created
4420 * @param {Object} key
4421 * @param {Object} value
4422 */
4423 boxAttr = function(key, value) {
4424 if (box) {
4425 box.attr(key, value);
4426 } else {
4427 deferredAttr[key] = value;
4428 }
4429 };
4430
4431 /**
4432 * After the text element is added, get the desired size of the border box
4433 * and add it before the text in the DOM.
4434 */
4435 wrapper.onAdd = function() {
4436 text.add(wrapper);
4437 wrapper.attr({
4438 text: (str || str === 0) ? str : '', // alignment is available now // #3295: 0 not rendered if given as a value
4439 x: x,
4440 y: y
4441 });
4442
4443 if (box && defined(anchorX)) {
4444 wrapper.attr({
4445 anchorX: anchorX,
4446 anchorY: anchorY
4447 });
4448 }
4449 };
4450
4451 /*
4452 * Add specific attribute setters.
4453 */
4454
4455 // only change local variables
4456 wrapper.widthSetter = function(value) {
4457 width = value;
4458 };
4459 wrapper.heightSetter = function(value) {
4460 height = value;
4461 };
4462 wrapper['text-alignSetter'] = function(value) {
4463 textAlign = value;
4464 };
4465 wrapper.paddingSetter = function(value) {
4466 if (defined(value) && value !== padding) {
4467 padding = wrapper.padding = value;
4468 updateTextPadding();
4469 }
4470 };
4471 wrapper.paddingLeftSetter = function(value) {
4472 if (defined(value) && value !== paddingLeft) {
4473 paddingLeft = value;
4474 updateTextPadding();
4475 }
4476 };
4477
4478
4479 // change local variable and prevent setting attribute on the group
4480 wrapper.alignSetter = function(value) {
4481 value = {
4482 left: 0,
4483 center: 0.5,
4484 right: 1
4485 }[value];
4486 if (value !== alignFactor) {
4487 alignFactor = value;
4488 if (bBox) { // Bounding box exists, means we're dynamically changing
4489 wrapper.attr({
4490 x: wrapperX
4491 }); // #5134
4492 }
4493 }
4494 };
4495
4496 // apply these to the box and the text alike
4497 wrapper.textSetter = function(value) {
4498 if (value !== undefined) {
4499 text.textSetter(value);
4500 }
4501 updateBoxSize();
4502 updateTextPadding();
4503 };
4504
4505 // apply these to the box but not to the text
4506 wrapper['stroke-widthSetter'] = function(value, key) {
4507 if (value) {
4508 needsBox = true;
4509 }
4510 strokeWidth = this['stroke-width'] = value;
4511 boxAttr(key, value);
4512 };
4513
4514 wrapper.strokeSetter = wrapper.fillSetter = wrapper.rSetter = function(value, key) {
4515 if (key === 'fill' && value) {
4516 needsBox = true;
4517 }
4518 boxAttr(key, value);
4519 };
4520
4521 wrapper.anchorXSetter = function(value, key) {
4522 anchorX = value;
4523 boxAttr(key, Math.round(value) - getCrispAdjust() - wrapperX);
4524 };
4525 wrapper.anchorYSetter = function(value, key) {
4526 anchorY = value;
4527 boxAttr(key, value - wrapperY);
4528 };
4529
4530 // rename attributes
4531 wrapper.xSetter = function(value) {
4532 wrapper.x = value; // for animation getter
4533 if (alignFactor) {
4534 value -= alignFactor * ((width || bBox.width) + 2 * padding);
4535 }
4536 wrapperX = Math.round(value);
4537 wrapper.attr('translateX', wrapperX);
4538 };
4539 wrapper.ySetter = function(value) {
4540 wrapperY = wrapper.y = Math.round(value);
4541 wrapper.attr('translateY', wrapperY);
4542 };
4543
4544 // Redirect certain methods to either the box or the text
4545 var baseCss = wrapper.css;
4546 return extend(wrapper, {
4547 /**
4548 * Pick up some properties and apply them to the text instead of the wrapper
4549 */
4550 css: function(styles) {
4551 if (styles) {
4552 var textStyles = {};
4553 styles = merge(styles); // create a copy to avoid altering the original object (#537)
4554 each(wrapper.textProps, function(prop) {
4555 if (styles[prop] !== undefined) {
4556 textStyles[prop] = styles[prop];
4557 delete styles[prop];
4558 }
4559 });
4560 text.css(textStyles);
4561 }
4562 return baseCss.call(wrapper, styles);
4563 },
4564 /**
4565 * Return the bounding box of the box, not the group
4566 */
4567 getBBox: function() {
4568 return {
4569 width: bBox.width + 2 * padding,
4570 height: bBox.height + 2 * padding,
4571 x: bBox.x - padding,
4572 y: bBox.y - padding
4573 };
4574 },
4575
4576 /**
4577 * Apply the shadow to the box
4578 */
4579 shadow: function(b) {
4580 if (b) {
4581 updateBoxSize();
4582 if (box) {
4583 box.shadow(b);
4584 }
4585 }
4586 return wrapper;
4587 },
4588
4589 /**
4590 * Destroy and release memory.
4591 */
4592 destroy: function() {
4593
4594 // Added by button implementation
4595 removeEvent(wrapper.element, 'mouseenter');
4596 removeEvent(wrapper.element, 'mouseleave');
4597
4598 if (text) {
4599 text = text.destroy();
4600 }
4601 if (box) {
4602 box = box.destroy();
4603 }
4604 // Call base implementation to destroy the rest
4605 SVGElement.prototype.destroy.call(wrapper);
4606
4607 // Release local pointers (#1298)
4608 wrapper = renderer = updateBoxSize = updateTextPadding = boxAttr = null;
4609 }
4610 });
4611 }
4612 }; // end SVGRenderer
4613
4614
4615 // general renderer
4616 H.Renderer = SVGRenderer;
4617
4618 }(Highcharts));
4619 (function(H) {
4620 /**
4621 * (c) 2010-2016 Torstein Honsi
4622 *
4623 * License: www.highcharts.com/license
4624 */
4625 'use strict';
4626 var attr = H.attr,
4627 createElement = H.createElement,
4628 css = H.css,
4629 defined = H.defined,
4630 each = H.each,
4631 extend = H.extend,
4632 isFirefox = H.isFirefox,
4633 isMS = H.isMS,
4634 isWebKit = H.isWebKit,
4635 pInt = H.pInt,
4636 SVGElement = H.SVGElement,
4637 SVGRenderer = H.SVGRenderer,
4638 win = H.win,
4639 wrap = H.wrap;
4640
4641 // extend SvgElement for useHTML option
4642 extend(SVGElement.prototype, {
4643 /**
4644 * Apply CSS to HTML elements. This is used in text within SVG rendering and
4645 * by the VML renderer
4646 */
4647 htmlCss: function(styles) {
4648 var wrapper = this,
4649 element = wrapper.element,
4650 textWidth = styles && element.tagName === 'SPAN' && styles.width;
4651
4652 if (textWidth) {
4653 delete styles.width;
4654 wrapper.textWidth = textWidth;
4655 wrapper.updateTransform();
4656 }
4657 if (styles && styles.textOverflow === 'ellipsis') {
4658 styles.whiteSpace = 'nowrap';
4659 styles.overflow = 'hidden';
4660 }
4661 wrapper.styles = extend(wrapper.styles, styles);
4662 css(wrapper.element, styles);
4663
4664 return wrapper;
4665 },
4666
4667 /**
4668 * VML and useHTML method for calculating the bounding box based on offsets
4669 * @param {Boolean} refresh Whether to force a fresh value from the DOM or to
4670 * use the cached value
4671 *
4672 * @return {Object} A hash containing values for x, y, width and height
4673 */
4674
4675 htmlGetBBox: function() {
4676 var wrapper = this,
4677 element = wrapper.element;
4678
4679 // faking getBBox in exported SVG in legacy IE
4680 // faking getBBox in exported SVG in legacy IE (is this a duplicate of the fix for #1079?)
4681 if (element.nodeName === 'text') {
4682 element.style.position = 'absolute';
4683 }
4684
4685 return {
4686 x: element.offsetLeft,
4687 y: element.offsetTop,
4688 width: element.offsetWidth,
4689 height: element.offsetHeight
4690 };
4691 },
4692
4693 /**
4694 * VML override private method to update elements based on internal
4695 * properties based on SVG transform
4696 */
4697 htmlUpdateTransform: function() {
4698 // aligning non added elements is expensive
4699 if (!this.added) {
4700 this.alignOnAdd = true;
4701 return;
4702 }
4703
4704 var wrapper = this,
4705 renderer = wrapper.renderer,
4706 elem = wrapper.element,
4707 translateX = wrapper.translateX || 0,
4708 translateY = wrapper.translateY || 0,
4709 x = wrapper.x || 0,
4710 y = wrapper.y || 0,
4711 align = wrapper.textAlign || 'left',
4712 alignCorrection = {
4713 left: 0,
4714 center: 0.5,
4715 right: 1
4716 }[align],
4717 styles = wrapper.styles;
4718
4719 // apply translate
4720 css(elem, {
4721 marginLeft: translateX,
4722 marginTop: translateY
4723 });
4724
4725
4726 if (wrapper.shadows) { // used in labels/tooltip
4727 each(wrapper.shadows, function(shadow) {
4728 css(shadow, {
4729 marginLeft: translateX + 1,
4730 marginTop: translateY + 1
4731 });
4732 });
4733 }
4734
4735
4736 // apply inversion
4737 if (wrapper.inverted) { // wrapper is a group
4738 each(elem.childNodes, function(child) {
4739 renderer.invertChild(child, elem);
4740 });
4741 }
4742
4743 if (elem.tagName === 'SPAN') {
4744
4745 var rotation = wrapper.rotation,
4746 baseline,
4747 textWidth = pInt(wrapper.textWidth),
4748 whiteSpace = styles && styles.whiteSpace,
4749 currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth, wrapper.textAlign].join(',');
4750
4751 if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed
4752
4753
4754 baseline = renderer.fontMetrics(elem.style.fontSize).b;
4755
4756 // Renderer specific handling of span rotation
4757 if (defined(rotation)) {
4758 wrapper.setSpanRotation(rotation, alignCorrection, baseline);
4759 }
4760
4761 // Reset multiline/ellipsis in order to read width (#4928, #5417)
4762 css(elem, {
4763 width: '',
4764 whiteSpace: whiteSpace || 'nowrap'
4765 });
4766
4767 // Update textWidth
4768 if (elem.offsetWidth > textWidth && /[ \-]/.test(elem.textContent || elem.innerText)) { // #983, #1254
4769 css(elem, {
4770 width: textWidth + 'px',
4771 display: 'block',
4772 whiteSpace: whiteSpace || 'normal' // #3331
4773 });
4774 }
4775
4776
4777 wrapper.getSpanCorrection(elem.offsetWidth, baseline, alignCorrection, rotation, align);
4778 }
4779
4780 // apply position with correction
4781 css(elem, {
4782 left: (x + (wrapper.xCorr || 0)) + 'px',
4783 top: (y + (wrapper.yCorr || 0)) + 'px'
4784 });
4785
4786 // force reflow in webkit to apply the left and top on useHTML element (#1249)
4787 if (isWebKit) {
4788 baseline = elem.offsetHeight; // assigned to baseline for lint purpose
4789 }
4790
4791 // record current text transform
4792 wrapper.cTT = currentTextTransform;
4793 }
4794 },
4795
4796 /**
4797 * Set the rotation of an individual HTML span
4798 */
4799 setSpanRotation: function(rotation, alignCorrection, baseline) {
4800 var rotationStyle = {},
4801 cssTransformKey = isMS ? '-ms-transform' : isWebKit ? '-webkit-transform' : isFirefox ? 'MozTransform' : win.opera ? '-o-transform' : '';
4802
4803 rotationStyle[cssTransformKey] = rotationStyle.transform = 'rotate(' + rotation + 'deg)';
4804 rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] = rotationStyle.transformOrigin = (alignCorrection * 100) + '% ' + baseline + 'px';
4805 css(this.element, rotationStyle);
4806 },
4807
4808 /**
4809 * Get the correction in X and Y positioning as the element is rotated.
4810 */
4811 getSpanCorrection: function(width, baseline, alignCorrection) {
4812 this.xCorr = -width * alignCorrection;
4813 this.yCorr = -baseline;
4814 }
4815 });
4816
4817 // Extend SvgRenderer for useHTML option.
4818 extend(SVGRenderer.prototype, {
4819 /**
4820 * Create HTML text node. This is used by the VML renderer as well as the SVG
4821 * renderer through the useHTML option.
4822 *
4823 * @param {String} str
4824 * @param {Number} x
4825 * @param {Number} y
4826 */
4827 html: function(str, x, y) {
4828 var wrapper = this.createElement('span'),
4829 element = wrapper.element,
4830 renderer = wrapper.renderer,
4831 isSVG = renderer.isSVG,
4832 addSetters = function(element, style) {
4833 // These properties are set as attributes on the SVG group, and as
4834 // identical CSS properties on the div. (#3542)
4835 each(['display', 'opacity', 'visibility'], function(prop) {
4836 wrap(element, prop + 'Setter', function(proceed, value, key, elem) {
4837 proceed.call(this, value, key, elem);
4838 style[key] = value;
4839 });
4840 });
4841 };
4842
4843 // Text setter
4844 wrapper.textSetter = function(value) {
4845 if (value !== element.innerHTML) {
4846 delete this.bBox;
4847 }
4848 element.innerHTML = this.textStr = value;
4849 wrapper.htmlUpdateTransform();
4850 };
4851
4852 // Add setters for the element itself (#4938)
4853 if (isSVG) { // #4938, only for HTML within SVG
4854 addSetters(wrapper, wrapper.element.style);
4855 }
4856
4857 // Various setters which rely on update transform
4858 wrapper.xSetter = wrapper.ySetter = wrapper.alignSetter = wrapper.rotationSetter = function(value, key) {
4859 if (key === 'align') {
4860 key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML.
4861 }
4862 wrapper[key] = value;
4863 wrapper.htmlUpdateTransform();
4864 };
4865
4866 // Set the default attributes
4867 wrapper
4868 .attr({
4869 text: str,
4870 x: Math.round(x),
4871 y: Math.round(y)
4872 })
4873 .css({
4874
4875 fontFamily: this.style.fontFamily,
4876 fontSize: this.style.fontSize,
4877
4878 position: 'absolute'
4879 });
4880
4881 // Keep the whiteSpace style outside the wrapper.styles collection
4882 element.style.whiteSpace = 'nowrap';
4883
4884 // Use the HTML specific .css method
4885 wrapper.css = wrapper.htmlCss;
4886
4887 // This is specific for HTML within SVG
4888 if (isSVG) {
4889 wrapper.add = function(svgGroupWrapper) {
4890
4891 var htmlGroup,
4892 container = renderer.box.parentNode,
4893 parentGroup,
4894 parents = [];
4895
4896 this.parentGroup = svgGroupWrapper;
4897
4898 // Create a mock group to hold the HTML elements
4899 if (svgGroupWrapper) {
4900 htmlGroup = svgGroupWrapper.div;
4901 if (!htmlGroup) {
4902
4903 // Read the parent chain into an array and read from top down
4904 parentGroup = svgGroupWrapper;
4905 while (parentGroup) {
4906
4907 parents.push(parentGroup);
4908
4909 // Move up to the next parent group
4910 parentGroup = parentGroup.parentGroup;
4911 }
4912
4913 // Ensure dynamically updating position when any parent is translated
4914 each(parents.reverse(), function(parentGroup) {
4915 var htmlGroupStyle,
4916 cls = attr(parentGroup.element, 'class');
4917
4918 if (cls) {
4919 cls = {
4920 className: cls
4921 };
4922 } // else null
4923
4924 // Create a HTML div and append it to the parent div to emulate
4925 // the SVG group structure
4926 htmlGroup = parentGroup.div = parentGroup.div || createElement('div', cls, {
4927 position: 'absolute',
4928 left: (parentGroup.translateX || 0) + 'px',
4929 top: (parentGroup.translateY || 0) + 'px',
4930 display: parentGroup.display,
4931 opacity: parentGroup.opacity, // #5075
4932 pointerEvents: parentGroup.styles && parentGroup.styles.pointerEvents // #5595
4933 }, htmlGroup || container); // the top group is appended to container
4934
4935 // Shortcut
4936 htmlGroupStyle = htmlGroup.style;
4937
4938 // Set listeners to update the HTML div's position whenever the SVG group
4939 // position is changed
4940 extend(parentGroup, {
4941 translateXSetter: function(value, key) {
4942 htmlGroupStyle.left = value + 'px';
4943 parentGroup[key] = value;
4944 parentGroup.doTransform = true;
4945 },
4946 translateYSetter: function(value, key) {
4947 htmlGroupStyle.top = value + 'px';
4948 parentGroup[key] = value;
4949 parentGroup.doTransform = true;
4950 }
4951 });
4952 addSetters(parentGroup, htmlGroupStyle);
4953 });
4954
4955 }
4956 } else {
4957 htmlGroup = container;
4958 }
4959
4960 htmlGroup.appendChild(element);
4961
4962 // Shared with VML:
4963 wrapper.added = true;
4964 if (wrapper.alignOnAdd) {
4965 wrapper.htmlUpdateTransform();
4966 }
4967
4968 return wrapper;
4969 };
4970 }
4971 return wrapper;
4972 }
4973 });
4974
4975 }(Highcharts));
4976 (function(H) {
4977 /**
4978 * (c) 2010-2016 Torstein Honsi
4979 *
4980 * License: www.highcharts.com/license
4981 */
4982 'use strict';
4983
4984 var VMLRenderer,
4985 VMLRendererExtension,
4986 VMLElement,
4987
4988 createElement = H.createElement,
4989 css = H.css,
4990 defined = H.defined,
4991 deg2rad = H.deg2rad,
4992 discardElement = H.discardElement,
4993 doc = H.doc,
4994 each = H.each,
4995 erase = H.erase,
4996 extend = H.extend,
4997 extendClass = H.extendClass,
4998 isArray = H.isArray,
4999 isNumber = H.isNumber,
5000 isObject = H.isObject,
5001 merge = H.merge,
5002 noop = H.noop,
5003 pick = H.pick,
5004 pInt = H.pInt,
5005 svg = H.svg,
5006 SVGElement = H.SVGElement,
5007 SVGRenderer = H.SVGRenderer,
5008 win = H.win;
5009
5010 /* ****************************************************************************
5011 * *
5012 * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE *
5013 * *
5014 * For applications and websites that don't need IE support, like platform *
5015 * targeted mobile apps and web apps, this code can be removed. *
5016 * *
5017 *****************************************************************************/
5018
5019 /**
5020 * @constructor
5021 */
5022 if (!svg) {
5023
5024 /**
5025 * The VML element wrapper.
5026 */
5027 VMLElement = {
5028
5029 docMode8: doc && doc.documentMode === 8,
5030
5031 /**
5032 * Initialize a new VML element wrapper. It builds the markup as a string
5033 * to minimize DOM traffic.
5034 * @param {Object} renderer
5035 * @param {Object} nodeName
5036 */
5037 init: function(renderer, nodeName) {
5038 var wrapper = this,
5039 markup = ['<', nodeName, ' filled="f" stroked="f"'],
5040 style = ['position: ', 'absolute', ';'],
5041 isDiv = nodeName === 'div';
5042
5043 // divs and shapes need size
5044 if (nodeName === 'shape' || isDiv) {
5045 style.push('left:0;top:0;width:1px;height:1px;');
5046 }
5047 style.push('visibility: ', isDiv ? 'hidden' : 'visible');
5048
5049 markup.push(' style="', style.join(''), '"/>');
5050
5051 // create element with default attributes and style
5052 if (nodeName) {
5053 markup = isDiv || nodeName === 'span' || nodeName === 'img' ?
5054 markup.join('') :
5055 renderer.prepVML(markup);
5056 wrapper.element = createElement(markup);
5057 }
5058
5059 wrapper.renderer = renderer;
5060 },
5061
5062 /**
5063 * Add the node to the given parent
5064 * @param {Object} parent
5065 */
5066 add: function(parent) {
5067 var wrapper = this,
5068 renderer = wrapper.renderer,
5069 element = wrapper.element,
5070 box = renderer.box,
5071 inverted = parent && parent.inverted,
5072
5073 // get the parent node
5074 parentNode = parent ?
5075 parent.element || parent :
5076 box;
5077
5078 if (parent) {
5079 this.parentGroup = parent;
5080 }
5081
5082 // if the parent group is inverted, apply inversion on all children
5083 if (inverted) { // only on groups
5084 renderer.invertChild(element, parentNode);
5085 }
5086
5087 // append it
5088 parentNode.appendChild(element);
5089
5090 // align text after adding to be able to read offset
5091 wrapper.added = true;
5092 if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) {
5093 wrapper.updateTransform();
5094 }
5095
5096 // fire an event for internal hooks
5097 if (wrapper.onAdd) {
5098 wrapper.onAdd();
5099 }
5100
5101 // IE8 Standards can't set the class name before the element is appended
5102 if (this.className) {
5103 this.attr('class', this.className);
5104 }
5105
5106 return wrapper;
5107 },
5108
5109 /**
5110 * VML always uses htmlUpdateTransform
5111 */
5112 updateTransform: SVGElement.prototype.htmlUpdateTransform,
5113
5114 /**
5115 * Set the rotation of a span with oldIE's filter
5116 */
5117 setSpanRotation: function() {
5118 // Adjust for alignment and rotation. Rotation of useHTML content is not yet implemented
5119 // but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+
5120 // has support for CSS3 transform. The getBBox method also needs to be updated
5121 // to compensate for the rotation, like it currently does for SVG.
5122 // Test case: http://jsfiddle.net/highcharts/Ybt44/
5123
5124 var rotation = this.rotation,
5125 costheta = Math.cos(rotation * deg2rad),
5126 sintheta = Math.sin(rotation * deg2rad);
5127
5128 css(this.element, {
5129 filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta,
5130 ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta,
5131 ', sizingMethod=\'auto expand\')'
5132 ].join('') : 'none'
5133 });
5134 },
5135
5136 /**
5137 * Get the positioning correction for the span after rotating.
5138 */
5139 getSpanCorrection: function(width, baseline, alignCorrection, rotation, align) {
5140
5141 var costheta = rotation ? Math.cos(rotation * deg2rad) : 1,
5142 sintheta = rotation ? Math.sin(rotation * deg2rad) : 0,
5143 height = pick(this.elemHeight, this.element.offsetHeight),
5144 quad,
5145 nonLeft = align && align !== 'left';
5146
5147 // correct x and y
5148 this.xCorr = costheta < 0 && -width;
5149 this.yCorr = sintheta < 0 && -height;
5150
5151 // correct for baseline and corners spilling out after rotation
5152 quad = costheta * sintheta < 0;
5153 this.xCorr += sintheta * baseline * (quad ? 1 - alignCorrection : alignCorrection);
5154 this.yCorr -= costheta * baseline * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1);
5155 // correct for the length/height of the text
5156 if (nonLeft) {
5157 this.xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1);
5158 if (rotation) {
5159 this.yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1);
5160 }
5161 css(this.element, {
5162 textAlign: align
5163 });
5164 }
5165 },
5166
5167 /**
5168 * Converts a subset of an SVG path definition to its VML counterpart. Takes an array
5169 * as the parameter and returns a string.
5170 */
5171 pathToVML: function(value) {
5172 // convert paths
5173 var i = value.length,
5174 path = [];
5175
5176 while (i--) {
5177
5178 // Multiply by 10 to allow subpixel precision.
5179 // Substracting half a pixel seems to make the coordinates
5180 // align with SVG, but this hasn't been tested thoroughly
5181 if (isNumber(value[i])) {
5182 path[i] = Math.round(value[i] * 10) - 5;
5183 } else if (value[i] === 'Z') { // close the path
5184 path[i] = 'x';
5185 } else {
5186 path[i] = value[i];
5187
5188 // When the start X and end X coordinates of an arc are too close,
5189 // they are rounded to the same value above. In this case, substract or
5190 // add 1 from the end X and Y positions. #186, #760, #1371, #1410.
5191 if (value.isArc && (value[i] === 'wa' || value[i] === 'at')) {
5192 // Start and end X
5193 if (path[i + 5] === path[i + 7]) {
5194 path[i + 7] += value[i + 7] > value[i + 5] ? 1 : -1;
5195 }
5196 // Start and end Y
5197 if (path[i + 6] === path[i + 8]) {
5198 path[i + 8] += value[i + 8] > value[i + 6] ? 1 : -1;
5199 }
5200 }
5201 }
5202 }
5203
5204
5205 // Loop up again to handle path shortcuts (#2132)
5206 /*while (i++ < path.length) {
5207 if (path[i] === 'H') { // horizontal line to
5208 path[i] = 'L';
5209 path.splice(i + 2, 0, path[i - 1]);
5210 } else if (path[i] === 'V') { // vertical line to
5211 path[i] = 'L';
5212 path.splice(i + 1, 0, path[i - 2]);
5213 }
5214 }*/
5215 return path.join(' ') || 'x';
5216 },
5217
5218 /**
5219 * Set the element's clipping to a predefined rectangle
5220 *
5221 * @param {String} id The id of the clip rectangle
5222 */
5223 clip: function(clipRect) {
5224 var wrapper = this,
5225 clipMembers,
5226 cssRet;
5227
5228 if (clipRect) {
5229 clipMembers = clipRect.members;
5230 erase(clipMembers, wrapper); // Ensure unique list of elements (#1258)
5231 clipMembers.push(wrapper);
5232 wrapper.destroyClip = function() {
5233 erase(clipMembers, wrapper);
5234 };
5235 cssRet = clipRect.getCSS(wrapper);
5236
5237 } else {
5238 if (wrapper.destroyClip) {
5239 wrapper.destroyClip();
5240 }
5241 cssRet = {
5242 clip: wrapper.docMode8 ? 'inherit' : 'rect(auto)'
5243 }; // #1214
5244 }
5245
5246 return wrapper.css(cssRet);
5247
5248 },
5249
5250 /**
5251 * Set styles for the element
5252 * @param {Object} styles
5253 */
5254 css: SVGElement.prototype.htmlCss,
5255
5256 /**
5257 * Removes a child either by removeChild or move to garbageBin.
5258 * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not.
5259 */
5260 safeRemoveChild: function(element) {
5261 // discardElement will detach the node from its parent before attaching it
5262 // to the garbage bin. Therefore it is important that the node is attached and have parent.
5263 if (element.parentNode) {
5264 discardElement(element);
5265 }
5266 },
5267
5268 /**
5269 * Extend element.destroy by removing it from the clip members array
5270 */
5271 destroy: function() {
5272 if (this.destroyClip) {
5273 this.destroyClip();
5274 }
5275
5276 return SVGElement.prototype.destroy.apply(this);
5277 },
5278
5279 /**
5280 * Add an event listener. VML override for normalizing event parameters.
5281 * @param {String} eventType
5282 * @param {Function} handler
5283 */
5284 on: function(eventType, handler) {
5285 // simplest possible event model for internal use
5286 this.element['on' + eventType] = function() {
5287 var evt = win.event;
5288 evt.target = evt.srcElement;
5289 handler(evt);
5290 };
5291 return this;
5292 },
5293
5294 /**
5295 * In stacked columns, cut off the shadows so that they don't overlap
5296 */
5297 cutOffPath: function(path, length) {
5298
5299 var len;
5300
5301 path = path.split(/[ ,]/); // The extra comma tricks the trailing comma remover in "gulp scripts" task
5302 len = path.length;
5303
5304 if (len === 9 || len === 11) {
5305 path[len - 4] = path[len - 2] = pInt(path[len - 2]) - 10 * length;
5306 }
5307 return path.join(' ');
5308 },
5309
5310 /**
5311 * Apply a drop shadow by copying elements and giving them different strokes
5312 * @param {Boolean|Object} shadowOptions
5313 */
5314 shadow: function(shadowOptions, group, cutOff) {
5315 var shadows = [],
5316 i,
5317 element = this.element,
5318 renderer = this.renderer,
5319 shadow,
5320 elemStyle = element.style,
5321 markup,
5322 path = element.path,
5323 strokeWidth,
5324 modifiedPath,
5325 shadowWidth,
5326 shadowElementOpacity;
5327
5328 // some times empty paths are not strings
5329 if (path && typeof path.value !== 'string') {
5330 path = 'x';
5331 }
5332 modifiedPath = path;
5333
5334 if (shadowOptions) {
5335 shadowWidth = pick(shadowOptions.width, 3);
5336 shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth;
5337 for (i = 1; i <= 3; i++) {
5338
5339 strokeWidth = (shadowWidth * 2) + 1 - (2 * i);
5340
5341 // Cut off shadows for stacked column items
5342 if (cutOff) {
5343 modifiedPath = this.cutOffPath(path.value, strokeWidth + 0.5);
5344 }
5345
5346 markup = ['<shape isShadow="true" strokeweight="', strokeWidth,
5347 '" filled="false" path="', modifiedPath,
5348 '" coordsize="10 10" style="', element.style.cssText, '" />'
5349 ];
5350
5351 shadow = createElement(renderer.prepVML(markup),
5352 null, {
5353 left: pInt(elemStyle.left) + pick(shadowOptions.offsetX, 1),
5354 top: pInt(elemStyle.top) + pick(shadowOptions.offsetY, 1)
5355 }
5356 );
5357 if (cutOff) {
5358 shadow.cutOff = strokeWidth + 1;
5359 }
5360
5361 // apply the opacity
5362 markup = ['<stroke color="', shadowOptions.color || '#000000', '" opacity="', shadowElementOpacity * i, '"/>'];
5363 createElement(renderer.prepVML(markup), null, null, shadow);
5364
5365
5366 // insert it
5367 if (group) {
5368 group.element.appendChild(shadow);
5369 } else {
5370 element.parentNode.insertBefore(shadow, element);
5371 }
5372
5373 // record it
5374 shadows.push(shadow);
5375
5376 }
5377
5378 this.shadows = shadows;
5379 }
5380 return this;
5381 },
5382 updateShadows: noop, // Used in SVG only
5383
5384 setAttr: function(key, value) {
5385 if (this.docMode8) { // IE8 setAttribute bug
5386 this.element[key] = value;
5387 } else {
5388 this.element.setAttribute(key, value);
5389 }
5390 },
5391 classSetter: function(value) {
5392 // IE8 Standards mode has problems retrieving the className unless set like this.
5393 // IE8 Standards can't set the class name before the element is appended.
5394 (this.added ? this.element : this).className = value;
5395 },
5396 dashstyleSetter: function(value, key, element) {
5397 var strokeElem = element.getElementsByTagName('stroke')[0] ||
5398 createElement(this.renderer.prepVML(['<stroke/>']), null, null, element);
5399 strokeElem[key] = value || 'solid';
5400 this[key] = value;
5401 /* because changing stroke-width will change the dash length
5402 and cause an epileptic effect */
5403 },
5404 dSetter: function(value, key, element) {
5405 var i,
5406 shadows = this.shadows;
5407 value = value || [];
5408 this.d = value.join && value.join(' '); // used in getter for animation
5409
5410 element.path = value = this.pathToVML(value);
5411
5412 // update shadows
5413 if (shadows) {
5414 i = shadows.length;
5415 while (i--) {
5416 shadows[i].path = shadows[i].cutOff ? this.cutOffPath(value, shadows[i].cutOff) : value;
5417 }
5418 }
5419 this.setAttr(key, value);
5420 },
5421 fillSetter: function(value, key, element) {
5422 var nodeName = element.nodeName;
5423 if (nodeName === 'SPAN') { // text color
5424 element.style.color = value;
5425 } else if (nodeName !== 'IMG') { // #1336
5426 element.filled = value !== 'none';
5427 this.setAttr('fillcolor', this.renderer.color(value, element, key, this));
5428 }
5429 },
5430 'fill-opacitySetter': function(value, key, element) {
5431 createElement(
5432 this.renderer.prepVML(['<', key.split('-')[0], ' opacity="', value, '"/>']),
5433 null,
5434 null,
5435 element
5436 );
5437 },
5438 opacitySetter: noop, // Don't bother - animation is too slow and filters introduce artifacts
5439 rotationSetter: function(value, key, element) {
5440 var style = element.style;
5441 this[key] = style[key] = value; // style is for #1873
5442
5443 // Correction for the 1x1 size of the shape container. Used in gauge needles.
5444 style.left = -Math.round(Math.sin(value * deg2rad) + 1) + 'px';
5445 style.top = Math.round(Math.cos(value * deg2rad)) + 'px';
5446 },
5447 strokeSetter: function(value, key, element) {
5448 this.setAttr('strokecolor', this.renderer.color(value, element, key, this));
5449 },
5450 'stroke-widthSetter': function(value, key, element) {
5451 element.stroked = !!value; // VML "stroked" attribute
5452 this[key] = value; // used in getter, issue #113
5453 if (isNumber(value)) {
5454 value += 'px';
5455 }
5456 this.setAttr('strokeweight', value);
5457 },
5458 titleSetter: function(value, key) {
5459 this.setAttr(key, value);
5460 },
5461 visibilitySetter: function(value, key, element) {
5462
5463 // Handle inherited visibility
5464 if (value === 'inherit') {
5465 value = 'visible';
5466 }
5467
5468 // Let the shadow follow the main element
5469 if (this.shadows) {
5470 each(this.shadows, function(shadow) {
5471 shadow.style[key] = value;
5472 });
5473 }
5474
5475 // Instead of toggling the visibility CSS property, move the div out of the viewport.
5476 // This works around #61 and #586
5477 if (element.nodeName === 'DIV') {
5478 value = value === 'hidden' ? '-999em' : 0;
5479
5480 // In order to redraw, IE7 needs the div to be visible when tucked away
5481 // outside the viewport. So the visibility is actually opposite of
5482 // the expected value. This applies to the tooltip only.
5483 if (!this.docMode8) {
5484 element.style[key] = value ? 'visible' : 'hidden';
5485 }
5486 key = 'top';
5487 }
5488 element.style[key] = value;
5489 },
5490 displaySetter: function(value, key, element) {
5491 element.style[key] = value;
5492 },
5493 xSetter: function(value, key, element) {
5494 this[key] = value; // used in getter
5495
5496 if (key === 'x') {
5497 key = 'left';
5498 } else if (key === 'y') {
5499 key = 'top';
5500 }
5501 /* else {
5502 value = Math.max(0, value); // don't set width or height below zero (#311)
5503 }*/
5504
5505 // clipping rectangle special
5506 if (this.updateClipping) {
5507 this[key] = value; // the key is now 'left' or 'top' for 'x' and 'y'
5508 this.updateClipping();
5509 } else {
5510 // normal
5511 element.style[key] = value;
5512 }
5513 },
5514 zIndexSetter: function(value, key, element) {
5515 element.style[key] = value;
5516 }
5517 };
5518 VMLElement['stroke-opacitySetter'] = VMLElement['fill-opacitySetter'];
5519 H.VMLElement = VMLElement = extendClass(SVGElement, VMLElement);
5520
5521 // Some shared setters
5522 VMLElement.prototype.ySetter =
5523 VMLElement.prototype.widthSetter =
5524 VMLElement.prototype.heightSetter =
5525 VMLElement.prototype.xSetter;
5526
5527
5528 /**
5529 * The VML renderer
5530 */
5531 VMLRendererExtension = { // inherit SVGRenderer
5532
5533 Element: VMLElement,
5534 isIE8: win.navigator.userAgent.indexOf('MSIE 8.0') > -1,
5535
5536
5537 /**
5538 * Initialize the VMLRenderer
5539 * @param {Object} container
5540 * @param {Number} width
5541 * @param {Number} height
5542 */
5543 init: function(container, width, height) {
5544 var renderer = this,
5545 boxWrapper,
5546 box,
5547 css;
5548
5549 renderer.alignedObjects = [];
5550
5551 boxWrapper = renderer.createElement('div')
5552 .css({
5553 position: 'relative'
5554 });
5555 box = boxWrapper.element;
5556 container.appendChild(boxWrapper.element);
5557
5558
5559 // generate the containing box
5560 renderer.isVML = true;
5561 renderer.box = box;
5562 renderer.boxWrapper = boxWrapper;
5563 renderer.gradients = {};
5564 renderer.cache = {}; // Cache for numerical bounding boxes
5565 renderer.cacheKeys = [];
5566 renderer.imgCount = 0;
5567
5568
5569 renderer.setSize(width, height, false);
5570
5571 // The only way to make IE6 and IE7 print is to use a global namespace. However,
5572 // with IE8 the only way to make the dynamic shapes visible in screen and print mode
5573 // seems to be to add the xmlns attribute and the behaviour style inline.
5574 if (!doc.namespaces.hcv) {
5575
5576 doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml');
5577
5578 // Setup default CSS (#2153, #2368, #2384)
5579 css = 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke' +
5580 '{ behavior:url(#default#VML); display: inline-block; } ';
5581 try {
5582 doc.createStyleSheet().cssText = css;
5583 } catch (e) {
5584 doc.styleSheets[0].cssText += css;
5585 }
5586
5587 }
5588 },
5589
5590
5591 /**
5592 * Detect whether the renderer is hidden. This happens when one of the parent elements
5593 * has display: none
5594 */
5595 isHidden: function() {
5596 return !this.box.offsetWidth;
5597 },
5598
5599 /**
5600 * Define a clipping rectangle. In VML it is accomplished by storing the values
5601 * for setting the CSS style to all associated members.
5602 *
5603 * @param {Number} x
5604 * @param {Number} y
5605 * @param {Number} width
5606 * @param {Number} height
5607 */
5608 clipRect: function(x, y, width, height) {
5609
5610 // create a dummy element
5611 var clipRect = this.createElement(),
5612 isObj = isObject(x);
5613
5614 // mimic a rectangle with its style object for automatic updating in attr
5615 return extend(clipRect, {
5616 members: [],
5617 count: 0,
5618 left: (isObj ? x.x : x) + 1,
5619 top: (isObj ? x.y : y) + 1,
5620 width: (isObj ? x.width : width) - 1,
5621 height: (isObj ? x.height : height) - 1,
5622 getCSS: function(wrapper) {
5623 var element = wrapper.element,
5624 nodeName = element.nodeName,
5625 isShape = nodeName === 'shape',
5626 inverted = wrapper.inverted,
5627 rect = this,
5628 top = rect.top - (isShape ? element.offsetTop : 0),
5629 left = rect.left,
5630 right = left + rect.width,
5631 bottom = top + rect.height,
5632 ret = {
5633 clip: 'rect(' +
5634 Math.round(inverted ? left : top) + 'px,' +
5635 Math.round(inverted ? bottom : right) + 'px,' +
5636 Math.round(inverted ? right : bottom) + 'px,' +
5637 Math.round(inverted ? top : left) + 'px)'
5638 };
5639
5640 // issue 74 workaround
5641 if (!inverted && wrapper.docMode8 && nodeName === 'DIV') {
5642 extend(ret, {
5643 width: right + 'px',
5644 height: bottom + 'px'
5645 });
5646 }
5647 return ret;
5648 },
5649
5650 // used in attr and animation to update the clipping of all members
5651 updateClipping: function() {
5652 each(clipRect.members, function(member) {
5653 if (member.element) { // Deleted series, like in stock/members/series-remove demo. Should be removed from members, but this will do.
5654 member.css(clipRect.getCSS(member));
5655 }
5656 });
5657 }
5658 });
5659
5660 },
5661
5662
5663 /**
5664 * Take a color and return it if it's a string, make it a gradient if it's a
5665 * gradient configuration object, and apply opacity.
5666 *
5667 * @param {Object} color The color or config object
5668 */
5669 color: function(color, elem, prop, wrapper) {
5670 var renderer = this,
5671 colorObject,
5672 regexRgba = /^rgba/,
5673 markup,
5674 fillType,
5675 ret = 'none';
5676
5677 // Check for linear or radial gradient
5678 if (color && color.linearGradient) {
5679 fillType = 'gradient';
5680 } else if (color && color.radialGradient) {
5681 fillType = 'pattern';
5682 }
5683
5684
5685 if (fillType) {
5686
5687 var stopColor,
5688 stopOpacity,
5689 gradient = color.linearGradient || color.radialGradient,
5690 x1,
5691 y1,
5692 x2,
5693 y2,
5694 opacity1,
5695 opacity2,
5696 color1,
5697 color2,
5698 fillAttr = '',
5699 stops = color.stops,
5700 firstStop,
5701 lastStop,
5702 colors = [],
5703 addFillNode = function() {
5704 // Add the fill subnode. When colors attribute is used, the meanings of opacity and o:opacity2
5705 // are reversed.
5706 markup = ['<fill colors="' + colors.join(',') + '" opacity="', opacity2, '" o:opacity2="', opacity1,
5707 '" type="', fillType, '" ', fillAttr, 'focus="100%" method="any" />'
5708 ];
5709 createElement(renderer.prepVML(markup), null, null, elem);
5710 };
5711
5712 // Extend from 0 to 1
5713 firstStop = stops[0];
5714 lastStop = stops[stops.length - 1];
5715 if (firstStop[0] > 0) {
5716 stops.unshift([
5717 0,
5718 firstStop[1]
5719 ]);
5720 }
5721 if (lastStop[0] < 1) {
5722 stops.push([
5723 1,
5724 lastStop[1]
5725 ]);
5726 }
5727
5728 // Compute the stops
5729 each(stops, function(stop, i) {
5730 if (regexRgba.test(stop[1])) {
5731 colorObject = H.color(stop[1]);
5732 stopColor = colorObject.get('rgb');
5733 stopOpacity = colorObject.get('a');
5734 } else {
5735 stopColor = stop[1];
5736 stopOpacity = 1;
5737 }
5738
5739 // Build the color attribute
5740 colors.push((stop[0] * 100) + '% ' + stopColor);
5741
5742 // Only start and end opacities are allowed, so we use the first and the last
5743 if (!i) {
5744 opacity1 = stopOpacity;
5745 color2 = stopColor;
5746 } else {
5747 opacity2 = stopOpacity;
5748 color1 = stopColor;
5749 }
5750 });
5751
5752 // Apply the gradient to fills only.
5753 if (prop === 'fill') {
5754
5755 // Handle linear gradient angle
5756 if (fillType === 'gradient') {
5757 x1 = gradient.x1 || gradient[0] || 0;
5758 y1 = gradient.y1 || gradient[1] || 0;
5759 x2 = gradient.x2 || gradient[2] || 0;
5760 y2 = gradient.y2 || gradient[3] || 0;
5761 fillAttr = 'angle="' + (90 - Math.atan(
5762 (y2 - y1) / // y vector
5763 (x2 - x1) // x vector
5764 ) * 180 / Math.PI) + '"';
5765
5766 addFillNode();
5767
5768 // Radial (circular) gradient
5769 } else {
5770
5771 var r = gradient.r,
5772 sizex = r * 2,
5773 sizey = r * 2,
5774 cx = gradient.cx,
5775 cy = gradient.cy,
5776 radialReference = elem.radialReference,
5777 bBox,
5778 applyRadialGradient = function() {
5779 if (radialReference) {
5780 bBox = wrapper.getBBox();
5781 cx += (radialReference[0] - bBox.x) / bBox.width - 0.5;
5782 cy += (radialReference[1] - bBox.y) / bBox.height - 0.5;
5783 sizex *= radialReference[2] / bBox.width;
5784 sizey *= radialReference[2] / bBox.height;
5785 }
5786 fillAttr = 'src="' + H.getOptions().global.VMLRadialGradientURL + '" ' +
5787 'size="' + sizex + ',' + sizey + '" ' +
5788 'origin="0.5,0.5" ' +
5789 'position="' + cx + ',' + cy + '" ' +
5790 'color2="' + color2 + '" ';
5791
5792 addFillNode();
5793 };
5794
5795 // Apply radial gradient
5796 if (wrapper.added) {
5797 applyRadialGradient();
5798 } else {
5799 // We need to know the bounding box to get the size and position right
5800 wrapper.onAdd = applyRadialGradient;
5801 }
5802
5803 // The fill element's color attribute is broken in IE8 standards mode, so we
5804 // need to set the parent shape's fillcolor attribute instead.
5805 ret = color1;
5806 }
5807
5808 // Gradients are not supported for VML stroke, return the first color. #722.
5809 } else {
5810 ret = stopColor;
5811 }
5812
5813 // If the color is an rgba color, split it and add a fill node
5814 // to hold the opacity component
5815 } else if (regexRgba.test(color) && elem.tagName !== 'IMG') {
5816
5817 colorObject = H.color(color);
5818
5819 wrapper[prop + '-opacitySetter'](colorObject.get('a'), prop, elem);
5820
5821 ret = colorObject.get('rgb');
5822
5823
5824 } else {
5825 var propNodes = elem.getElementsByTagName(prop); // 'stroke' or 'fill' node
5826 if (propNodes.length) {
5827 propNodes[0].opacity = 1;
5828 propNodes[0].type = 'solid';
5829 }
5830 ret = color;
5831 }
5832
5833 return ret;
5834 },
5835
5836 /**
5837 * Take a VML string and prepare it for either IE8 or IE6/IE7.
5838 * @param {Array} markup A string array of the VML markup to prepare
5839 */
5840 prepVML: function(markup) {
5841 var vmlStyle = 'display:inline-block;behavior:url(#default#VML);',
5842 isIE8 = this.isIE8;
5843
5844 markup = markup.join('');
5845
5846 if (isIE8) { // add xmlns and style inline
5847 markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />');
5848 if (markup.indexOf('style="') === -1) {
5849 markup = markup.replace('/>', ' style="' + vmlStyle + '" />');
5850 } else {
5851 markup = markup.replace('style="', 'style="' + vmlStyle);
5852 }
5853
5854 } else { // add namespace
5855 markup = markup.replace('<', '<hcv:');
5856 }
5857
5858 return markup;
5859 },
5860
5861 /**
5862 * Create rotated and aligned text
5863 * @param {String} str
5864 * @param {Number} x
5865 * @param {Number} y
5866 */
5867 text: SVGRenderer.prototype.html,
5868
5869 /**
5870 * Create and return a path element
5871 * @param {Array} path
5872 */
5873 path: function(path) {
5874 var attr = {
5875 // subpixel precision down to 0.1 (width and height = 1px)
5876 coordsize: '10 10'
5877 };
5878 if (isArray(path)) {
5879 attr.d = path;
5880 } else if (isObject(path)) { // attributes
5881 extend(attr, path);
5882 }
5883 // create the shape
5884 return this.createElement('shape').attr(attr);
5885 },
5886
5887 /**
5888 * Create and return a circle element. In VML circles are implemented as
5889 * shapes, which is faster than v:oval
5890 * @param {Number} x
5891 * @param {Number} y
5892 * @param {Number} r
5893 */
5894 circle: function(x, y, r) {
5895 var circle = this.symbol('circle');
5896 if (isObject(x)) {
5897 r = x.r;
5898 y = x.y;
5899 x = x.x;
5900 }
5901 circle.isCircle = true; // Causes x and y to mean center (#1682)
5902 circle.r = r;
5903 return circle.attr({
5904 x: x,
5905 y: y
5906 });
5907 },
5908
5909 /**
5910 * Create a group using an outer div and an inner v:group to allow rotating
5911 * and flipping. A simple v:group would have problems with positioning
5912 * child HTML elements and CSS clip.
5913 *
5914 * @param {String} name The name of the group
5915 */
5916 g: function(name) {
5917 var wrapper,
5918 attribs;
5919
5920 // set the class name
5921 if (name) {
5922 attribs = {
5923 'className': 'highcharts-' + name,
5924 'class': 'highcharts-' + name
5925 };
5926 }
5927
5928 // the div to hold HTML and clipping
5929 wrapper = this.createElement('div').attr(attribs);
5930
5931 return wrapper;
5932 },
5933
5934 /**
5935 * VML override to create a regular HTML image
5936 * @param {String} src
5937 * @param {Number} x
5938 * @param {Number} y
5939 * @param {Number} width
5940 * @param {Number} height
5941 */
5942 image: function(src, x, y, width, height) {
5943 var obj = this.createElement('img')
5944 .attr({
5945 src: src
5946 });
5947
5948 if (arguments.length > 1) {
5949 obj.attr({
5950 x: x,
5951 y: y,
5952 width: width,
5953 height: height
5954 });
5955 }
5956 return obj;
5957 },
5958
5959 /**
5960 * For rectangles, VML uses a shape for rect to overcome bugs and rotation problems
5961 */
5962 createElement: function(nodeName) {
5963 return nodeName === 'rect' ? this.symbol(nodeName) : SVGRenderer.prototype.createElement.call(this, nodeName);
5964 },
5965
5966 /**
5967 * In the VML renderer, each child of an inverted div (group) is inverted
5968 * @param {Object} element
5969 * @param {Object} parentNode
5970 */
5971 invertChild: function(element, parentNode) {
5972 var ren = this,
5973 parentStyle = parentNode.style,
5974 imgStyle = element.tagName === 'IMG' && element.style; // #1111
5975
5976 css(element, {
5977 flip: 'x',
5978 left: pInt(parentStyle.width) - (imgStyle ? pInt(imgStyle.top) : 1),
5979 top: pInt(parentStyle.height) - (imgStyle ? pInt(imgStyle.left) : 1),
5980 rotation: -90
5981 });
5982
5983 // Recursively invert child elements, needed for nested composite shapes like box plots and error bars. #1680, #1806.
5984 each(element.childNodes, function(child) {
5985 ren.invertChild(child, element);
5986 });
5987 },
5988
5989 /**
5990 * Symbol definitions that override the parent SVG renderer's symbols
5991 *
5992 */
5993 symbols: {
5994 // VML specific arc function
5995 arc: function(x, y, w, h, options) {
5996 var start = options.start,
5997 end = options.end,
5998 radius = options.r || w || h,
5999 innerRadius = options.innerR,
6000 cosStart = Math.cos(start),
6001 sinStart = Math.sin(start),
6002 cosEnd = Math.cos(end),
6003 sinEnd = Math.sin(end),
6004 ret;
6005
6006 if (end - start === 0) { // no angle, don't show it.
6007 return ['x'];
6008 }
6009
6010 ret = [
6011 'wa', // clockwise arc to
6012 x - radius, // left
6013 y - radius, // top
6014 x + radius, // right
6015 y + radius, // bottom
6016 x + radius * cosStart, // start x
6017 y + radius * sinStart, // start y
6018 x + radius * cosEnd, // end x
6019 y + radius * sinEnd // end y
6020 ];
6021
6022 if (options.open && !innerRadius) {
6023 ret.push(
6024 'e',
6025 'M',
6026 x, // - innerRadius,
6027 y // - innerRadius
6028 );
6029 }
6030
6031 ret.push(
6032 'at', // anti clockwise arc to
6033 x - innerRadius, // left
6034 y - innerRadius, // top
6035 x + innerRadius, // right
6036 y + innerRadius, // bottom
6037 x + innerRadius * cosEnd, // start x
6038 y + innerRadius * sinEnd, // start y
6039 x + innerRadius * cosStart, // end x
6040 y + innerRadius * sinStart, // end y
6041 'x', // finish path
6042 'e' // close
6043 );
6044
6045 ret.isArc = true;
6046 return ret;
6047
6048 },
6049 // Add circle symbol path. This performs significantly faster than v:oval.
6050 circle: function(x, y, w, h, wrapper) {
6051
6052 if (wrapper && defined(wrapper.r)) {
6053 w = h = 2 * wrapper.r;
6054 }
6055
6056 // Center correction, #1682
6057 if (wrapper && wrapper.isCircle) {
6058 x -= w / 2;
6059 y -= h / 2;
6060 }
6061
6062 // Return the path
6063 return [
6064 'wa', // clockwisearcto
6065 x, // left
6066 y, // top
6067 x + w, // right
6068 y + h, // bottom
6069 x + w, // start x
6070 y + h / 2, // start y
6071 x + w, // end x
6072 y + h / 2, // end y
6073 //'x', // finish path
6074 'e' // close
6075 ];
6076 },
6077 /**
6078 * Add rectangle symbol path which eases rotation and omits arcsize problems
6079 * compared to the built-in VML roundrect shape. When borders are not rounded,
6080 * use the simpler square path, else use the callout path without the arrow.
6081 */
6082 rect: function(x, y, w, h, options) {
6083 return SVGRenderer.prototype.symbols[!defined(options) || !options.r ? 'square' : 'callout'].call(0, x, y, w, h, options);
6084 }
6085 }
6086 };
6087 H.VMLRenderer = VMLRenderer = function() {
6088 this.init.apply(this, arguments);
6089 };
6090 VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension);
6091
6092 // general renderer
6093 H.Renderer = VMLRenderer;
6094 }
6095
6096 // This method is used with exporting in old IE, when emulating SVG (see #2314)
6097 SVGRenderer.prototype.measureSpanWidth = function(text, styles) {
6098 var measuringSpan = doc.createElement('span'),
6099 offsetWidth,
6100 textNode = doc.createTextNode(text);
6101
6102 measuringSpan.appendChild(textNode);
6103 css(measuringSpan, styles);
6104 this.box.appendChild(measuringSpan);
6105 offsetWidth = measuringSpan.offsetWidth;
6106 discardElement(measuringSpan); // #2463
6107 return offsetWidth;
6108 };
6109
6110
6111 /* ****************************************************************************
6112 * *
6113 * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE *
6114 * *
6115 *****************************************************************************/
6116
6117
6118 }(Highcharts));
6119 (function(H) {
6120 /**
6121 * (c) 2010-2016 Torstein Honsi
6122 *
6123 * License: www.highcharts.com/license
6124 */
6125 'use strict';
6126 var color = H.color,
6127 each = H.each,
6128 getTZOffset = H.getTZOffset,
6129 isTouchDevice = H.isTouchDevice,
6130 merge = H.merge,
6131 pick = H.pick,
6132 svg = H.svg,
6133 win = H.win;
6134
6135 /* ****************************************************************************
6136 * Handle the options *
6137 *****************************************************************************/
6138 H.defaultOptions = {
6139
6140 colors: '#7cb5ec #434348 #90ed7d #f7a35c #8085e9 #f15c80 #e4d354 #2b908f #f45b5b #91e8e1'.split(' '),
6141
6142 symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'],
6143 lang: {
6144 loading: 'Loading...',
6145 months: ['January', 'February', 'March', 'April', 'May', 'June', 'July',
6146 'August', 'September', 'October', 'November', 'December'
6147 ],
6148 shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
6149 weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
6150 // invalidDate: '',
6151 decimalPoint: '.',
6152 numericSymbols: ['k', 'M', 'G', 'T', 'P', 'E'], // SI prefixes used in axis labels
6153 resetZoom: 'Reset zoom',
6154 resetZoomTitle: 'Reset zoom level 1:1',
6155 thousandsSep: ' '
6156 },
6157 global: {
6158 useUTC: true,
6159 //timezoneOffset: 0,
6160
6161 VMLRadialGradientURL: 'http://code.highcharts.com@product.cdnpath@/5.0.0/gfx/vml-radial-gradient.png'
6162
6163 },
6164 chart: {
6165 //animation: true,
6166 //alignTicks: false,
6167 //reflow: true,
6168 //className: null,
6169 //events: { load, selection },
6170 //margin: [null],
6171 //marginTop: null,
6172 //marginRight: null,
6173 //marginBottom: null,
6174 //marginLeft: null,
6175 borderRadius: 0,
6176
6177 defaultSeriesType: 'line',
6178 ignoreHiddenSeries: true,
6179 //inverted: false,
6180 spacing: [10, 10, 15, 10],
6181 //spacingTop: 10,
6182 //spacingRight: 10,
6183 //spacingBottom: 15,
6184 //spacingLeft: 10,
6185 //zoomType: ''
6186 resetZoomButton: {
6187 theme: {
6188 zIndex: 20
6189 },
6190 position: {
6191 align: 'right',
6192 x: -10,
6193 //verticalAlign: 'top',
6194 y: 10
6195 }
6196 // relativeTo: 'plot'
6197 },
6198 width: null,
6199 height: null,
6200
6201
6202 borderColor: '#335cad',
6203 //borderWidth: 0,
6204 //style: {
6205 // fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font
6206 // fontSize: '12px'
6207 //},
6208 backgroundColor: '#ffffff',
6209 //plotBackgroundColor: null,
6210 plotBorderColor: '#cccccc'
6211 //plotBorderWidth: 0,
6212 //plotShadow: false
6213
6214 },
6215
6216 title: {
6217 text: 'Chart title',
6218 align: 'center',
6219 // floating: false,
6220 margin: 15,
6221 // x: 0,
6222 // verticalAlign: 'top',
6223 // y: null,
6224
6225 style: {
6226 color: '#333333',
6227 fontSize: '18px'
6228 },
6229
6230 widthAdjust: -44
6231
6232 },
6233 subtitle: {
6234 text: '',
6235 align: 'center',
6236 // floating: false
6237 // x: 0,
6238 // verticalAlign: 'top',
6239 // y: null,
6240
6241 style: {
6242 color: '#666666'
6243 },
6244
6245 widthAdjust: -44
6246 },
6247
6248 plotOptions: {},
6249 labels: {
6250 //items: [],
6251 style: {
6252 //font: defaultFont,
6253 position: 'absolute',
6254 color: '#333333'
6255 }
6256 },
6257 legend: {
6258 enabled: true,
6259 align: 'center',
6260 //floating: false,
6261 layout: 'horizontal',
6262 labelFormatter: function() {
6263 return this.name;
6264 },
6265 //borderWidth: 0,
6266 borderColor: '#999999',
6267 borderRadius: 0,
6268 navigation: {
6269
6270 activeColor: '#003399',
6271 inactiveColor: '#cccccc'
6272
6273 // animation: true,
6274 // arrowSize: 12
6275 // style: {} // text styles
6276 },
6277 // margin: 20,
6278 // reversed: false,
6279 // backgroundColor: null,
6280 /*style: {
6281 padding: '5px'
6282 },*/
6283
6284 itemStyle: {
6285 color: '#333333',
6286 fontSize: '12px',
6287 fontWeight: 'bold'
6288 },
6289 itemHoverStyle: {
6290 //cursor: 'pointer', removed as of #601
6291 color: '#000000'
6292 },
6293 itemHiddenStyle: {
6294 color: '#cccccc'
6295 },
6296 shadow: false,
6297
6298 itemCheckboxStyle: {
6299 position: 'absolute',
6300 width: '13px', // for IE precision
6301 height: '13px'
6302 },
6303 // itemWidth: undefined,
6304 squareSymbol: true,
6305 // symbolRadius: 0,
6306 // symbolWidth: 16,
6307 symbolPadding: 5,
6308 verticalAlign: 'bottom',
6309 // width: undefined,
6310 x: 0,
6311 y: 0,
6312 title: {
6313 //text: null,
6314
6315 style: {
6316 fontWeight: 'bold'
6317 }
6318
6319 }
6320 },
6321
6322 loading: {
6323 // hideDuration: 100,
6324 // showDuration: 0,
6325
6326 labelStyle: {
6327 fontWeight: 'bold',
6328 position: 'relative',
6329 top: '45%'
6330 },
6331 style: {
6332 position: 'absolute',
6333 backgroundColor: '#ffffff',
6334 opacity: 0.5,
6335 textAlign: 'center'
6336 }
6337
6338 },
6339
6340 tooltip: {
6341 enabled: true,
6342 animation: svg,
6343 //crosshairs: null,
6344 borderRadius: 3,
6345 dateTimeLabelFormats: {
6346 millisecond: '%A, %b %e, %H:%M:%S.%L',
6347 second: '%A, %b %e, %H:%M:%S',
6348 minute: '%A, %b %e, %H:%M',
6349 hour: '%A, %b %e, %H:%M',
6350 day: '%A, %b %e, %Y',
6351 week: 'Week from %A, %b %e, %Y',
6352 month: '%B %Y',
6353 year: '%Y'
6354 },
6355 footerFormat: '',
6356 //formatter: defaultFormatter,
6357 /* todo: em font-size when finished comparing against HC4
6358 headerFormat: '<span style="font-size: 0.85em">{point.key}</span><br/>',
6359 */
6360 padding: 8,
6361
6362 //shape: 'callout',
6363 //shared: false,
6364 snap: isTouchDevice ? 25 : 10,
6365
6366 backgroundColor: color('#f7f7f7').setOpacity(0.85).get(),
6367 borderWidth: 1,
6368 headerFormat: '<span style="font-size: 10px">{point.key}</span><br/>',
6369 pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b><br/>',
6370 shadow: true,
6371 style: {
6372 color: '#333333',
6373 cursor: 'default',
6374 fontSize: '12px',
6375 pointerEvents: 'none', // #1686 http://caniuse.com/#feat=pointer-events
6376 whiteSpace: 'nowrap'
6377 }
6378
6379 //xDateFormat: '%A, %b %e, %Y',
6380 //valueDecimals: null,
6381 //valuePrefix: '',
6382 //valueSuffix: ''
6383 },
6384
6385 credits: {
6386 enabled: true,
6387 href: 'http://www.highcharts.com',
6388 position: {
6389 align: 'right',
6390 x: -10,
6391 verticalAlign: 'bottom',
6392 y: -5
6393 },
6394
6395 style: {
6396 cursor: 'pointer',
6397 color: '#999999',
6398 fontSize: '9px'
6399 },
6400
6401 text: 'Highcharts.com'
6402 }
6403 };
6404
6405
6406
6407 /**
6408 * Set the time methods globally based on the useUTC option. Time method can be either
6409 * local time or UTC (default).
6410 */
6411 function setTimeMethods() {
6412 var globalOptions = H.defaultOptions.global,
6413 Date,
6414 useUTC = globalOptions.useUTC,
6415 GET = useUTC ? 'getUTC' : 'get',
6416 SET = useUTC ? 'setUTC' : 'set';
6417
6418 H.Date = Date = globalOptions.Date || win.Date; // Allow using a different Date class
6419 Date.hcTimezoneOffset = useUTC && globalOptions.timezoneOffset;
6420 Date.hcGetTimezoneOffset = useUTC && globalOptions.getTimezoneOffset;
6421 Date.hcMakeTime = function(year, month, date, hours, minutes, seconds) {
6422 var d;
6423 if (useUTC) {
6424 d = Date.UTC.apply(0, arguments);
6425 d += getTZOffset(d);
6426 } else {
6427 d = new Date(
6428 year,
6429 month,
6430 pick(date, 1),
6431 pick(hours, 0),
6432 pick(minutes, 0),
6433 pick(seconds, 0)
6434 ).getTime();
6435 }
6436 return d;
6437 };
6438 each(['Minutes', 'Hours', 'Day', 'Date', 'Month', 'FullYear'], function(s) {
6439 Date['hcGet' + s] = GET + s;
6440 });
6441 each(['Milliseconds', 'Seconds', 'Minutes', 'Hours', 'Date', 'Month', 'FullYear'], function(s) {
6442 Date['hcSet' + s] = SET + s;
6443 });
6444 }
6445
6446 /**
6447 * Merge the default options with custom options and return the new options structure
6448 * @param {Object} options The new custom options
6449 */
6450 H.setOptions = function(options) {
6451
6452 // Copy in the default options
6453 H.defaultOptions = merge(true, H.defaultOptions, options);
6454
6455 // Apply UTC
6456 setTimeMethods();
6457
6458 return H.defaultOptions;
6459 };
6460
6461 /**
6462 * Get the updated default options. Until 3.0.7, merely exposing defaultOptions for outside modules
6463 * wasn't enough because the setOptions method created a new object.
6464 */
6465 H.getOptions = function() {
6466 return H.defaultOptions;
6467 };
6468
6469
6470 // Series defaults
6471 H.defaultPlotOptions = H.defaultOptions.plotOptions;
6472
6473 // set the default time methods
6474 setTimeMethods();
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 arrayMax = H.arrayMax,
6485 arrayMin = H.arrayMin,
6486 defined = H.defined,
6487 destroyObjectProperties = H.destroyObjectProperties,
6488 each = H.each,
6489 erase = H.erase,
6490 merge = H.merge,
6491 pick = H.pick;
6492 /*
6493 * The object wrapper for plot lines and plot bands
6494 * @param {Object} options
6495 */
6496 H.PlotLineOrBand = function(axis, options) {
6497 this.axis = axis;
6498
6499 if (options) {
6500 this.options = options;
6501 this.id = options.id;
6502 }
6503 };
6504
6505 H.PlotLineOrBand.prototype = {
6506
6507 /**
6508 * Render the plot line or plot band. If it is already existing,
6509 * move it.
6510 */
6511 render: function() {
6512 var plotLine = this,
6513 axis = plotLine.axis,
6514 horiz = axis.horiz,
6515 options = plotLine.options,
6516 optionsLabel = options.label,
6517 label = plotLine.label,
6518 to = options.to,
6519 from = options.from,
6520 value = options.value,
6521 isBand = defined(from) && defined(to),
6522 isLine = defined(value),
6523 svgElem = plotLine.svgElem,
6524 isNew = !svgElem,
6525 path = [],
6526 addEvent,
6527 eventType,
6528 color = options.color,
6529 zIndex = pick(options.zIndex, 0),
6530 events = options.events,
6531 attribs = {
6532 'class': 'highcharts-plot-' + (isBand ? 'band ' : 'line ') + (options.className || '')
6533 },
6534 groupAttribs = {},
6535 renderer = axis.chart.renderer,
6536 groupName = isBand ? 'bands' : 'lines',
6537 group,
6538 log2lin = axis.log2lin;
6539
6540 // logarithmic conversion
6541 if (axis.isLog) {
6542 from = log2lin(from);
6543 to = log2lin(to);
6544 value = log2lin(value);
6545 }
6546
6547
6548 // Set the presentational attributes
6549 if (isLine) {
6550 attribs = {
6551 stroke: color,
6552 'stroke-width': options.width
6553 };
6554 if (options.dashStyle) {
6555 attribs.dashstyle = options.dashStyle;
6556 }
6557
6558 } else if (isBand) { // plot band
6559 if (color) {
6560 attribs.fill = color;
6561 }
6562 if (options.borderWidth) {
6563 attribs.stroke = options.borderColor;
6564 attribs['stroke-width'] = options.borderWidth;
6565 }
6566 }
6567
6568
6569 // Grouping and zIndex
6570 groupAttribs.zIndex = zIndex;
6571 groupName += '-' + zIndex;
6572
6573 group = axis[groupName];
6574 if (!group) {
6575 axis[groupName] = group = renderer.g('plot-' + groupName)
6576 .attr(groupAttribs).add();
6577 }
6578
6579 // Create the path
6580 if (isNew) {
6581 plotLine.svgElem = svgElem =
6582 renderer
6583 .path()
6584 .attr(attribs).add(group);
6585 }
6586
6587
6588 // Set the path or return
6589 if (isLine) {
6590 path = axis.getPlotLinePath(value, svgElem.strokeWidth());
6591 } else if (isBand) { // plot band
6592 path = axis.getPlotBandPath(from, to, options);
6593 } else {
6594 return;
6595 }
6596
6597 // common for lines and bands
6598 if (isNew && path && path.length) {
6599 svgElem.attr({
6600 d: path
6601 });
6602
6603 // events
6604 if (events) {
6605 addEvent = function(eventType) {
6606 svgElem.on(eventType, function(e) {
6607 events[eventType].apply(plotLine, [e]);
6608 });
6609 };
6610 for (eventType in events) {
6611 addEvent(eventType);
6612 }
6613 }
6614 } else if (svgElem) {
6615 if (path) {
6616 svgElem.show();
6617 svgElem.animate({
6618 d: path
6619 });
6620 } else {
6621 svgElem.hide();
6622 if (label) {
6623 plotLine.label = label = label.destroy();
6624 }
6625 }
6626 }
6627
6628 // the plot band/line label
6629 if (optionsLabel && defined(optionsLabel.text) && path && path.length &&
6630 axis.width > 0 && axis.height > 0 && !path.flat) {
6631 // apply defaults
6632 optionsLabel = merge({
6633 align: horiz && isBand && 'center',
6634 x: horiz ? !isBand && 4 : 10,
6635 verticalAlign: !horiz && isBand && 'middle',
6636 y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4,
6637 rotation: horiz && !isBand && 90
6638 }, optionsLabel);
6639
6640 this.renderLabel(optionsLabel, path, isBand, zIndex);
6641
6642 } else if (label) { // move out of sight
6643 label.hide();
6644 }
6645
6646 // chainable
6647 return plotLine;
6648 },
6649
6650 /**
6651 * Render and align label for plot line or band.
6652 */
6653 renderLabel: function(optionsLabel, path, isBand, zIndex) {
6654 var plotLine = this,
6655 label = plotLine.label,
6656 renderer = plotLine.axis.chart.renderer,
6657 attribs,
6658 xs,
6659 ys,
6660 x,
6661 y;
6662
6663 // add the SVG element
6664 if (!label) {
6665 attribs = {
6666 align: optionsLabel.textAlign || optionsLabel.align,
6667 rotation: optionsLabel.rotation,
6668 'class': 'highcharts-plot-' + (isBand ? 'band' : 'line') + '-label ' + (optionsLabel.className || '')
6669 };
6670
6671 attribs.zIndex = zIndex;
6672
6673 plotLine.label = label = renderer.text(
6674 optionsLabel.text,
6675 0,
6676 0,
6677 optionsLabel.useHTML
6678 )
6679 .attr(attribs)
6680 .add();
6681
6682
6683 label.css(optionsLabel.style);
6684
6685 }
6686
6687 // get the bounding box and align the label
6688 // #3000 changed to better handle choice between plotband or plotline
6689 xs = [path[1], path[4], (isBand ? path[6] : path[1])];
6690 ys = [path[2], path[5], (isBand ? path[7] : path[2])];
6691 x = arrayMin(xs);
6692 y = arrayMin(ys);
6693
6694 label.align(optionsLabel, false, {
6695 x: x,
6696 y: y,
6697 width: arrayMax(xs) - x,
6698 height: arrayMax(ys) - y
6699 });
6700 label.show();
6701 },
6702
6703 /**
6704 * Remove the plot line or band
6705 */
6706 destroy: function() {
6707 // remove it from the lookup
6708 erase(this.axis.plotLinesAndBands, this);
6709
6710 delete this.axis;
6711 destroyObjectProperties(this);
6712 }
6713 };
6714
6715 /**
6716 * Object with members for extending the Axis prototype
6717 * @todo Extend directly instead of adding object to Highcharts first
6718 */
6719
6720 H.AxisPlotLineOrBandExtension = {
6721
6722 /**
6723 * Create the path for a plot band
6724 */
6725 getPlotBandPath: function(from, to) {
6726 var toPath = this.getPlotLinePath(to, null, null, true),
6727 path = this.getPlotLinePath(from, null, null, true);
6728
6729 if (path && toPath) {
6730
6731 // Flat paths don't need labels (#3836)
6732 path.flat = path.toString() === toPath.toString();
6733
6734 path.push(
6735 toPath[4],
6736 toPath[5],
6737 toPath[1],
6738 toPath[2]
6739 );
6740 } else { // outside the axis area
6741 path = null;
6742 }
6743
6744 return path;
6745 },
6746
6747 addPlotBand: function(options) {
6748 return this.addPlotBandOrLine(options, 'plotBands');
6749 },
6750
6751 addPlotLine: function(options) {
6752 return this.addPlotBandOrLine(options, 'plotLines');
6753 },
6754
6755 /**
6756 * Add a plot band or plot line after render time
6757 *
6758 * @param options {Object} The plotBand or plotLine configuration object
6759 */
6760 addPlotBandOrLine: function(options, coll) {
6761 var obj = new H.PlotLineOrBand(this, options).render(),
6762 userOptions = this.userOptions;
6763
6764 if (obj) { // #2189
6765 // Add it to the user options for exporting and Axis.update
6766 if (coll) {
6767 userOptions[coll] = userOptions[coll] || [];
6768 userOptions[coll].push(options);
6769 }
6770 this.plotLinesAndBands.push(obj);
6771 }
6772
6773 return obj;
6774 },
6775
6776 /**
6777 * Remove a plot band or plot line from the chart by id
6778 * @param {Object} id
6779 */
6780 removePlotBandOrLine: function(id) {
6781 var plotLinesAndBands = this.plotLinesAndBands,
6782 options = this.options,
6783 userOptions = this.userOptions,
6784 i = plotLinesAndBands.length;
6785 while (i--) {
6786 if (plotLinesAndBands[i].id === id) {
6787 plotLinesAndBands[i].destroy();
6788 }
6789 }
6790 each([options.plotLines || [], userOptions.plotLines || [], options.plotBands || [], userOptions.plotBands || []], function(arr) {
6791 i = arr.length;
6792 while (i--) {
6793 if (arr[i].id === id) {
6794 erase(arr, arr[i]);
6795 }
6796 }
6797 });
6798 }
6799 };
6800
6801 }(Highcharts));
6802 (function(H) {
6803 /**
6804 * (c) 2010-2016 Torstein Honsi
6805 *
6806 * License: www.highcharts.com/license
6807 */
6808 'use strict';
6809 var correctFloat = H.correctFloat,
6810 defined = H.defined,
6811 destroyObjectProperties = H.destroyObjectProperties,
6812 isNumber = H.isNumber,
6813 merge = H.merge,
6814 pick = H.pick,
6815 stop = H.stop,
6816 deg2rad = H.deg2rad;
6817
6818 /**
6819 * The Tick class
6820 */
6821 H.Tick = function(axis, pos, type, noLabel) {
6822 this.axis = axis;
6823 this.pos = pos;
6824 this.type = type || '';
6825 this.isNew = true;
6826
6827 if (!type && !noLabel) {
6828 this.addLabel();
6829 }
6830 };
6831
6832 H.Tick.prototype = {
6833 /**
6834 * Write the tick label
6835 */
6836 addLabel: function() {
6837 var tick = this,
6838 axis = tick.axis,
6839 options = axis.options,
6840 chart = axis.chart,
6841 categories = axis.categories,
6842 names = axis.names,
6843 pos = tick.pos,
6844 labelOptions = options.labels,
6845 str,
6846 tickPositions = axis.tickPositions,
6847 isFirst = pos === tickPositions[0],
6848 isLast = pos === tickPositions[tickPositions.length - 1],
6849 value = categories ?
6850 pick(categories[pos], names[pos], pos) :
6851 pos,
6852 label = tick.label,
6853 tickPositionInfo = tickPositions.info,
6854 dateTimeLabelFormat;
6855
6856 // Set the datetime label format. If a higher rank is set for this position, use that. If not,
6857 // use the general format.
6858 if (axis.isDatetimeAxis && tickPositionInfo) {
6859 dateTimeLabelFormat = options.dateTimeLabelFormats[tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName];
6860 }
6861 // set properties for access in render method
6862 tick.isFirst = isFirst;
6863 tick.isLast = isLast;
6864
6865 // get the string
6866 str = axis.labelFormatter.call({
6867 axis: axis,
6868 chart: chart,
6869 isFirst: isFirst,
6870 isLast: isLast,
6871 dateTimeLabelFormat: dateTimeLabelFormat,
6872 value: axis.isLog ? correctFloat(axis.lin2log(value)) : value
6873 });
6874
6875 // prepare CSS
6876 //css = width && { width: Math.max(1, Math.round(width - 2 * (labelOptions.padding || 10))) + 'px' };
6877
6878 // first call
6879 if (!defined(label)) {
6880
6881 tick.label = label =
6882 defined(str) && labelOptions.enabled ?
6883 chart.renderer.text(
6884 str,
6885 0,
6886 0,
6887 labelOptions.useHTML
6888 )
6889
6890 // without position absolute, IE export sometimes is wrong
6891 .css(merge(labelOptions.style))
6892
6893 .add(axis.labelGroup):
6894 null;
6895 tick.labelLength = label && label.getBBox().width; // Un-rotated length
6896 tick.rotation = 0; // Base value to detect change for new calls to getBBox
6897
6898 // update
6899 } else if (label) {
6900 label.attr({
6901 text: str
6902 });
6903 }
6904 },
6905
6906 /**
6907 * Get the offset height or width of the label
6908 */
6909 getLabelSize: function() {
6910 return this.label ?
6911 this.label.getBBox()[this.axis.horiz ? 'height' : 'width'] :
6912 0;
6913 },
6914
6915 /**
6916 * Handle the label overflow by adjusting the labels to the left and right edge, or
6917 * hide them if they collide into the neighbour label.
6918 */
6919 handleOverflow: function(xy) {
6920 var axis = this.axis,
6921 pxPos = xy.x,
6922 chartWidth = axis.chart.chartWidth,
6923 spacing = axis.chart.spacing,
6924 leftBound = pick(axis.labelLeft, Math.min(axis.pos, spacing[3])),
6925 rightBound = pick(axis.labelRight, Math.max(axis.pos + axis.len, chartWidth - spacing[1])),
6926 label = this.label,
6927 rotation = this.rotation,
6928 factor = {
6929 left: 0,
6930 center: 0.5,
6931 right: 1
6932 }[axis.labelAlign],
6933 labelWidth = label.getBBox().width,
6934 slotWidth = axis.getSlotWidth(),
6935 modifiedSlotWidth = slotWidth,
6936 xCorrection = factor,
6937 goRight = 1,
6938 leftPos,
6939 rightPos,
6940 textWidth,
6941 css = {};
6942
6943 // Check if the label overshoots the chart spacing box. If it does, move it.
6944 // If it now overshoots the slotWidth, add ellipsis.
6945 if (!rotation) {
6946 leftPos = pxPos - factor * labelWidth;
6947 rightPos = pxPos + (1 - factor) * labelWidth;
6948
6949 if (leftPos < leftBound) {
6950 modifiedSlotWidth = xy.x + modifiedSlotWidth * (1 - factor) - leftBound;
6951 } else if (rightPos > rightBound) {
6952 modifiedSlotWidth = rightBound - xy.x + modifiedSlotWidth * factor;
6953 goRight = -1;
6954 }
6955
6956 modifiedSlotWidth = Math.min(slotWidth, modifiedSlotWidth); // #4177
6957 if (modifiedSlotWidth < slotWidth && axis.labelAlign === 'center') {
6958 xy.x += goRight * (slotWidth - modifiedSlotWidth - xCorrection * (slotWidth - Math.min(labelWidth, modifiedSlotWidth)));
6959 }
6960 // If the label width exceeds the available space, set a text width to be
6961 // picked up below. Also, if a width has been set before, we need to set a new
6962 // one because the reported labelWidth will be limited by the box (#3938).
6963 if (labelWidth > modifiedSlotWidth || (axis.autoRotation && (label.styles || {}).width)) {
6964 textWidth = modifiedSlotWidth;
6965 }
6966
6967 // Add ellipsis to prevent rotated labels to be clipped against the edge of the chart
6968 } else if (rotation < 0 && pxPos - factor * labelWidth < leftBound) {
6969 textWidth = Math.round(pxPos / Math.cos(rotation * deg2rad) - leftBound);
6970 } else if (rotation > 0 && pxPos + factor * labelWidth > rightBound) {
6971 textWidth = Math.round((chartWidth - pxPos) / Math.cos(rotation * deg2rad));
6972 }
6973
6974 if (textWidth) {
6975 css.width = textWidth;
6976 if (!(axis.options.labels.style || {}).textOverflow) {
6977 css.textOverflow = 'ellipsis';
6978 }
6979 label.css(css);
6980 }
6981 },
6982
6983 /**
6984 * Get the x and y position for ticks and labels
6985 */
6986 getPosition: function(horiz, pos, tickmarkOffset, old) {
6987 var axis = this.axis,
6988 chart = axis.chart,
6989 cHeight = (old && chart.oldChartHeight) || chart.chartHeight;
6990
6991 return {
6992 x: horiz ?
6993 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),
6994
6995 y: horiz ?
6996 cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) : cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB
6997 };
6998
6999 },
7000
7001 /**
7002 * Get the x, y position of the tick label
7003 */
7004 getLabelPosition: function(x, y, label, horiz, labelOptions, tickmarkOffset, index, step) {
7005 var axis = this.axis,
7006 transA = axis.transA,
7007 reversed = axis.reversed,
7008 staggerLines = axis.staggerLines,
7009 rotCorr = axis.tickRotCorr || {
7010 x: 0,
7011 y: 0
7012 },
7013 yOffset = labelOptions.y,
7014 line;
7015
7016 if (!defined(yOffset)) {
7017 if (axis.side === 0) {
7018 yOffset = label.rotation ? -8 : -label.getBBox().height;
7019 } else if (axis.side === 2) {
7020 yOffset = rotCorr.y + 8;
7021 } else {
7022 // #3140, #3140
7023 yOffset = Math.cos(label.rotation * deg2rad) * (rotCorr.y - label.getBBox(false, 0).height / 2);
7024 }
7025 }
7026
7027 x = x + labelOptions.x + rotCorr.x - (tickmarkOffset && horiz ?
7028 tickmarkOffset * transA * (reversed ? -1 : 1) : 0);
7029 y = y + yOffset - (tickmarkOffset && !horiz ?
7030 tickmarkOffset * transA * (reversed ? 1 : -1) : 0);
7031
7032 // Correct for staggered labels
7033 if (staggerLines) {
7034 line = (index / (step || 1) % staggerLines);
7035 if (axis.opposite) {
7036 line = staggerLines - line - 1;
7037 }
7038 y += line * (axis.labelOffset / staggerLines);
7039 }
7040
7041 return {
7042 x: x,
7043 y: Math.round(y)
7044 };
7045 },
7046
7047 /**
7048 * Extendible method to return the path of the marker
7049 */
7050 getMarkPath: function(x, y, tickLength, tickWidth, horiz, renderer) {
7051 return renderer.crispLine([
7052 'M',
7053 x,
7054 y,
7055 'L',
7056 x + (horiz ? 0 : -tickLength),
7057 y + (horiz ? tickLength : 0)
7058 ], tickWidth);
7059 },
7060
7061 /**
7062 * Put everything in place
7063 *
7064 * @param index {Number}
7065 * @param old {Boolean} Use old coordinates to prepare an animation into new position
7066 */
7067 render: function(index, old, opacity) {
7068 var tick = this,
7069 axis = tick.axis,
7070 options = axis.options,
7071 chart = axis.chart,
7072 renderer = chart.renderer,
7073 horiz = axis.horiz,
7074 type = tick.type,
7075 label = tick.label,
7076 pos = tick.pos,
7077 labelOptions = options.labels,
7078 gridLine = tick.gridLine,
7079 tickPrefix = type ? type + 'Tick' : 'tick',
7080 tickSize = axis.tickSize(tickPrefix),
7081 gridLinePath,
7082 mark = tick.mark,
7083 isNewMark = !mark,
7084 step = labelOptions.step,
7085 attribs = {},
7086 show = true,
7087 tickmarkOffset = axis.tickmarkOffset,
7088 xy = tick.getPosition(horiz, pos, tickmarkOffset, old),
7089 x = xy.x,
7090 y = xy.y,
7091 reverseCrisp = ((horiz && x === axis.pos + axis.len) || (!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687
7092
7093
7094 var gridPrefix = type ? type + 'Grid' : 'grid',
7095 gridLineWidth = options[gridPrefix + 'LineWidth'],
7096 gridLineColor = options[gridPrefix + 'LineColor'],
7097 dashStyle = options[gridPrefix + 'LineDashStyle'],
7098 tickWidth = pick(options[tickPrefix + 'Width'], !type && axis.isXAxis ? 1 : 0), // X axis defaults to 1
7099 tickColor = options[tickPrefix + 'Color'];
7100
7101
7102 opacity = pick(opacity, 1);
7103 this.isActive = true;
7104
7105 // Create the grid line
7106 if (!gridLine) {
7107
7108 attribs.stroke = gridLineColor;
7109 attribs['stroke-width'] = gridLineWidth;
7110 if (dashStyle) {
7111 attribs.dashstyle = dashStyle;
7112 }
7113
7114 if (!type) {
7115 attribs.zIndex = 1;
7116 }
7117 if (old) {
7118 attribs.opacity = 0;
7119 }
7120 tick.gridLine = gridLine = renderer.path()
7121 .attr(attribs)
7122 .addClass('highcharts-' + (type ? type + '-' : '') + 'grid-line')
7123 .add(axis.gridGroup);
7124 }
7125
7126 // If the parameter 'old' is set, the current call will be followed
7127 // by another call, therefore do not do any animations this time
7128 if (!old && gridLine) {
7129 gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLine.strokeWidth() * reverseCrisp, old, true);
7130 if (gridLinePath) {
7131 gridLine[tick.isNew ? 'attr' : 'animate']({
7132 d: gridLinePath,
7133 opacity: opacity
7134 });
7135 }
7136 }
7137
7138 // create the tick mark
7139 if (tickSize) {
7140
7141 // negate the length
7142 if (axis.opposite) {
7143 tickSize[0] = -tickSize[0];
7144 }
7145
7146 // First time, create it
7147 if (isNewMark) {
7148 tick.mark = mark = renderer.path()
7149 .addClass('highcharts-' + (type ? type + '-' : '') + 'tick')
7150 .add(axis.axisGroup);
7151
7152
7153 mark.attr({
7154 stroke: tickColor,
7155 'stroke-width': tickWidth
7156 });
7157
7158 }
7159 mark[isNewMark ? 'attr' : 'animate']({
7160 d: tick.getMarkPath(x, y, tickSize[0], mark.strokeWidth() * reverseCrisp, horiz, renderer),
7161 opacity: opacity
7162 });
7163
7164 }
7165
7166 // the label is created on init - now move it into place
7167 if (label && isNumber(x)) {
7168 label.xy = xy = tick.getLabelPosition(x, y, label, horiz, labelOptions, tickmarkOffset, index, step);
7169
7170 // Apply show first and show last. If the tick is both first and last, it is
7171 // a single centered tick, in which case we show the label anyway (#2100).
7172 if ((tick.isFirst && !tick.isLast && !pick(options.showFirstLabel, 1)) ||
7173 (tick.isLast && !tick.isFirst && !pick(options.showLastLabel, 1))) {
7174 show = false;
7175
7176 // Handle label overflow and show or hide accordingly
7177 } else if (horiz && !axis.isRadial && !labelOptions.step && !labelOptions.rotation && !old && opacity !== 0) {
7178 tick.handleOverflow(xy);
7179 }
7180
7181 // apply step
7182 if (step && index % step) {
7183 // show those indices dividable by step
7184 show = false;
7185 }
7186
7187 // Set the new position, and show or hide
7188 if (show && isNumber(xy.y)) {
7189 xy.opacity = opacity;
7190 label[tick.isNew ? 'attr' : 'animate'](xy);
7191 } else {
7192 stop(label); // #5332
7193 label.attr('y', -9999); // #1338
7194 }
7195 tick.isNew = false;
7196 }
7197 },
7198
7199 /**
7200 * Destructor for the tick prototype
7201 */
7202 destroy: function() {
7203 destroyObjectProperties(this, this.axis);
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 Date = H.Date,
9756 defaultOptions = H.defaultOptions,
9757 defined = H.defined,
9758 each = H.each,
9759 extend = H.extend,
9760 getMagnitude = H.getMagnitude,
9761 getTZOffset = H.getTZOffset,
9762 grep = H.grep,
9763 normalizeTickInterval = H.normalizeTickInterval,
9764 pick = H.pick,
9765 timeUnits = H.timeUnits;
9766 /**
9767 * Set the tick positions to a time unit that makes sense, for example
9768 * on the first of each month or on every Monday. Return an array
9769 * with the time positions. Used in datetime axes as well as for grouping
9770 * data on a datetime axis.
9771 *
9772 * @param {Object} normalizedInterval The interval in axis values (ms) and the count
9773 * @param {Number} min The minimum in axis values
9774 * @param {Number} max The maximum in axis values
9775 * @param {Number} startOfWeek
9776 */
9777 Axis.prototype.getTimeTicks = function(normalizedInterval, min, max, startOfWeek) {
9778 var tickPositions = [],
9779 i,
9780 higherRanks = {},
9781 useUTC = defaultOptions.global.useUTC,
9782 minYear, // used in months and years as a basis for Date.UTC()
9783 minDate = new Date(min - getTZOffset(min)),
9784 makeTime = Date.hcMakeTime,
9785 interval = normalizedInterval.unitRange,
9786 count = normalizedInterval.count;
9787
9788 if (defined(min)) { // #1300
9789 minDate[Date.hcSetMilliseconds](interval >= timeUnits.second ? 0 : // #3935
9790 count * Math.floor(minDate.getMilliseconds() / count)); // #3652, #3654
9791
9792 if (interval >= timeUnits.second) { // second
9793 minDate[Date.hcSetSeconds](interval >= timeUnits.minute ? 0 : // #3935
9794 count * Math.floor(minDate.getSeconds() / count));
9795 }
9796
9797 if (interval >= timeUnits.minute) { // minute
9798 minDate[Date.hcSetMinutes](interval >= timeUnits.hour ? 0 :
9799 count * Math.floor(minDate[Date.hcGetMinutes]() / count));
9800 }
9801
9802 if (interval >= timeUnits.hour) { // hour
9803 minDate[Date.hcSetHours](interval >= timeUnits.day ? 0 :
9804 count * Math.floor(minDate[Date.hcGetHours]() / count));
9805 }
9806
9807 if (interval >= timeUnits.day) { // day
9808 minDate[Date.hcSetDate](interval >= timeUnits.month ? 1 :
9809 count * Math.floor(minDate[Date.hcGetDate]() / count));
9810 }
9811
9812 if (interval >= timeUnits.month) { // month
9813 minDate[Date.hcSetMonth](interval >= timeUnits.year ? 0 :
9814 count * Math.floor(minDate[Date.hcGetMonth]() / count));
9815 minYear = minDate[Date.hcGetFullYear]();
9816 }
9817
9818 if (interval >= timeUnits.year) { // year
9819 minYear -= minYear % count;
9820 minDate[Date.hcSetFullYear](minYear);
9821 }
9822
9823 // week is a special case that runs outside the hierarchy
9824 if (interval === timeUnits.week) {
9825 // get start of current week, independent of count
9826 minDate[Date.hcSetDate](minDate[Date.hcGetDate]() - minDate[Date.hcGetDay]() +
9827 pick(startOfWeek, 1));
9828 }
9829
9830
9831 // get tick positions
9832 i = 1;
9833 if (Date.hcTimezoneOffset || Date.hcGetTimezoneOffset) {
9834 minDate = minDate.getTime();
9835 minDate = new Date(minDate + getTZOffset(minDate));
9836 }
9837 minYear = minDate[Date.hcGetFullYear]();
9838 var time = minDate.getTime(),
9839 minMonth = minDate[Date.hcGetMonth](),
9840 minDateDate = minDate[Date.hcGetDate](),
9841 variableDayLength = !useUTC || !!Date.hcGetTimezoneOffset, // #4951
9842 localTimezoneOffset = (timeUnits.day +
9843 (useUTC ? getTZOffset(minDate) : minDate.getTimezoneOffset() * 60 * 1000)
9844 ) % timeUnits.day; // #950, #3359
9845
9846 // iterate and add tick positions at appropriate values
9847 while (time < max) {
9848 tickPositions.push(time);
9849
9850 // if the interval is years, use Date.UTC to increase years
9851 if (interval === timeUnits.year) {
9852 time = makeTime(minYear + i * count, 0);
9853
9854 // if the interval is months, use Date.UTC to increase months
9855 } else if (interval === timeUnits.month) {
9856 time = makeTime(minYear, minMonth + i * count);
9857
9858 // if we're using global time, the interval is not fixed as it jumps
9859 // one hour at the DST crossover
9860 } else if (variableDayLength && (interval === timeUnits.day || interval === timeUnits.week)) {
9861 time = makeTime(minYear, minMonth, minDateDate +
9862 i * count * (interval === timeUnits.day ? 1 : 7));
9863
9864 // else, the interval is fixed and we use simple addition
9865 } else {
9866 time += interval * count;
9867 }
9868
9869 i++;
9870 }
9871
9872 // push the last time
9873 tickPositions.push(time);
9874
9875
9876 // mark new days if the time is dividible by day (#1649, #1760)
9877 each(grep(tickPositions, function(time) {
9878 return interval <= timeUnits.hour && time % timeUnits.day === localTimezoneOffset;
9879 }), function(time) {
9880 higherRanks[time] = 'day';
9881 });
9882 }
9883
9884
9885 // record information on the chosen unit - for dynamic label formatter
9886 tickPositions.info = extend(normalizedInterval, {
9887 higherRanks: higherRanks,
9888 totalRange: interval * count
9889 });
9890
9891 return tickPositions;
9892 };
9893
9894 /**
9895 * Get a normalized tick interval for dates. Returns a configuration object with
9896 * unit range (interval), count and name. Used to prepare data for getTimeTicks.
9897 * Previously this logic was part of getTimeTicks, but as getTimeTicks now runs
9898 * of segments in stock charts, the normalizing logic was extracted in order to
9899 * prevent it for running over again for each segment having the same interval.
9900 * #662, #697.
9901 */
9902 Axis.prototype.normalizeTimeTickInterval = function(tickInterval, unitsOption) {
9903 var units = unitsOption || [
9904 [
9905 'millisecond', // unit name
9906 [1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples
9907 ],
9908 [
9909 'second', [1, 2, 5, 10, 15, 30]
9910 ],
9911 [
9912 'minute', [1, 2, 5, 10, 15, 30]
9913 ],
9914 [
9915 'hour', [1, 2, 3, 4, 6, 8, 12]
9916 ],
9917 [
9918 'day', [1, 2]
9919 ],
9920 [
9921 'week', [1, 2]
9922 ],
9923 [
9924 'month', [1, 2, 3, 4, 6]
9925 ],
9926 [
9927 'year',
9928 null
9929 ]
9930 ],
9931 unit = units[units.length - 1], // default unit is years
9932 interval = timeUnits[unit[0]],
9933 multiples = unit[1],
9934 count,
9935 i;
9936
9937 // loop through the units to find the one that best fits the tickInterval
9938 for (i = 0; i < units.length; i++) {
9939 unit = units[i];
9940 interval = timeUnits[unit[0]];
9941 multiples = unit[1];
9942
9943
9944 if (units[i + 1]) {
9945 // lessThan is in the middle between the highest multiple and the next unit.
9946 var lessThan = (interval * multiples[multiples.length - 1] +
9947 timeUnits[units[i + 1][0]]) / 2;
9948
9949 // break and keep the current unit
9950 if (tickInterval <= lessThan) {
9951 break;
9952 }
9953 }
9954 }
9955
9956 // prevent 2.5 years intervals, though 25, 250 etc. are allowed
9957 if (interval === timeUnits.year && tickInterval < 5 * interval) {
9958 multiples = [1, 2, 5];
9959 }
9960
9961 // get the count
9962 count = normalizeTickInterval(
9963 tickInterval / interval,
9964 multiples,
9965 unit[0] === 'year' ? Math.max(getMagnitude(tickInterval / interval), 1) : 1 // #1913, #2360
9966 );
9967
9968 return {
9969 unitRange: interval,
9970 count: count,
9971 unitName: unit[0]
9972 };
9973 };
9974
9975 }(Highcharts));
9976 (function(H) {
9977 /**
9978 * (c) 2010-2016 Torstein Honsi
9979 *
9980 * License: www.highcharts.com/license
9981 */
9982 'use strict';
9983 var Axis = H.Axis,
9984 getMagnitude = H.getMagnitude,
9985 map = H.map,
9986 normalizeTickInterval = H.normalizeTickInterval,
9987 pick = H.pick;
9988 /**
9989 * Methods defined on the Axis prototype
9990 */
9991
9992 /**
9993 * Set the tick positions of a logarithmic axis
9994 */
9995 Axis.prototype.getLogTickPositions = function(interval, min, max, minor) {
9996 var axis = this,
9997 options = axis.options,
9998 axisLength = axis.len,
9999 lin2log = axis.lin2log,
10000 log2lin = axis.log2lin,
10001 // Since we use this method for both major and minor ticks,
10002 // use a local variable and return the result
10003 positions = [];
10004
10005 // Reset
10006 if (!minor) {
10007 axis._minorAutoInterval = null;
10008 }
10009
10010 // First case: All ticks fall on whole logarithms: 1, 10, 100 etc.
10011 if (interval >= 0.5) {
10012 interval = Math.round(interval);
10013 positions = axis.getLinearTickPositions(interval, min, max);
10014
10015 // Second case: We need intermediary ticks. For example
10016 // 1, 2, 4, 6, 8, 10, 20, 40 etc.
10017 } else if (interval >= 0.08) {
10018 var roundedMin = Math.floor(min),
10019 intermediate,
10020 i,
10021 j,
10022 len,
10023 pos,
10024 lastPos,
10025 break2;
10026
10027 if (interval > 0.3) {
10028 intermediate = [1, 2, 4];
10029 } else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc
10030 intermediate = [1, 2, 4, 6, 8];
10031 } else { // 0.1 equals ten minor ticks per 1, 10, 100 etc
10032 intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
10033 }
10034
10035 for (i = roundedMin; i < max + 1 && !break2; i++) {
10036 len = intermediate.length;
10037 for (j = 0; j < len && !break2; j++) {
10038 pos = log2lin(lin2log(i) * intermediate[j]);
10039 if (pos > min && (!minor || lastPos <= max) && lastPos !== undefined) { // #1670, lastPos is #3113
10040 positions.push(lastPos);
10041 }
10042
10043 if (lastPos > max) {
10044 break2 = true;
10045 }
10046 lastPos = pos;
10047 }
10048 }
10049
10050 // Third case: We are so deep in between whole logarithmic values that
10051 // we might as well handle the tick positions like a linear axis. For
10052 // example 1.01, 1.02, 1.03, 1.04.
10053 } else {
10054 var realMin = lin2log(min),
10055 realMax = lin2log(max),
10056 tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'],
10057 filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption,
10058 tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1),
10059 totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength;
10060
10061 interval = pick(
10062 filteredTickIntervalOption,
10063 axis._minorAutoInterval,
10064 (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1)
10065 );
10066
10067 interval = normalizeTickInterval(
10068 interval,
10069 null,
10070 getMagnitude(interval)
10071 );
10072
10073 positions = map(axis.getLinearTickPositions(
10074 interval,
10075 realMin,
10076 realMax
10077 ), log2lin);
10078
10079 if (!minor) {
10080 axis._minorAutoInterval = interval / 5;
10081 }
10082 }
10083
10084 // Set the axis-level tickInterval variable
10085 if (!minor) {
10086 axis.tickInterval = interval;
10087 }
10088 return positions;
10089 };
10090
10091 Axis.prototype.log2lin = function(num) {
10092 return Math.log(num) / Math.LN10;
10093 };
10094
10095 Axis.prototype.lin2log = function(num) {
10096 return Math.pow(10, num);
10097 };
10098
10099 }(Highcharts));
10100 (function(H) {
10101 /**
10102 * (c) 2010-2016 Torstein Honsi
10103 *
10104 * License: www.highcharts.com/license
10105 */
10106 'use strict';
10107 var addEvent = H.addEvent,
10108 dateFormat = H.dateFormat,
10109 each = H.each,
10110 extend = H.extend,
10111 format = H.format,
10112 isNumber = H.isNumber,
10113 map = H.map,
10114 merge = H.merge,
10115 pick = H.pick,
10116 splat = H.splat,
10117 stop = H.stop,
10118 syncTimeout = H.syncTimeout,
10119 timeUnits = H.timeUnits;
10120 /**
10121 * The tooltip object
10122 * @param {Object} chart The chart instance
10123 * @param {Object} options Tooltip options
10124 */
10125 H.Tooltip = function() {
10126 this.init.apply(this, arguments);
10127 };
10128
10129 H.Tooltip.prototype = {
10130
10131 init: function(chart, options) {
10132
10133 // Save the chart and options
10134 this.chart = chart;
10135 this.options = options;
10136
10137 // Keep track of the current series
10138 //this.currentSeries = undefined;
10139
10140 // List of crosshairs
10141 this.crosshairs = [];
10142
10143 // Current values of x and y when animating
10144 this.now = {
10145 x: 0,
10146 y: 0
10147 };
10148
10149 // The tooltip is initially hidden
10150 this.isHidden = true;
10151
10152
10153
10154 // Public property for getting the shared state.
10155 this.split = options.split && !chart.inverted;
10156 this.shared = options.shared || this.split;
10157
10158
10159 // Create the label
10160 if (this.split) {
10161 this.label = this.chart.renderer.g('tooltip');
10162 } else {
10163 this.label = chart.renderer.label(
10164 '',
10165 0,
10166 0,
10167 options.shape || 'callout',
10168 null,
10169 null,
10170 options.useHTML,
10171 null,
10172 'tooltip'
10173 )
10174 .attr({
10175 padding: options.padding,
10176 r: options.borderRadius,
10177 display: 'none' // #2301, #2657, #3532, #5570
10178 });
10179
10180
10181 this.label
10182 .attr({
10183 'fill': options.backgroundColor,
10184 'stroke-width': options.borderWidth
10185 })
10186 // #2301, #2657
10187 .css(options.style)
10188 .shadow(options.shadow);
10189
10190 }
10191 this.label.attr({
10192 zIndex: 8
10193 })
10194 .add();
10195 },
10196
10197 update: function(options) {
10198 this.destroy();
10199 this.init(this.chart, merge(true, this.options, options));
10200 },
10201
10202 /**
10203 * Destroy the tooltip and its elements.
10204 */
10205 destroy: function() {
10206 // Destroy and clear local variables
10207 if (this.label) {
10208 this.label = this.label.destroy();
10209 }
10210 clearTimeout(this.hideTimer);
10211 clearTimeout(this.tooltipTimeout);
10212 },
10213
10214 /**
10215 * Provide a soft movement for the tooltip
10216 *
10217 * @param {Number} x
10218 * @param {Number} y
10219 * @private
10220 */
10221 move: function(x, y, anchorX, anchorY) {
10222 var tooltip = this,
10223 now = tooltip.now,
10224 animate = tooltip.options.animation !== false && !tooltip.isHidden &&
10225 // When we get close to the target position, abort animation and land on the right place (#3056)
10226 (Math.abs(x - now.x) > 1 || Math.abs(y - now.y) > 1),
10227 skipAnchor = tooltip.followPointer || tooltip.len > 1;
10228
10229 // Get intermediate values for animation
10230 extend(now, {
10231 x: animate ? (2 * now.x + x) / 3 : x,
10232 y: animate ? (now.y + y) / 2 : y,
10233 anchorX: skipAnchor ? undefined : animate ? (2 * now.anchorX + anchorX) / 3 : anchorX,
10234 anchorY: skipAnchor ? undefined : animate ? (now.anchorY + anchorY) / 2 : anchorY
10235 });
10236
10237 // Move to the intermediate value
10238 tooltip.label.attr(now);
10239
10240
10241 // Run on next tick of the mouse tracker
10242 if (animate) {
10243
10244 // Never allow two timeouts
10245 clearTimeout(this.tooltipTimeout);
10246
10247 // Set the fixed interval ticking for the smooth tooltip
10248 this.tooltipTimeout = setTimeout(function() {
10249 // The interval function may still be running during destroy, so check that the chart is really there before calling.
10250 if (tooltip) {
10251 tooltip.move(x, y, anchorX, anchorY);
10252 }
10253 }, 32);
10254
10255 }
10256 },
10257
10258 /**
10259 * Hide the tooltip
10260 */
10261 hide: function(delay) {
10262 var tooltip = this;
10263 clearTimeout(this.hideTimer); // disallow duplicate timers (#1728, #1766)
10264 delay = pick(delay, this.options.hideDelay, 500);
10265 if (!this.isHidden) {
10266 this.hideTimer = syncTimeout(function() {
10267 tooltip.label[delay ? 'fadeOut' : 'hide']();
10268 tooltip.isHidden = true;
10269 }, delay);
10270 }
10271 },
10272
10273 /**
10274 * Extendable method to get the anchor position of the tooltip
10275 * from a point or set of points
10276 */
10277 getAnchor: function(points, mouseEvent) {
10278 var ret,
10279 chart = this.chart,
10280 inverted = chart.inverted,
10281 plotTop = chart.plotTop,
10282 plotLeft = chart.plotLeft,
10283 plotX = 0,
10284 plotY = 0,
10285 yAxis,
10286 xAxis;
10287
10288 points = splat(points);
10289
10290 // Pie uses a special tooltipPos
10291 ret = points[0].tooltipPos;
10292
10293 // When tooltip follows mouse, relate the position to the mouse
10294 if (this.followPointer && mouseEvent) {
10295 if (mouseEvent.chartX === undefined) {
10296 mouseEvent = chart.pointer.normalize(mouseEvent);
10297 }
10298 ret = [
10299 mouseEvent.chartX - chart.plotLeft,
10300 mouseEvent.chartY - plotTop
10301 ];
10302 }
10303 // When shared, use the average position
10304 if (!ret) {
10305 each(points, function(point) {
10306 yAxis = point.series.yAxis;
10307 xAxis = point.series.xAxis;
10308 plotX += point.plotX + (!inverted && xAxis ? xAxis.left - plotLeft : 0);
10309 plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) +
10310 (!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151
10311 });
10312
10313 plotX /= points.length;
10314 plotY /= points.length;
10315
10316 ret = [
10317 inverted ? chart.plotWidth - plotY : plotX,
10318 this.shared && !inverted && points.length > 1 && mouseEvent ?
10319 mouseEvent.chartY - plotTop : // place shared tooltip next to the mouse (#424)
10320 inverted ? chart.plotHeight - plotX : plotY
10321 ];
10322 }
10323
10324 return map(ret, Math.round);
10325 },
10326
10327 /**
10328 * Place the tooltip in a chart without spilling over
10329 * and not covering the point it self.
10330 */
10331 getPosition: function(boxWidth, boxHeight, point) {
10332
10333 var chart = this.chart,
10334 distance = this.distance,
10335 ret = {},
10336 h = point.h || 0, // #4117
10337 swapped,
10338 first = ['y', chart.chartHeight, boxHeight, point.plotY + chart.plotTop, chart.plotTop, chart.plotTop + chart.plotHeight],
10339 second = ['x', chart.chartWidth, boxWidth, point.plotX + chart.plotLeft, chart.plotLeft, chart.plotLeft + chart.plotWidth],
10340 // The far side is right or bottom
10341 preferFarSide = !this.followPointer && pick(point.ttBelow, !chart.inverted === !!point.negative), // #4984
10342 /**
10343 * Handle the preferred dimension. When the preferred dimension is tooltip
10344 * on top or bottom of the point, it will look for space there.
10345 */
10346 firstDimension = function(dim, outerSize, innerSize, point, min, max) {
10347 var roomLeft = innerSize < point - distance,
10348 roomRight = point + distance + innerSize < outerSize,
10349 alignedLeft = point - distance - innerSize,
10350 alignedRight = point + distance;
10351
10352 if (preferFarSide && roomRight) {
10353 ret[dim] = alignedRight;
10354 } else if (!preferFarSide && roomLeft) {
10355 ret[dim] = alignedLeft;
10356 } else if (roomLeft) {
10357 ret[dim] = Math.min(max - innerSize, alignedLeft - h < 0 ? alignedLeft : alignedLeft - h);
10358 } else if (roomRight) {
10359 ret[dim] = Math.max(min, alignedRight + h + innerSize > outerSize ? alignedRight : alignedRight + h);
10360 } else {
10361 return false;
10362 }
10363 },
10364 /**
10365 * Handle the secondary dimension. If the preferred dimension is tooltip
10366 * on top or bottom of the point, the second dimension is to align the tooltip
10367 * above the point, trying to align center but allowing left or right
10368 * align within the chart box.
10369 */
10370 secondDimension = function(dim, outerSize, innerSize, point) {
10371 var retVal;
10372
10373 // Too close to the edge, return false and swap dimensions
10374 if (point < distance || point > outerSize - distance) {
10375 retVal = false;
10376 // Align left/top
10377 } else if (point < innerSize / 2) {
10378 ret[dim] = 1;
10379 // Align right/bottom
10380 } else if (point > outerSize - innerSize / 2) {
10381 ret[dim] = outerSize - innerSize - 2;
10382 // Align center
10383 } else {
10384 ret[dim] = point - innerSize / 2;
10385 }
10386 return retVal;
10387 },
10388 /**
10389 * Swap the dimensions
10390 */
10391 swap = function(count) {
10392 var temp = first;
10393 first = second;
10394 second = temp;
10395 swapped = count;
10396 },
10397 run = function() {
10398 if (firstDimension.apply(0, first) !== false) {
10399 if (secondDimension.apply(0, second) === false && !swapped) {
10400 swap(true);
10401 run();
10402 }
10403 } else if (!swapped) {
10404 swap(true);
10405 run();
10406 } else {
10407 ret.x = ret.y = 0;
10408 }
10409 };
10410
10411 // Under these conditions, prefer the tooltip on the side of the point
10412 if (chart.inverted || this.len > 1) {
10413 swap();
10414 }
10415 run();
10416
10417 return ret;
10418
10419 },
10420
10421 /**
10422 * In case no user defined formatter is given, this will be used. Note that the context
10423 * here is an object holding point, series, x, y etc.
10424 *
10425 * @returns {String|Array<String>}
10426 */
10427 defaultFormatter: function(tooltip) {
10428 var items = this.points || splat(this),
10429 s;
10430
10431 // build the header
10432 s = [tooltip.tooltipFooterHeaderFormatter(items[0])]; //#3397: abstraction to enable formatting of footer and header
10433
10434 // build the values
10435 s = s.concat(tooltip.bodyFormatter(items));
10436
10437 // footer
10438 s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true)); //#3397: abstraction to enable formatting of footer and header
10439
10440 return s;
10441 },
10442
10443 /**
10444 * Refresh the tooltip's text and position.
10445 * @param {Object} point
10446 */
10447 refresh: function(point, mouseEvent) {
10448 var tooltip = this,
10449 chart = tooltip.chart,
10450 label = tooltip.label,
10451 options = tooltip.options,
10452 x,
10453 y,
10454 anchor,
10455 textConfig = {},
10456 text,
10457 pointConfig = [],
10458 formatter = options.formatter || tooltip.defaultFormatter,
10459 hoverPoints = chart.hoverPoints,
10460 shared = tooltip.shared,
10461 currentSeries;
10462
10463 clearTimeout(this.hideTimer);
10464
10465 // get the reference point coordinates (pie charts use tooltipPos)
10466 tooltip.followPointer = splat(point)[0].series.tooltipOptions.followPointer;
10467 anchor = tooltip.getAnchor(point, mouseEvent);
10468 x = anchor[0];
10469 y = anchor[1];
10470
10471 // shared tooltip, array is sent over
10472 if (shared && !(point.series && point.series.noSharedTooltip)) {
10473
10474 // hide previous hoverPoints and set new
10475
10476 chart.hoverPoints = point;
10477 if (hoverPoints) {
10478 each(hoverPoints, function(point) {
10479 point.setState();
10480 });
10481 }
10482
10483 each(point, function(item) {
10484 item.setState('hover');
10485
10486 pointConfig.push(item.getLabelConfig());
10487 });
10488
10489 textConfig = {
10490 x: point[0].category,
10491 y: point[0].y
10492 };
10493 textConfig.points = pointConfig;
10494 this.len = pointConfig.length;
10495 point = point[0];
10496
10497 // single point tooltip
10498 } else {
10499 textConfig = point.getLabelConfig();
10500 }
10501 text = formatter.call(textConfig, tooltip);
10502
10503 // register the current series
10504 currentSeries = point.series;
10505 this.distance = pick(currentSeries.tooltipOptions.distance, 16);
10506
10507 // update the inner HTML
10508 if (text === false) {
10509 this.hide();
10510 } else {
10511
10512 // show it
10513 if (tooltip.isHidden) {
10514 stop(label);
10515 label.attr({
10516 opacity: 1,
10517 display: 'block'
10518 }).show();
10519 }
10520
10521 // update text
10522 if (tooltip.split) {
10523 this.renderSplit(text, chart.hoverPoints);
10524 } else {
10525 label.attr({
10526 text: text.join ? text.join('') : text
10527 });
10528
10529 // Set the stroke color of the box to reflect the point
10530 label.removeClass(/highcharts-color-[\d]+/g)
10531 .addClass('highcharts-color-' + pick(point.colorIndex, currentSeries.colorIndex));
10532
10533
10534 label.attr({
10535 stroke: options.borderColor || point.color || currentSeries.color || '#666666'
10536 });
10537
10538
10539 tooltip.updatePosition({
10540 plotX: x,
10541 plotY: y,
10542 negative: point.negative,
10543 ttBelow: point.ttBelow,
10544 h: anchor[2] || 0
10545 });
10546 }
10547
10548 this.isHidden = false;
10549 }
10550 },
10551
10552 /**
10553 * Render the split tooltip. Loops over each point's text and adds
10554 * a label next to the point, then uses the distribute function to
10555 * find best non-overlapping positions.
10556 */
10557 renderSplit: function(labels, points) {
10558 var tooltip = this,
10559 boxes = [],
10560 chart = this.chart,
10561 ren = chart.renderer,
10562 rightAligned = true,
10563 options = this.options,
10564 headerHeight;
10565
10566 /**
10567 * Destroy a single-series tooltip
10568 */
10569 function destroy(tt) {
10570 tt.connector = tt.connector.destroy();
10571 tt.destroy();
10572 }
10573
10574 // Create the individual labels
10575 each(labels.slice(0, labels.length - 1), function(str, i) {
10576 var point = points[i - 1] ||
10577 // Item 0 is the header. Instead of this, we could also use the crosshair label
10578 {
10579 isHeader: true,
10580 plotX: points[0].plotX
10581 },
10582 owner = point.series || tooltip,
10583 tt = owner.tt,
10584 series = point.series || {},
10585 colorClass = 'highcharts-color-' + pick(point.colorIndex, series.colorIndex, 'none'),
10586 target,
10587 x,
10588 bBox;
10589
10590 // Store the tooltip referance on the series
10591 if (!tt) {
10592 owner.tt = tt = ren.label(null, null, null, point.isHeader && 'callout')
10593 .addClass('highcharts-tooltip-box ' + colorClass)
10594 .attr({
10595 'padding': options.padding,
10596 'r': options.borderRadius,
10597
10598 'fill': options.backgroundColor,
10599 'stroke': point.color || series.color || '#333333',
10600 'stroke-width': options.borderWidth
10601
10602 })
10603 .add(tooltip.label);
10604
10605 // Add a connector back to the point
10606 if (point.series) {
10607 tt.connector = ren.path()
10608 .addClass('highcharts-tooltip-connector ' + colorClass)
10609
10610 .attr({
10611 'stroke-width': series.options.lineWidth || 2,
10612 'stroke': point.color || series.color || '#666666'
10613 })
10614
10615 .add(tooltip.label);
10616
10617 addEvent(point.series, 'hide', function() {
10618 this.tt = destroy(this.tt);
10619 });
10620 }
10621 }
10622 tt.isActive = true;
10623 tt.attr({
10624 text: str
10625 });
10626
10627 // Get X position now, so we can move all to the other side in case of overflow
10628 bBox = tt.getBBox();
10629 if (point.isHeader) {
10630 headerHeight = bBox.height;
10631 x = point.plotX + chart.plotLeft - bBox.width / 2;
10632 } else {
10633 x = point.plotX + chart.plotLeft - pick(options.distance, 16) -
10634 bBox.width;
10635 }
10636
10637
10638 // If overflow left, we don't use this x in the next loop
10639 if (x < 0) {
10640 rightAligned = false;
10641 }
10642
10643 // Prepare for distribution
10644 target = (point.series && point.series.yAxis && point.series.yAxis.pos) + (point.plotY || 0);
10645 target -= chart.plotTop;
10646 boxes.push({
10647 target: point.isHeader ? chart.plotHeight + headerHeight : target,
10648 rank: point.isHeader ? 1 : 0,
10649 size: owner.tt.getBBox().height + 1,
10650 point: point,
10651 x: x,
10652 tt: tt
10653 });
10654 });
10655
10656 // Clean previous run (for missing points)
10657 each(chart.series, function(series) {
10658 var tt = series.tt;
10659 if (tt) {
10660 if (!tt.isActive) {
10661 series.tt = destroy(tt);
10662 } else {
10663 tt.isActive = false;
10664 }
10665 }
10666 });
10667
10668 // Distribute and put in place
10669 H.distribute(boxes, chart.plotHeight + headerHeight);
10670 each(boxes, function(box) {
10671 var point = box.point,
10672 tt = box.tt,
10673 attr;
10674
10675 // Put the label in place
10676 attr = {
10677 display: box.pos === undefined ? 'none' : '',
10678 x: (rightAligned || point.isHeader ? box.x : point.plotX + chart.plotLeft + pick(options.distance, 16)),
10679 y: box.pos + chart.plotTop
10680 };
10681 if (point.isHeader) {
10682 attr.anchorX = point.plotX + chart.plotLeft;
10683 attr.anchorY = attr.y - 100;
10684 }
10685 tt.attr(attr);
10686
10687 // Draw the connector to the point
10688 if (!point.isHeader) {
10689 tt.connector.attr({
10690 d: [
10691 'M',
10692 point.plotX + chart.plotLeft,
10693 point.plotY + point.series.yAxis.pos,
10694 'L',
10695 rightAligned ?
10696 point.plotX + chart.plotLeft - pick(options.distance, 16) :
10697 point.plotX + chart.plotLeft + pick(options.distance, 16),
10698 box.pos + chart.plotTop + tt.getBBox().height / 2
10699 ]
10700 });
10701 }
10702 });
10703 },
10704
10705 /**
10706 * Find the new position and perform the move
10707 */
10708 updatePosition: function(point) {
10709 var chart = this.chart,
10710 label = this.label,
10711 pos = (this.options.positioner || this.getPosition).call(
10712 this,
10713 label.width,
10714 label.height,
10715 point
10716 );
10717
10718 // do the move
10719 this.move(
10720 Math.round(pos.x),
10721 Math.round(pos.y || 0), // can be undefined (#3977)
10722 point.plotX + chart.plotLeft,
10723 point.plotY + chart.plotTop
10724 );
10725 },
10726
10727 /**
10728 * Get the best X date format based on the closest point range on the axis.
10729 */
10730 getXDateFormat: function(point, options, xAxis) {
10731 var xDateFormat,
10732 dateTimeLabelFormats = options.dateTimeLabelFormats,
10733 closestPointRange = xAxis && xAxis.closestPointRange,
10734 n,
10735 blank = '01-01 00:00:00.000',
10736 strpos = {
10737 millisecond: 15,
10738 second: 12,
10739 minute: 9,
10740 hour: 6,
10741 day: 3
10742 },
10743 date,
10744 lastN = 'millisecond'; // for sub-millisecond data, #4223
10745
10746 if (closestPointRange) {
10747 date = dateFormat('%m-%d %H:%M:%S.%L', point.x);
10748 for (n in timeUnits) {
10749
10750 // If the range is exactly one week and we're looking at a Sunday/Monday, go for the week format
10751 if (closestPointRange === timeUnits.week && +dateFormat('%w', point.x) === xAxis.options.startOfWeek &&
10752 date.substr(6) === blank.substr(6)) {
10753 n = 'week';
10754 break;
10755 }
10756
10757 // The first format that is too great for the range
10758 if (timeUnits[n] > closestPointRange) {
10759 n = lastN;
10760 break;
10761 }
10762
10763 // If the point is placed every day at 23:59, we need to show
10764 // the minutes as well. #2637.
10765 if (strpos[n] && date.substr(strpos[n]) !== blank.substr(strpos[n])) {
10766 break;
10767 }
10768
10769 // Weeks are outside the hierarchy, only apply them on Mondays/Sundays like in the first condition
10770 if (n !== 'week') {
10771 lastN = n;
10772 }
10773 }
10774
10775 if (n) {
10776 xDateFormat = dateTimeLabelFormats[n];
10777 }
10778 } else {
10779 xDateFormat = dateTimeLabelFormats.day;
10780 }
10781
10782 return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581
10783 },
10784
10785 /**
10786 * Format the footer/header of the tooltip
10787 * #3397: abstraction to enable formatting of footer and header
10788 */
10789 tooltipFooterHeaderFormatter: function(labelConfig, isFooter) {
10790 var footOrHead = isFooter ? 'footer' : 'header',
10791 series = labelConfig.series,
10792 tooltipOptions = series.tooltipOptions,
10793 xDateFormat = tooltipOptions.xDateFormat,
10794 xAxis = series.xAxis,
10795 isDateTime = xAxis && xAxis.options.type === 'datetime' && isNumber(labelConfig.key),
10796 formatString = tooltipOptions[footOrHead + 'Format'];
10797
10798 // Guess the best date format based on the closest point distance (#568, #3418)
10799 if (isDateTime && !xDateFormat) {
10800 xDateFormat = this.getXDateFormat(labelConfig, tooltipOptions, xAxis);
10801 }
10802
10803 // Insert the footer date format if any
10804 if (isDateTime && xDateFormat) {
10805 formatString = formatString.replace('{point.key}', '{point.key:' + xDateFormat + '}');
10806 }
10807
10808 return format(formatString, {
10809 point: labelConfig,
10810 series: series
10811 });
10812 },
10813
10814 /**
10815 * Build the body (lines) of the tooltip by iterating over the items and returning one entry for each item,
10816 * abstracting this functionality allows to easily overwrite and extend it.
10817 */
10818 bodyFormatter: function(items) {
10819 return map(items, function(item) {
10820 var tooltipOptions = item.series.tooltipOptions;
10821 return (tooltipOptions.pointFormatter || item.point.tooltipFormatter).call(item.point, tooltipOptions.pointFormat);
10822 });
10823 }
10824
10825 };
10826
10827 }(Highcharts));
10828 (function(H) {
10829 /**
10830 * (c) 2010-2016 Torstein Honsi
10831 *
10832 * License: www.highcharts.com/license
10833 */
10834 'use strict';
10835 var addEvent = H.addEvent,
10836 attr = H.attr,
10837 charts = H.charts,
10838 color = H.color,
10839 css = H.css,
10840 defined = H.defined,
10841 doc = H.doc,
10842 each = H.each,
10843 extend = H.extend,
10844 fireEvent = H.fireEvent,
10845 offset = H.offset,
10846 pick = H.pick,
10847 removeEvent = H.removeEvent,
10848 splat = H.splat,
10849 Tooltip = H.Tooltip,
10850 win = H.win;
10851
10852 // Global flag for touch support
10853 H.hasTouch = doc && doc.documentElement.ontouchstart !== undefined;
10854
10855 /**
10856 * The mouse tracker object. All methods starting with "on" are primary DOM event handlers.
10857 * Subsequent methods should be named differently from what they are doing.
10858 * @param {Object} chart The Chart instance
10859 * @param {Object} options The root options object
10860 */
10861 H.Pointer = function(chart, options) {
10862 this.init(chart, options);
10863 };
10864
10865 H.Pointer.prototype = {
10866 /**
10867 * Initialize Pointer
10868 */
10869 init: function(chart, options) {
10870
10871 // Store references
10872 this.options = options;
10873 this.chart = chart;
10874
10875 // Do we need to handle click on a touch device?
10876 this.runChartClick = options.chart.events && !!options.chart.events.click;
10877
10878 this.pinchDown = [];
10879 this.lastValidTouch = {};
10880
10881 if (Tooltip && options.tooltip.enabled) {
10882 chart.tooltip = new Tooltip(chart, options.tooltip);
10883 this.followTouchMove = pick(options.tooltip.followTouchMove, true);
10884 }
10885
10886 this.setDOMEvents();
10887 },
10888
10889 /**
10890 * Resolve the zoomType option
10891 */
10892 zoomOption: function() {
10893 var chart = this.chart,
10894 zoomType = chart.options.chart.zoomType,
10895 zoomX = /x/.test(zoomType),
10896 zoomY = /y/.test(zoomType),
10897 inverted = chart.inverted;
10898
10899 this.zoomX = zoomX;
10900 this.zoomY = zoomY;
10901 this.zoomHor = (zoomX && !inverted) || (zoomY && inverted);
10902 this.zoomVert = (zoomY && !inverted) || (zoomX && inverted);
10903 this.hasZoom = zoomX || zoomY;
10904 },
10905
10906 /**
10907 * Add crossbrowser support for chartX and chartY
10908 * @param {Object} e The event object in standard browsers
10909 */
10910 normalize: function(e, chartPosition) {
10911 var chartX,
10912 chartY,
10913 ePos;
10914
10915 // IE normalizing
10916 e = e || win.event;
10917 if (!e.target) {
10918 e.target = e.srcElement;
10919 }
10920
10921 // iOS (#2757)
10922 ePos = e.touches ? (e.touches.length ? e.touches.item(0) : e.changedTouches[0]) : e;
10923
10924 // Get mouse position
10925 if (!chartPosition) {
10926 this.chartPosition = chartPosition = offset(this.chart.container);
10927 }
10928
10929 // chartX and chartY
10930 if (ePos.pageX === undefined) { // IE < 9. #886.
10931 chartX = Math.max(e.x, e.clientX - chartPosition.left); // #2005, #2129: the second case is
10932 // for IE10 quirks mode within framesets
10933 chartY = e.y;
10934 } else {
10935 chartX = ePos.pageX - chartPosition.left;
10936 chartY = ePos.pageY - chartPosition.top;
10937 }
10938
10939 return extend(e, {
10940 chartX: Math.round(chartX),
10941 chartY: Math.round(chartY)
10942 });
10943 },
10944
10945 /**
10946 * Get the click position in terms of axis values.
10947 *
10948 * @param {Object} e A pointer event
10949 */
10950 getCoordinates: function(e) {
10951 var coordinates = {
10952 xAxis: [],
10953 yAxis: []
10954 };
10955
10956 each(this.chart.axes, function(axis) {
10957 coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({
10958 axis: axis,
10959 value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY'])
10960 });
10961 });
10962 return coordinates;
10963 },
10964
10965 /**
10966 * With line type charts with a single tracker, get the point closest to the mouse.
10967 * Run Point.onMouseOver and display tooltip for the point or points.
10968 */
10969 runPointActions: function(e) {
10970
10971 var pointer = this,
10972 chart = pointer.chart,
10973 series = chart.series,
10974 tooltip = chart.tooltip,
10975 shared = tooltip ? tooltip.shared : false,
10976 followPointer,
10977 updatePosition = true,
10978 hoverPoint = chart.hoverPoint,
10979 hoverSeries = chart.hoverSeries,
10980 i,
10981 anchor,
10982 noSharedTooltip,
10983 stickToHoverSeries,
10984 directTouch,
10985 kdpoints = [],
10986 kdpointT;
10987
10988 // For hovering over the empty parts of the plot area (hoverSeries is undefined).
10989 // If there is one series with point tracking (combo chart), don't go to nearest neighbour.
10990 if (!shared && !hoverSeries) {
10991 for (i = 0; i < series.length; i++) {
10992 if (series[i].directTouch || !series[i].options.stickyTracking) {
10993 series = [];
10994 }
10995 }
10996 }
10997
10998 // If it has a hoverPoint and that series requires direct touch (like columns, #3899), or we're on
10999 // a noSharedTooltip series among shared tooltip series (#4546), use the hoverPoint . Otherwise,
11000 // search the k-d tree.
11001 stickToHoverSeries = hoverSeries && (shared ? hoverSeries.noSharedTooltip : hoverSeries.directTouch);
11002 if (stickToHoverSeries && hoverPoint) {
11003 kdpoints = [hoverPoint];
11004
11005 // Handle shared tooltip or cases where a series is not yet hovered
11006 } else {
11007 // When we have non-shared tooltip and sticky tracking is disabled,
11008 // search for the closest point only on hovered series: #5533, #5476
11009 if (!shared && hoverSeries && !hoverSeries.options.stickyTracking) {
11010 series = [hoverSeries];
11011 }
11012 // Find nearest points on all series
11013 each(series, function(s) {
11014 // Skip hidden series
11015 noSharedTooltip = s.noSharedTooltip && shared;
11016 directTouch = !shared && s.directTouch;
11017 if (s.visible && !noSharedTooltip && !directTouch && pick(s.options.enableMouseTracking, true)) { // #3821
11018 kdpointT = s.searchPoint(e, !noSharedTooltip && s.kdDimensions === 1); // #3828
11019 if (kdpointT && kdpointT.series) { // Point.series becomes null when reset and before redraw (#5197)
11020 kdpoints.push(kdpointT);
11021 }
11022 }
11023 });
11024
11025 // Sort kdpoints by distance to mouse pointer
11026 kdpoints.sort(function(p1, p2) {
11027 var isCloserX = p1.distX - p2.distX,
11028 isCloser = p1.dist - p2.dist,
11029 isAbove = p1.series.group.zIndex > p2.series.group.zIndex ? -1 : 1;
11030 // We have two points which are not in the same place on xAxis and shared tooltip:
11031 if (isCloserX !== 0) {
11032 return isCloserX;
11033 }
11034 // Points are not exactly in the same place on x/yAxis:
11035 if (isCloser !== 0) {
11036 return isCloser;
11037 }
11038 // The same xAxis and yAxis position, sort by z-index:
11039 return isAbove;
11040 });
11041 }
11042
11043 // Remove points with different x-positions, required for shared tooltip and crosshairs (#4645):
11044 if (shared) {
11045 i = kdpoints.length;
11046 while (i--) {
11047 if (kdpoints[i].clientX !== kdpoints[0].clientX || kdpoints[i].series.noSharedTooltip) {
11048 kdpoints.splice(i, 1);
11049 }
11050 }
11051 }
11052
11053 // Refresh tooltip for kdpoint if new hover point or tooltip was hidden // #3926, #4200
11054 if (kdpoints[0] && (kdpoints[0] !== pointer.hoverPoint || (tooltip && tooltip.isHidden))) {
11055 // Draw tooltip if necessary
11056 if (shared && !kdpoints[0].series.noSharedTooltip) {
11057 // Do mouseover on all points (#3919, #3985, #4410)
11058 for (i = 0; i >= 0; i--) {
11059 kdpoints[i].onMouseOver(e, kdpoints[i] !== ((hoverSeries && hoverSeries.directTouch && hoverPoint) || kdpoints[0]));
11060 }
11061 // Make sure that the hoverPoint and hoverSeries are stored for events (e.g. click), #5622
11062 if (hoverSeries && hoverSeries.directTouch && hoverPoint && hoverPoint !== kdpoints[0]) {
11063 hoverPoint.onMouseOver(e, false);
11064 }
11065 if (kdpoints.length && tooltip) {
11066 // Keep the order of series in tooltip:
11067 tooltip.refresh(kdpoints.sort(function(p1, p2) {
11068 return p1.series.index - p2.series.index;
11069 }), e);
11070 }
11071 } else {
11072 if (tooltip) {
11073 tooltip.refresh(kdpoints[0], e);
11074 }
11075 if (!hoverSeries || !hoverSeries.directTouch) { // #4448
11076 kdpoints[0].onMouseOver(e);
11077 }
11078 }
11079 pointer.prevKDPoint = kdpoints[0];
11080 updatePosition = false;
11081 }
11082 // Update positions (regardless of kdpoint or hoverPoint)
11083 if (updatePosition) {
11084 followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer;
11085 if (tooltip && followPointer && !tooltip.isHidden) {
11086 anchor = tooltip.getAnchor([{}], e);
11087 tooltip.updatePosition({
11088 plotX: anchor[0],
11089 plotY: anchor[1]
11090 });
11091 }
11092 }
11093
11094 // Start the event listener to pick up the tooltip and crosshairs
11095 if (!pointer._onDocumentMouseMove) {
11096 pointer._onDocumentMouseMove = function(e) {
11097 if (charts[H.hoverChartIndex]) {
11098 charts[H.hoverChartIndex].pointer.onDocumentMouseMove(e);
11099 }
11100 };
11101 addEvent(doc, 'mousemove', pointer._onDocumentMouseMove);
11102 }
11103
11104 // Crosshair. For each hover point, loop over axes and draw cross if that point
11105 // belongs to the axis (#4927).
11106 each(shared ? kdpoints : [pick(hoverPoint, kdpoints[0])], function drawPointCrosshair(point) { // #5269
11107 each(chart.axes, function drawAxisCrosshair(axis) {
11108 // In case of snap = false, point is undefined, and we draw the crosshair anyway (#5066)
11109 if (!point || point.series && point.series[axis.coll] === axis) { // #5658
11110 axis.drawCrosshair(e, point);
11111 }
11112 });
11113 });
11114 },
11115
11116 /**
11117 * Reset the tracking by hiding the tooltip, the hover series state and the hover point
11118 *
11119 * @param allowMove {Boolean} Instead of destroying the tooltip altogether, allow moving it if possible
11120 */
11121 reset: function(allowMove, delay) {
11122 var pointer = this,
11123 chart = pointer.chart,
11124 hoverSeries = chart.hoverSeries,
11125 hoverPoint = chart.hoverPoint,
11126 hoverPoints = chart.hoverPoints,
11127 tooltip = chart.tooltip,
11128 tooltipPoints = tooltip && tooltip.shared ? hoverPoints : hoverPoint;
11129
11130 // Check if the points have moved outside the plot area (#1003, #4736, #5101)
11131 if (allowMove && tooltipPoints) {
11132 each(splat(tooltipPoints), function(point) {
11133 if (point.series.isCartesian && point.plotX === undefined) {
11134 allowMove = false;
11135 }
11136 });
11137 }
11138
11139 // Just move the tooltip, #349
11140 if (allowMove) {
11141 if (tooltip && tooltipPoints) {
11142 tooltip.refresh(tooltipPoints);
11143 if (hoverPoint) { // #2500
11144 hoverPoint.setState(hoverPoint.state, true);
11145 each(chart.axes, function(axis) {
11146 if (axis.crosshair) {
11147 axis.drawCrosshair(null, hoverPoint);
11148 }
11149 });
11150 }
11151 }
11152
11153 // Full reset
11154 } else {
11155
11156 if (hoverPoint) {
11157 hoverPoint.onMouseOut();
11158 }
11159
11160 if (hoverPoints) {
11161 each(hoverPoints, function(point) {
11162 point.setState();
11163 });
11164 }
11165
11166 if (hoverSeries) {
11167 hoverSeries.onMouseOut();
11168 }
11169
11170 if (tooltip) {
11171 tooltip.hide(delay);
11172 }
11173
11174 if (pointer._onDocumentMouseMove) {
11175 removeEvent(doc, 'mousemove', pointer._onDocumentMouseMove);
11176 pointer._onDocumentMouseMove = null;
11177 }
11178
11179 // Remove crosshairs
11180 each(chart.axes, function(axis) {
11181 axis.hideCrosshair();
11182 });
11183
11184 pointer.hoverX = pointer.prevKDPoint = chart.hoverPoints = chart.hoverPoint = null;
11185 }
11186 },
11187
11188 /**
11189 * Scale series groups to a certain scale and translation
11190 */
11191 scaleGroups: function(attribs, clip) {
11192
11193 var chart = this.chart,
11194 seriesAttribs;
11195
11196 // Scale each series
11197 each(chart.series, function(series) {
11198 seriesAttribs = attribs || series.getPlotBox(); // #1701
11199 if (series.xAxis && series.xAxis.zoomEnabled) {
11200 series.group.attr(seriesAttribs);
11201 if (series.markerGroup) {
11202 series.markerGroup.attr(seriesAttribs);
11203 series.markerGroup.clip(clip ? chart.clipRect : null);
11204 }
11205 if (series.dataLabelsGroup) {
11206 series.dataLabelsGroup.attr(seriesAttribs);
11207 }
11208 }
11209 });
11210
11211 // Clip
11212 chart.clipRect.attr(clip || chart.clipBox);
11213 },
11214
11215 /**
11216 * Start a drag operation
11217 */
11218 dragStart: function(e) {
11219 var chart = this.chart;
11220
11221 // Record the start position
11222 chart.mouseIsDown = e.type;
11223 chart.cancelClick = false;
11224 chart.mouseDownX = this.mouseDownX = e.chartX;
11225 chart.mouseDownY = this.mouseDownY = e.chartY;
11226 },
11227
11228 /**
11229 * Perform a drag operation in response to a mousemove event while the mouse is down
11230 */
11231 drag: function(e) {
11232
11233 var chart = this.chart,
11234 chartOptions = chart.options.chart,
11235 chartX = e.chartX,
11236 chartY = e.chartY,
11237 zoomHor = this.zoomHor,
11238 zoomVert = this.zoomVert,
11239 plotLeft = chart.plotLeft,
11240 plotTop = chart.plotTop,
11241 plotWidth = chart.plotWidth,
11242 plotHeight = chart.plotHeight,
11243 clickedInside,
11244 size,
11245 selectionMarker = this.selectionMarker,
11246 mouseDownX = this.mouseDownX,
11247 mouseDownY = this.mouseDownY,
11248 panKey = chartOptions.panKey && e[chartOptions.panKey + 'Key'];
11249
11250 // If the device supports both touch and mouse (like IE11), and we are touch-dragging
11251 // inside the plot area, don't handle the mouse event. #4339.
11252 if (selectionMarker && selectionMarker.touch) {
11253 return;
11254 }
11255
11256 // If the mouse is outside the plot area, adjust to cooordinates
11257 // inside to prevent the selection marker from going outside
11258 if (chartX < plotLeft) {
11259 chartX = plotLeft;
11260 } else if (chartX > plotLeft + plotWidth) {
11261 chartX = plotLeft + plotWidth;
11262 }
11263
11264 if (chartY < plotTop) {
11265 chartY = plotTop;
11266 } else if (chartY > plotTop + plotHeight) {
11267 chartY = plotTop + plotHeight;
11268 }
11269
11270 // determine if the mouse has moved more than 10px
11271 this.hasDragged = Math.sqrt(
11272 Math.pow(mouseDownX - chartX, 2) +
11273 Math.pow(mouseDownY - chartY, 2)
11274 );
11275
11276 if (this.hasDragged > 10) {
11277 clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop);
11278
11279 // make a selection
11280 if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside && !panKey) {
11281 if (!selectionMarker) {
11282 this.selectionMarker = selectionMarker = chart.renderer.rect(
11283 plotLeft,
11284 plotTop,
11285 zoomHor ? 1 : plotWidth,
11286 zoomVert ? 1 : plotHeight,
11287 0
11288 )
11289 .attr({
11290
11291 fill: chartOptions.selectionMarkerFill || color('#335cad').setOpacity(0.25).get(),
11292
11293 'class': 'highcharts-selection-marker',
11294 'zIndex': 7
11295 })
11296 .add();
11297 }
11298 }
11299
11300 // adjust the width of the selection marker
11301 if (selectionMarker && zoomHor) {
11302 size = chartX - mouseDownX;
11303 selectionMarker.attr({
11304 width: Math.abs(size),
11305 x: (size > 0 ? 0 : size) + mouseDownX
11306 });
11307 }
11308 // adjust the height of the selection marker
11309 if (selectionMarker && zoomVert) {
11310 size = chartY - mouseDownY;
11311 selectionMarker.attr({
11312 height: Math.abs(size),
11313 y: (size > 0 ? 0 : size) + mouseDownY
11314 });
11315 }
11316
11317 // panning
11318 if (clickedInside && !selectionMarker && chartOptions.panning) {
11319 chart.pan(e, chartOptions.panning);
11320 }
11321 }
11322 },
11323
11324 /**
11325 * On mouse up or touch end across the entire document, drop the selection.
11326 */
11327 drop: function(e) {
11328 var pointer = this,
11329 chart = this.chart,
11330 hasPinched = this.hasPinched;
11331
11332 if (this.selectionMarker) {
11333 var selectionData = {
11334 originalEvent: e, // #4890
11335 xAxis: [],
11336 yAxis: []
11337 },
11338 selectionBox = this.selectionMarker,
11339 selectionLeft = selectionBox.attr ? selectionBox.attr('x') : selectionBox.x,
11340 selectionTop = selectionBox.attr ? selectionBox.attr('y') : selectionBox.y,
11341 selectionWidth = selectionBox.attr ? selectionBox.attr('width') : selectionBox.width,
11342 selectionHeight = selectionBox.attr ? selectionBox.attr('height') : selectionBox.height,
11343 runZoom;
11344
11345 // a selection has been made
11346 if (this.hasDragged || hasPinched) {
11347
11348 // record each axis' min and max
11349 each(chart.axes, function(axis) {
11350 if (axis.zoomEnabled && defined(axis.min) && (hasPinched || pointer[{
11351 xAxis: 'zoomX',
11352 yAxis: 'zoomY'
11353 }[axis.coll]])) { // #859, #3569
11354 var horiz = axis.horiz,
11355 minPixelPadding = e.type === 'touchend' ? axis.minPixelPadding : 0, // #1207, #3075
11356 selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop) + minPixelPadding),
11357 selectionMax = axis.toValue((horiz ? selectionLeft + selectionWidth : selectionTop + selectionHeight) - minPixelPadding);
11358
11359 selectionData[axis.coll].push({
11360 axis: axis,
11361 min: Math.min(selectionMin, selectionMax), // for reversed axes
11362 max: Math.max(selectionMin, selectionMax)
11363 });
11364 runZoom = true;
11365 }
11366 });
11367 if (runZoom) {
11368 fireEvent(chart, 'selection', selectionData, function(args) {
11369 chart.zoom(extend(args, hasPinched ? {
11370 animation: false
11371 } : null));
11372 });
11373 }
11374
11375 }
11376 this.selectionMarker = this.selectionMarker.destroy();
11377
11378 // Reset scaling preview
11379 if (hasPinched) {
11380 this.scaleGroups();
11381 }
11382 }
11383
11384 // Reset all
11385 if (chart) { // it may be destroyed on mouse up - #877
11386 css(chart.container, {
11387 cursor: chart._cursor
11388 });
11389 chart.cancelClick = this.hasDragged > 10; // #370
11390 chart.mouseIsDown = this.hasDragged = this.hasPinched = false;
11391 this.pinchDown = [];
11392 }
11393 },
11394
11395 onContainerMouseDown: function(e) {
11396
11397 e = this.normalize(e);
11398
11399 this.zoomOption();
11400
11401 // issue #295, dragging not always working in Firefox
11402 if (e.preventDefault) {
11403 e.preventDefault();
11404 }
11405
11406 this.dragStart(e);
11407 },
11408
11409
11410
11411 onDocumentMouseUp: function(e) {
11412 if (charts[H.hoverChartIndex]) {
11413 charts[H.hoverChartIndex].pointer.drop(e);
11414 }
11415 },
11416
11417 /**
11418 * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea.
11419 * Issue #149 workaround. The mouseleave event does not always fire.
11420 */
11421 onDocumentMouseMove: function(e) {
11422 var chart = this.chart,
11423 chartPosition = this.chartPosition;
11424
11425 e = this.normalize(e, chartPosition);
11426
11427 // If we're outside, hide the tooltip
11428 if (chartPosition && !this.inClass(e.target, 'highcharts-tracker') &&
11429 !chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
11430 this.reset();
11431 }
11432 },
11433
11434 /**
11435 * When mouse leaves the container, hide the tooltip.
11436 */
11437 onContainerMouseLeave: function(e) {
11438 var chart = charts[H.hoverChartIndex];
11439 if (chart && (e.relatedTarget || e.toElement)) { // #4886, MS Touch end fires mouseleave but with no related target
11440 chart.pointer.reset();
11441 chart.pointer.chartPosition = null; // also reset the chart position, used in #149 fix
11442 }
11443 },
11444
11445 // The mousemove, touchmove and touchstart event handler
11446 onContainerMouseMove: function(e) {
11447
11448 var chart = this.chart;
11449
11450 if (!defined(H.hoverChartIndex) || !charts[H.hoverChartIndex] || !charts[H.hoverChartIndex].mouseIsDown) {
11451 H.hoverChartIndex = chart.index;
11452 }
11453
11454 e = this.normalize(e);
11455 e.returnValue = false; // #2251, #3224
11456
11457 if (chart.mouseIsDown === 'mousedown') {
11458 this.drag(e);
11459 }
11460
11461 // Show the tooltip and run mouse over events (#977)
11462 if ((this.inClass(e.target, 'highcharts-tracker') ||
11463 chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) && !chart.openMenu) {
11464 this.runPointActions(e);
11465 }
11466 },
11467
11468 /**
11469 * Utility to detect whether an element has, or has a parent with, a specific
11470 * class name. Used on detection of tracker objects and on deciding whether
11471 * hovering the tooltip should cause the active series to mouse out.
11472 */
11473 inClass: function(element, className) {
11474 var elemClassName;
11475 while (element) {
11476 elemClassName = attr(element, 'class');
11477 if (elemClassName) {
11478 if (elemClassName.indexOf(className) !== -1) {
11479 return true;
11480 }
11481 if (elemClassName.indexOf('highcharts-container') !== -1) {
11482 return false;
11483 }
11484 }
11485 element = element.parentNode;
11486 }
11487 },
11488
11489 onTrackerMouseOut: function(e) {
11490 var series = this.chart.hoverSeries,
11491 relatedTarget = e.relatedTarget || e.toElement;
11492
11493 if (series && relatedTarget && !series.options.stickyTracking &&
11494 !this.inClass(relatedTarget, 'highcharts-tooltip') &&
11495 !this.inClass(relatedTarget, 'highcharts-series-' + series.index)) { // #2499, #4465
11496 series.onMouseOut();
11497 }
11498 },
11499
11500 onContainerClick: function(e) {
11501 var chart = this.chart,
11502 hoverPoint = chart.hoverPoint,
11503 plotLeft = chart.plotLeft,
11504 plotTop = chart.plotTop;
11505
11506 e = this.normalize(e);
11507
11508 if (!chart.cancelClick) {
11509
11510 // On tracker click, fire the series and point events. #783, #1583
11511 if (hoverPoint && this.inClass(e.target, 'highcharts-tracker')) {
11512
11513 // the series click event
11514 fireEvent(hoverPoint.series, 'click', extend(e, {
11515 point: hoverPoint
11516 }));
11517
11518 // the point click event
11519 if (chart.hoverPoint) { // it may be destroyed (#1844)
11520 hoverPoint.firePointEvent('click', e);
11521 }
11522
11523 // When clicking outside a tracker, fire a chart event
11524 } else {
11525 extend(e, this.getCoordinates(e));
11526
11527 // fire a click event in the chart
11528 if (chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) {
11529 fireEvent(chart, 'click', e);
11530 }
11531 }
11532
11533
11534 }
11535 },
11536
11537 /**
11538 * Set the JS DOM events on the container and document. This method should contain
11539 * a one-to-one assignment between methods and their handlers. Any advanced logic should
11540 * be moved to the handler reflecting the event's name.
11541 */
11542 setDOMEvents: function() {
11543
11544 var pointer = this,
11545 container = pointer.chart.container;
11546
11547 container.onmousedown = function(e) {
11548 pointer.onContainerMouseDown(e);
11549 };
11550 container.onmousemove = function(e) {
11551 pointer.onContainerMouseMove(e);
11552 };
11553 container.onclick = function(e) {
11554 pointer.onContainerClick(e);
11555 };
11556 addEvent(container, 'mouseleave', pointer.onContainerMouseLeave);
11557 if (H.chartCount === 1) {
11558 addEvent(doc, 'mouseup', pointer.onDocumentMouseUp);
11559 }
11560 if (H.hasTouch) {
11561 container.ontouchstart = function(e) {
11562 pointer.onContainerTouchStart(e);
11563 };
11564 container.ontouchmove = function(e) {
11565 pointer.onContainerTouchMove(e);
11566 };
11567 if (H.chartCount === 1) {
11568 addEvent(doc, 'touchend', pointer.onDocumentTouchEnd);
11569 }
11570 }
11571
11572 },
11573
11574 /**
11575 * Destroys the Pointer object and disconnects DOM events.
11576 */
11577 destroy: function() {
11578 var prop;
11579
11580 removeEvent(this.chart.container, 'mouseleave', this.onContainerMouseLeave);
11581 if (!H.chartCount) {
11582 removeEvent(doc, 'mouseup', this.onDocumentMouseUp);
11583 removeEvent(doc, 'touchend', this.onDocumentTouchEnd);
11584 }
11585
11586 // memory and CPU leak
11587 clearInterval(this.tooltipTimeout);
11588
11589 for (prop in this) {
11590 this[prop] = null;
11591 }
11592 }
11593 };
11594
11595 }(Highcharts));
11596 (function(H) {
11597 /**
11598 * (c) 2010-2016 Torstein Honsi
11599 *
11600 * License: www.highcharts.com/license
11601 */
11602 'use strict';
11603 var charts = H.charts,
11604 each = H.each,
11605 extend = H.extend,
11606 map = H.map,
11607 noop = H.noop,
11608 pick = H.pick,
11609 Pointer = H.Pointer;
11610
11611 /* Support for touch devices */
11612 extend(Pointer.prototype, {
11613
11614 /**
11615 * Run translation operations
11616 */
11617 pinchTranslate: function(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) {
11618 if (this.zoomHor || this.pinchHor) {
11619 this.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
11620 }
11621 if (this.zoomVert || this.pinchVert) {
11622 this.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
11623 }
11624 },
11625
11626 /**
11627 * Run translation operations for each direction (horizontal and vertical) independently
11628 */
11629 pinchTranslateDirection: function(horiz, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch, forcedScale) {
11630 var chart = this.chart,
11631 xy = horiz ? 'x' : 'y',
11632 XY = horiz ? 'X' : 'Y',
11633 sChartXY = 'chart' + XY,
11634 wh = horiz ? 'width' : 'height',
11635 plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')],
11636 selectionWH,
11637 selectionXY,
11638 clipXY,
11639 scale = forcedScale || 1,
11640 inverted = chart.inverted,
11641 bounds = chart.bounds[horiz ? 'h' : 'v'],
11642 singleTouch = pinchDown.length === 1,
11643 touch0Start = pinchDown[0][sChartXY],
11644 touch0Now = touches[0][sChartXY],
11645 touch1Start = !singleTouch && pinchDown[1][sChartXY],
11646 touch1Now = !singleTouch && touches[1][sChartXY],
11647 outOfBounds,
11648 transformScale,
11649 scaleKey,
11650 setScale = function() {
11651 if (!singleTouch && Math.abs(touch0Start - touch1Start) > 20) { // Don't zoom if fingers are too close on this axis
11652 scale = forcedScale || Math.abs(touch0Now - touch1Now) / Math.abs(touch0Start - touch1Start);
11653 }
11654
11655 clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start;
11656 selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale;
11657 };
11658
11659 // Set the scale, first pass
11660 setScale();
11661
11662 selectionXY = clipXY; // the clip position (x or y) is altered if out of bounds, the selection position is not
11663
11664 // Out of bounds
11665 if (selectionXY < bounds.min) {
11666 selectionXY = bounds.min;
11667 outOfBounds = true;
11668 } else if (selectionXY + selectionWH > bounds.max) {
11669 selectionXY = bounds.max - selectionWH;
11670 outOfBounds = true;
11671 }
11672
11673 // Is the chart dragged off its bounds, determined by dataMin and dataMax?
11674 if (outOfBounds) {
11675
11676 // Modify the touchNow position in order to create an elastic drag movement. This indicates
11677 // to the user that the chart is responsive but can't be dragged further.
11678 touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]);
11679 if (!singleTouch) {
11680 touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]);
11681 }
11682
11683 // Set the scale, second pass to adapt to the modified touchNow positions
11684 setScale();
11685
11686 } else {
11687 lastValidTouch[xy] = [touch0Now, touch1Now];
11688 }
11689
11690 // Set geometry for clipping, selection and transformation
11691 if (!inverted) {
11692 clip[xy] = clipXY - plotLeftTop;
11693 clip[wh] = selectionWH;
11694 }
11695 scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY;
11696 transformScale = inverted ? 1 / scale : scale;
11697
11698 selectionMarker[wh] = selectionWH;
11699 selectionMarker[xy] = selectionXY;
11700 transform[scaleKey] = scale;
11701 transform['translate' + XY] = (transformScale * plotLeftTop) + (touch0Now - (transformScale * touch0Start));
11702 },
11703
11704 /**
11705 * Handle touch events with two touches
11706 */
11707 pinch: function(e) {
11708
11709 var self = this,
11710 chart = self.chart,
11711 pinchDown = self.pinchDown,
11712 touches = e.touches,
11713 touchesLength = touches.length,
11714 lastValidTouch = self.lastValidTouch,
11715 hasZoom = self.hasZoom,
11716 selectionMarker = self.selectionMarker,
11717 transform = {},
11718 fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, 'highcharts-tracker') &&
11719 chart.runTrackerClick) || self.runChartClick),
11720 clip = {};
11721
11722 // Don't initiate panning until the user has pinched. This prevents us from
11723 // blocking page scrolling as users scroll down a long page (#4210).
11724 if (touchesLength > 1) {
11725 self.initiated = true;
11726 }
11727
11728 // On touch devices, only proceed to trigger click if a handler is defined
11729 if (hasZoom && self.initiated && !fireClickEvent) {
11730 e.preventDefault();
11731 }
11732
11733 // Normalize each touch
11734 map(touches, function(e) {
11735 return self.normalize(e);
11736 });
11737
11738 // Register the touch start position
11739 if (e.type === 'touchstart') {
11740 each(touches, function(e, i) {
11741 pinchDown[i] = {
11742 chartX: e.chartX,
11743 chartY: e.chartY
11744 };
11745 });
11746 lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && pinchDown[1].chartX];
11747 lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && pinchDown[1].chartY];
11748
11749 // Identify the data bounds in pixels
11750 each(chart.axes, function(axis) {
11751 if (axis.zoomEnabled) {
11752 var bounds = chart.bounds[axis.horiz ? 'h' : 'v'],
11753 minPixelPadding = axis.minPixelPadding,
11754 min = axis.toPixels(pick(axis.options.min, axis.dataMin)),
11755 max = axis.toPixels(pick(axis.options.max, axis.dataMax)),
11756 absMin = Math.min(min, max),
11757 absMax = Math.max(min, max);
11758
11759 // Store the bounds for use in the touchmove handler
11760 bounds.min = Math.min(axis.pos, absMin - minPixelPadding);
11761 bounds.max = Math.max(axis.pos + axis.len, absMax + minPixelPadding);
11762 }
11763 });
11764 self.res = true; // reset on next move
11765
11766 // Event type is touchmove, handle panning and pinching
11767 } else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first
11768
11769
11770 // Set the marker
11771 if (!selectionMarker) {
11772 self.selectionMarker = selectionMarker = extend({
11773 destroy: noop,
11774 touch: true
11775 }, chart.plotBox);
11776 }
11777
11778 self.pinchTranslate(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
11779
11780 self.hasPinched = hasZoom;
11781
11782 // Scale and translate the groups to provide visual feedback during pinching
11783 self.scaleGroups(transform, clip);
11784
11785 // Optionally move the tooltip on touchmove
11786 if (!hasZoom && self.followTouchMove && touchesLength === 1) {
11787 this.runPointActions(self.normalize(e));
11788 } else if (self.res) {
11789 self.res = false;
11790 this.reset(false, 0);
11791 }
11792 }
11793 },
11794
11795 /**
11796 * General touch handler shared by touchstart and touchmove.
11797 */
11798 touch: function(e, start) {
11799 var chart = this.chart,
11800 hasMoved,
11801 pinchDown;
11802
11803 H.hoverChartIndex = chart.index;
11804
11805 if (e.touches.length === 1) {
11806
11807 e = this.normalize(e);
11808
11809 if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop) && !chart.openMenu) {
11810
11811 // Run mouse events and display tooltip etc
11812 if (start) {
11813 this.runPointActions(e);
11814 }
11815
11816 // Android fires touchmove events after the touchstart even if the
11817 // finger hasn't moved, or moved only a pixel or two. In iOS however,
11818 // the touchmove doesn't fire unless the finger moves more than ~4px.
11819 // So we emulate this behaviour in Android by checking how much it
11820 // moved, and cancelling on small distances. #3450.
11821 if (e.type === 'touchmove') {
11822 pinchDown = this.pinchDown;
11823 hasMoved = pinchDown[0] ? Math.sqrt( // #5266
11824 Math.pow(pinchDown[0].chartX - e.chartX, 2) +
11825 Math.pow(pinchDown[0].chartY - e.chartY, 2)
11826 ) >= 4 : false;
11827 }
11828
11829 if (pick(hasMoved, true)) {
11830 this.pinch(e);
11831 }
11832
11833 } else if (start) {
11834 // Hide the tooltip on touching outside the plot area (#1203)
11835 this.reset();
11836 }
11837
11838 } else if (e.touches.length === 2) {
11839 this.pinch(e);
11840 }
11841 },
11842
11843 onContainerTouchStart: function(e) {
11844 this.zoomOption();
11845 this.touch(e, true);
11846 },
11847
11848 onContainerTouchMove: function(e) {
11849 this.touch(e);
11850 },
11851
11852 onDocumentTouchEnd: function(e) {
11853 if (charts[H.hoverChartIndex]) {
11854 charts[H.hoverChartIndex].pointer.drop(e);
11855 }
11856 }
11857
11858 });
11859
11860 }(Highcharts));
11861 (function(H) {
11862 /**
11863 * (c) 2010-2016 Torstein Honsi
11864 *
11865 * License: www.highcharts.com/license
11866 */
11867 'use strict';
11868 var addEvent = H.addEvent,
11869 charts = H.charts,
11870 css = H.css,
11871 doc = H.doc,
11872 extend = H.extend,
11873 noop = H.noop,
11874 Pointer = H.Pointer,
11875 removeEvent = H.removeEvent,
11876 win = H.win,
11877 wrap = H.wrap;
11878
11879 if (win.PointerEvent || win.MSPointerEvent) {
11880
11881 // The touches object keeps track of the points being touched at all times
11882 var touches = {},
11883 hasPointerEvent = !!win.PointerEvent,
11884 getWebkitTouches = function() {
11885 var key,
11886 fake = [];
11887 fake.item = function(i) {
11888 return this[i];
11889 };
11890 for (key in touches) {
11891 if (touches.hasOwnProperty(key)) {
11892 fake.push({
11893 pageX: touches[key].pageX,
11894 pageY: touches[key].pageY,
11895 target: touches[key].target
11896 });
11897 }
11898 }
11899 return fake;
11900 },
11901 translateMSPointer = function(e, method, wktype, func) {
11902 var p;
11903 if ((e.pointerType === 'touch' || e.pointerType === e.MSPOINTER_TYPE_TOUCH) && charts[H.hoverChartIndex]) {
11904 func(e);
11905 p = charts[H.hoverChartIndex].pointer;
11906 p[method]({
11907 type: wktype,
11908 target: e.currentTarget,
11909 preventDefault: noop,
11910 touches: getWebkitTouches()
11911 });
11912 }
11913 };
11914
11915 /**
11916 * Extend the Pointer prototype with methods for each event handler and more
11917 */
11918 extend(Pointer.prototype, {
11919 onContainerPointerDown: function(e) {
11920 translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function(e) {
11921 touches[e.pointerId] = {
11922 pageX: e.pageX,
11923 pageY: e.pageY,
11924 target: e.currentTarget
11925 };
11926 });
11927 },
11928 onContainerPointerMove: function(e) {
11929 translateMSPointer(e, 'onContainerTouchMove', 'touchmove', function(e) {
11930 touches[e.pointerId] = {
11931 pageX: e.pageX,
11932 pageY: e.pageY
11933 };
11934 if (!touches[e.pointerId].target) {
11935 touches[e.pointerId].target = e.currentTarget;
11936 }
11937 });
11938 },
11939 onDocumentPointerUp: function(e) {
11940 translateMSPointer(e, 'onDocumentTouchEnd', 'touchend', function(e) {
11941 delete touches[e.pointerId];
11942 });
11943 },
11944
11945 /**
11946 * Add or remove the MS Pointer specific events
11947 */
11948 batchMSEvents: function(fn) {
11949 fn(this.chart.container, hasPointerEvent ? 'pointerdown' : 'MSPointerDown', this.onContainerPointerDown);
11950 fn(this.chart.container, hasPointerEvent ? 'pointermove' : 'MSPointerMove', this.onContainerPointerMove);
11951 fn(doc, hasPointerEvent ? 'pointerup' : 'MSPointerUp', this.onDocumentPointerUp);
11952 }
11953 });
11954
11955 // Disable default IE actions for pinch and such on chart element
11956 wrap(Pointer.prototype, 'init', function(proceed, chart, options) {
11957 proceed.call(this, chart, options);
11958 if (this.hasZoom) { // #4014
11959 css(chart.container, {
11960 '-ms-touch-action': 'none',
11961 'touch-action': 'none'
11962 });
11963 }
11964 });
11965
11966 // Add IE specific touch events to chart
11967 wrap(Pointer.prototype, 'setDOMEvents', function(proceed) {
11968 proceed.apply(this);
11969 if (this.hasZoom || this.followTouchMove) {
11970 this.batchMSEvents(addEvent);
11971 }
11972 });
11973 // Destroy MS events also
11974 wrap(Pointer.prototype, 'destroy', function(proceed) {
11975 this.batchMSEvents(removeEvent);
11976 proceed.call(this);
11977 });
11978 }
11979
11980 }(Highcharts));
11981 (function(H) {
11982 /**
11983 * (c) 2010-2016 Torstein Honsi
11984 *
11985 * License: www.highcharts.com/license
11986 */
11987 'use strict';
11988 var Legend,
11989
11990 addEvent = H.addEvent,
11991 css = H.css,
11992 discardElement = H.discardElement,
11993 defined = H.defined,
11994 each = H.each,
11995 extend = H.extend,
11996 isFirefox = H.isFirefox,
11997 marginNames = H.marginNames,
11998 merge = H.merge,
11999 pick = H.pick,
12000 setAnimation = H.setAnimation,
12001 stableSort = H.stableSort,
12002 win = H.win,
12003 wrap = H.wrap;
12004 /**
12005 * The overview of the chart's series
12006 */
12007 Legend = H.Legend = function(chart, options) {
12008 this.init(chart, options);
12009 };
12010
12011 Legend.prototype = {
12012
12013 /**
12014 * Initialize the legend
12015 */
12016 init: function(chart, options) {
12017
12018 this.chart = chart;
12019
12020 this.setOptions(options);
12021
12022 if (options.enabled) {
12023
12024 // Render it
12025 this.render();
12026
12027 // move checkboxes
12028 addEvent(this.chart, 'endResize', function() {
12029 this.legend.positionCheckboxes();
12030 });
12031 }
12032 },
12033
12034 setOptions: function(options) {
12035
12036 var padding = pick(options.padding, 8);
12037
12038 this.options = options;
12039
12040
12041 this.itemStyle = options.itemStyle;
12042 this.itemHiddenStyle = merge(this.itemStyle, options.itemHiddenStyle);
12043
12044 this.itemMarginTop = options.itemMarginTop || 0;
12045 this.padding = padding;
12046 this.initialItemX = padding;
12047 this.initialItemY = padding - 5; // 5 is the number of pixels above the text
12048 this.maxItemWidth = 0;
12049 this.itemHeight = 0;
12050 this.symbolWidth = pick(options.symbolWidth, 16);
12051 this.pages = [];
12052
12053 },
12054
12055 /**
12056 * Update the legend with new options. Equivalent to running chart.update with a legend
12057 * configuration option.
12058 * @param {Object} options Legend options
12059 * @param {Boolean} redraw Whether to redraw the chart, defaults to true.
12060 */
12061 update: function(options, redraw) {
12062 var chart = this.chart;
12063
12064 this.setOptions(merge(true, this.options, options));
12065 this.destroy();
12066 chart.isDirtyLegend = chart.isDirtyBox = true;
12067 if (pick(redraw, true)) {
12068 chart.redraw();
12069 }
12070 },
12071
12072 /**
12073 * Set the colors for the legend item
12074 * @param {Object} item A Series or Point instance
12075 * @param {Object} visible Dimmed or colored
12076 */
12077 colorizeItem: function(item, visible) {
12078 item.legendGroup[visible ? 'removeClass' : 'addClass']('highcharts-legend-item-hidden');
12079
12080
12081 var legend = this,
12082 options = legend.options,
12083 legendItem = item.legendItem,
12084 legendLine = item.legendLine,
12085 legendSymbol = item.legendSymbol,
12086 hiddenColor = legend.itemHiddenStyle.color,
12087 textColor = visible ? options.itemStyle.color : hiddenColor,
12088 symbolColor = visible ? (item.color || hiddenColor) : hiddenColor,
12089 markerOptions = item.options && item.options.marker,
12090 symbolAttr = {
12091 fill: symbolColor
12092 },
12093 key;
12094
12095 if (legendItem) {
12096 legendItem.css({
12097 fill: textColor,
12098 color: textColor
12099 }); // color for #1553, oldIE
12100 }
12101 if (legendLine) {
12102 legendLine.attr({
12103 stroke: symbolColor
12104 });
12105 }
12106
12107 if (legendSymbol) {
12108
12109 // Apply marker options
12110 if (markerOptions && legendSymbol.isMarker) { // #585
12111 //symbolAttr.stroke = symbolColor;
12112 symbolAttr = item.pointAttribs();
12113 if (!visible) {
12114 for (key in symbolAttr) {
12115 symbolAttr[key] = hiddenColor;
12116 }
12117 }
12118 }
12119
12120 legendSymbol.attr(symbolAttr);
12121 }
12122
12123 },
12124
12125 /**
12126 * Position the legend item
12127 * @param {Object} item A Series or Point instance
12128 */
12129 positionItem: function(item) {
12130 var legend = this,
12131 options = legend.options,
12132 symbolPadding = options.symbolPadding,
12133 ltr = !options.rtl,
12134 legendItemPos = item._legendItemPos,
12135 itemX = legendItemPos[0],
12136 itemY = legendItemPos[1],
12137 checkbox = item.checkbox,
12138 legendGroup = item.legendGroup;
12139
12140 if (legendGroup && legendGroup.element) {
12141 legendGroup.translate(
12142 ltr ? itemX : legend.legendWidth - itemX - 2 * symbolPadding - 4,
12143 itemY
12144 );
12145 }
12146
12147 if (checkbox) {
12148 checkbox.x = itemX;
12149 checkbox.y = itemY;
12150 }
12151 },
12152
12153 /**
12154 * Destroy a single legend item
12155 * @param {Object} item The series or point
12156 */
12157 destroyItem: function(item) {
12158 var checkbox = item.checkbox;
12159
12160 // destroy SVG elements
12161 each(['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], function(key) {
12162 if (item[key]) {
12163 item[key] = item[key].destroy();
12164 }
12165 });
12166
12167 if (checkbox) {
12168 discardElement(item.checkbox);
12169 }
12170 },
12171
12172 /**
12173 * Destroys the legend.
12174 */
12175 destroy: function() {
12176 var legend = this,
12177 legendGroup = legend.group,
12178 box = legend.box;
12179
12180 if (box) {
12181 legend.box = box.destroy();
12182 }
12183
12184 // Destroy items
12185 each(this.getAllItems(), function(item) {
12186 each(['legendItem', 'legendGroup'], function(key) {
12187 if (item[key]) {
12188 item[key] = item[key].destroy();
12189 }
12190 });
12191 });
12192
12193 if (legendGroup) {
12194 legend.group = legendGroup.destroy();
12195 }
12196 },
12197
12198 /**
12199 * Position the checkboxes after the width is determined
12200 */
12201 positionCheckboxes: function(scrollOffset) {
12202 var alignAttr = this.group.alignAttr,
12203 translateY,
12204 clipHeight = this.clipHeight || this.legendHeight,
12205 titleHeight = this.titleHeight;
12206
12207 if (alignAttr) {
12208 translateY = alignAttr.translateY;
12209 each(this.allItems, function(item) {
12210 var checkbox = item.checkbox,
12211 top;
12212
12213 if (checkbox) {
12214 top = translateY + titleHeight + checkbox.y + (scrollOffset || 0) + 3;
12215 css(checkbox, {
12216 left: (alignAttr.translateX + item.checkboxOffset + checkbox.x - 20) + 'px',
12217 top: top + 'px',
12218 display: top > translateY - 6 && top < translateY + clipHeight - 6 ? '' : 'none'
12219 });
12220 }
12221 });
12222 }
12223 },
12224
12225 /**
12226 * Render the legend title on top of the legend
12227 */
12228 renderTitle: function() {
12229 var options = this.options,
12230 padding = this.padding,
12231 titleOptions = options.title,
12232 titleHeight = 0,
12233 bBox;
12234
12235 if (titleOptions.text) {
12236 if (!this.title) {
12237 this.title = this.chart.renderer.label(titleOptions.text, padding - 3, padding - 4, null, null, null, null, null, 'legend-title')
12238 .attr({
12239 zIndex: 1
12240 })
12241
12242 .css(titleOptions.style)
12243
12244 .add(this.group);
12245 }
12246 bBox = this.title.getBBox();
12247 titleHeight = bBox.height;
12248 this.offsetWidth = bBox.width; // #1717
12249 this.contentGroup.attr({
12250 translateY: titleHeight
12251 });
12252 }
12253 this.titleHeight = titleHeight;
12254 },
12255
12256 /**
12257 * Set the legend item text
12258 */
12259 setText: function(item) {
12260 var options = this.options;
12261 item.legendItem.attr({
12262 text: options.labelFormat ? H.format(options.labelFormat, item) : options.labelFormatter.call(item)
12263 });
12264 },
12265
12266 /**
12267 * Render a single specific legend item
12268 * @param {Object} item A series or point
12269 */
12270 renderItem: function(item) {
12271 var legend = this,
12272 chart = legend.chart,
12273 renderer = chart.renderer,
12274 options = legend.options,
12275 horizontal = options.layout === 'horizontal',
12276 symbolWidth = legend.symbolWidth,
12277 symbolPadding = options.symbolPadding,
12278
12279 itemStyle = legend.itemStyle,
12280 itemHiddenStyle = legend.itemHiddenStyle,
12281
12282 padding = legend.padding,
12283 itemDistance = horizontal ? pick(options.itemDistance, 20) : 0,
12284 ltr = !options.rtl,
12285 itemHeight,
12286 widthOption = options.width,
12287 itemMarginBottom = options.itemMarginBottom || 0,
12288 itemMarginTop = legend.itemMarginTop,
12289 initialItemX = legend.initialItemX,
12290 bBox,
12291 itemWidth,
12292 li = item.legendItem,
12293 isSeries = !item.series,
12294 series = !isSeries && item.series.drawLegendSymbol ? item.series : item,
12295 seriesOptions = series.options,
12296 showCheckbox = legend.createCheckboxForItem && seriesOptions && seriesOptions.showCheckbox,
12297 useHTML = options.useHTML,
12298 fontSize = 12;
12299
12300 if (!li) { // generate it once, later move it
12301
12302 // Generate the group box
12303 // A group to hold the symbol and text. Text is to be appended in Legend class.
12304 item.legendGroup = renderer.g('legend-item')
12305 .addClass('highcharts-' + series.type + '-series highcharts-color-' + item.colorIndex + ' ' +
12306 (item.options.className || '') +
12307 (isSeries ? 'highcharts-series-' + item.index : '')
12308 )
12309 .attr({
12310 zIndex: 1
12311 })
12312 .add(legend.scrollGroup);
12313
12314 // Generate the list item text and add it to the group
12315 item.legendItem = li = renderer.text(
12316 '',
12317 ltr ? symbolWidth + symbolPadding : -symbolPadding,
12318 legend.baseline || 0,
12319 useHTML
12320 )
12321
12322 .css(merge(item.visible ? itemStyle : itemHiddenStyle)) // merge to prevent modifying original (#1021)
12323
12324 .attr({
12325 align: ltr ? 'left' : 'right',
12326 zIndex: 2
12327 })
12328 .add(item.legendGroup);
12329
12330 // Get the baseline for the first item - the font size is equal for all
12331 if (!legend.baseline) {
12332
12333 fontSize = itemStyle.fontSize;
12334
12335 legend.fontMetrics = renderer.fontMetrics(
12336 fontSize,
12337 li
12338 );
12339 legend.baseline = legend.fontMetrics.f + 3 + itemMarginTop;
12340 li.attr('y', legend.baseline);
12341 }
12342
12343 // Draw the legend symbol inside the group box
12344 series.drawLegendSymbol(legend, item);
12345
12346 if (legend.setItemEvents) {
12347 legend.setItemEvents(item, li, useHTML);
12348 }
12349
12350 // add the HTML checkbox on top
12351 if (showCheckbox) {
12352 legend.createCheckboxForItem(item);
12353 }
12354 }
12355
12356 // Colorize the items
12357 legend.colorizeItem(item, item.visible);
12358
12359 // Always update the text
12360 legend.setText(item);
12361
12362 // calculate the positions for the next line
12363 bBox = li.getBBox();
12364
12365 itemWidth = item.checkboxOffset =
12366 options.itemWidth ||
12367 item.legendItemWidth ||
12368 symbolWidth + symbolPadding + bBox.width + itemDistance + (showCheckbox ? 20 : 0);
12369 legend.itemHeight = itemHeight = Math.round(item.legendItemHeight || bBox.height);
12370
12371 // if the item exceeds the width, start a new line
12372 if (horizontal && legend.itemX - initialItemX + itemWidth >
12373 (widthOption || (chart.chartWidth - 2 * padding - initialItemX - options.x))) {
12374 legend.itemX = initialItemX;
12375 legend.itemY += itemMarginTop + legend.lastLineHeight + itemMarginBottom;
12376 legend.lastLineHeight = 0; // reset for next line (#915, #3976)
12377 }
12378
12379 // If the item exceeds the height, start a new column
12380 /*if (!horizontal && legend.itemY + options.y + itemHeight > chart.chartHeight - spacingTop - spacingBottom) {
12381 legend.itemY = legend.initialItemY;
12382 legend.itemX += legend.maxItemWidth;
12383 legend.maxItemWidth = 0;
12384 }*/
12385
12386 // Set the edge positions
12387 legend.maxItemWidth = Math.max(legend.maxItemWidth, itemWidth);
12388 legend.lastItemY = itemMarginTop + legend.itemY + itemMarginBottom;
12389 legend.lastLineHeight = Math.max(itemHeight, legend.lastLineHeight); // #915
12390
12391 // cache the position of the newly generated or reordered items
12392 item._legendItemPos = [legend.itemX, legend.itemY];
12393
12394 // advance
12395 if (horizontal) {
12396 legend.itemX += itemWidth;
12397
12398 } else {
12399 legend.itemY += itemMarginTop + itemHeight + itemMarginBottom;
12400 legend.lastLineHeight = itemHeight;
12401 }
12402
12403 // the width of the widest item
12404 legend.offsetWidth = widthOption || Math.max(
12405 (horizontal ? legend.itemX - initialItemX - itemDistance : itemWidth) + padding,
12406 legend.offsetWidth
12407 );
12408 },
12409
12410 /**
12411 * Get all items, which is one item per series for normal series and one item per point
12412 * for pie series.
12413 */
12414 getAllItems: function() {
12415 var allItems = [];
12416 each(this.chart.series, function(series) {
12417 var seriesOptions = series && series.options;
12418
12419 // Handle showInLegend. If the series is linked to another series, defaults to false.
12420 if (series && pick(seriesOptions.showInLegend, !defined(seriesOptions.linkedTo) ? undefined : false, true)) {
12421
12422 // Use points or series for the legend item depending on legendType
12423 allItems = allItems.concat(
12424 series.legendItems ||
12425 (seriesOptions.legendType === 'point' ?
12426 series.data :
12427 series)
12428 );
12429 }
12430 });
12431 return allItems;
12432 },
12433
12434 /**
12435 * Adjust the chart margins by reserving space for the legend on only one side
12436 * of the chart. If the position is set to a corner, top or bottom is reserved
12437 * for horizontal legends and left or right for vertical ones.
12438 */
12439 adjustMargins: function(margin, spacing) {
12440 var chart = this.chart,
12441 options = this.options,
12442 // Use the first letter of each alignment option in order to detect the side
12443 alignment = options.align.charAt(0) + options.verticalAlign.charAt(0) + options.layout.charAt(0); // #4189 - use charAt(x) notation instead of [x] for IE7
12444
12445 if (!options.floating) {
12446
12447 each([
12448 /(lth|ct|rth)/,
12449 /(rtv|rm|rbv)/,
12450 /(rbh|cb|lbh)/,
12451 /(lbv|lm|ltv)/
12452 ], function(alignments, side) {
12453 if (alignments.test(alignment) && !defined(margin[side])) {
12454 // Now we have detected on which side of the chart we should reserve space for the legend
12455 chart[marginNames[side]] = Math.max(
12456 chart[marginNames[side]],
12457 chart.legend[(side + 1) % 2 ? 'legendHeight' : 'legendWidth'] + [1, -1, -1, 1][side] * options[(side % 2) ? 'x' : 'y'] +
12458 pick(options.margin, 12) +
12459 spacing[side]
12460 );
12461 }
12462 });
12463 }
12464 },
12465
12466 /**
12467 * Render the legend. This method can be called both before and after
12468 * chart.render. If called after, it will only rearrange items instead
12469 * of creating new ones.
12470 */
12471 render: function() {
12472 var legend = this,
12473 chart = legend.chart,
12474 renderer = chart.renderer,
12475 legendGroup = legend.group,
12476 allItems,
12477 display,
12478 legendWidth,
12479 legendHeight,
12480 box = legend.box,
12481 options = legend.options,
12482 padding = legend.padding;
12483
12484 legend.itemX = legend.initialItemX;
12485 legend.itemY = legend.initialItemY;
12486 legend.offsetWidth = 0;
12487 legend.lastItemY = 0;
12488
12489 if (!legendGroup) {
12490 legend.group = legendGroup = renderer.g('legend')
12491 .attr({
12492 zIndex: 7
12493 })
12494 .add();
12495 legend.contentGroup = renderer.g()
12496 .attr({
12497 zIndex: 1
12498 }) // above background
12499 .add(legendGroup);
12500 legend.scrollGroup = renderer.g()
12501 .add(legend.contentGroup);
12502 }
12503
12504 legend.renderTitle();
12505
12506 // add each series or point
12507 allItems = legend.getAllItems();
12508
12509 // sort by legendIndex
12510 stableSort(allItems, function(a, b) {
12511 return ((a.options && a.options.legendIndex) || 0) - ((b.options && b.options.legendIndex) || 0);
12512 });
12513
12514 // reversed legend
12515 if (options.reversed) {
12516 allItems.reverse();
12517 }
12518
12519 legend.allItems = allItems;
12520 legend.display = display = !!allItems.length;
12521
12522 // render the items
12523 legend.lastLineHeight = 0;
12524 each(allItems, function(item) {
12525 legend.renderItem(item);
12526 });
12527
12528 // Get the box
12529 legendWidth = (options.width || legend.offsetWidth) + padding;
12530 legendHeight = legend.lastItemY + legend.lastLineHeight + legend.titleHeight;
12531 legendHeight = legend.handleOverflow(legendHeight);
12532 legendHeight += padding;
12533
12534 // Draw the border and/or background
12535 if (!box) {
12536 legend.box = box = renderer.rect()
12537 .addClass('highcharts-legend-box')
12538 .attr({
12539 r: options.borderRadius
12540 })
12541 .add(legendGroup);
12542 box.isNew = true;
12543 }
12544
12545
12546 // Presentational
12547 box.attr({
12548 stroke: options.borderColor,
12549 'stroke-width': options.borderWidth || 0,
12550 fill: options.backgroundColor || 'none'
12551 })
12552 .shadow(options.shadow);
12553
12554
12555 if (legendWidth > 0 && legendHeight > 0) {
12556 box[box.isNew ? 'attr' : 'animate'](
12557 box.crisp({
12558 x: 0,
12559 y: 0,
12560 width: legendWidth,
12561 height: legendHeight
12562 }, box.strokeWidth())
12563 );
12564 box.isNew = false;
12565 }
12566
12567 // hide the border if no items
12568 box[display ? 'show' : 'hide']();
12569
12570
12571
12572 legend.legendWidth = legendWidth;
12573 legend.legendHeight = legendHeight;
12574
12575 // Now that the legend width and height are established, put the items in the
12576 // final position
12577 each(allItems, function(item) {
12578 legend.positionItem(item);
12579 });
12580
12581 // 1.x compatibility: positioning based on style
12582 /*var props = ['left', 'right', 'top', 'bottom'],
12583 prop,
12584 i = 4;
12585 while (i--) {
12586 prop = props[i];
12587 if (options.style[prop] && options.style[prop] !== 'auto') {
12588 options[i < 2 ? 'align' : 'verticalAlign'] = prop;
12589 options[i < 2 ? 'x' : 'y'] = pInt(options.style[prop]) * (i % 2 ? -1 : 1);
12590 }
12591 }*/
12592
12593 if (display) {
12594 legendGroup.align(extend({
12595 width: legendWidth,
12596 height: legendHeight
12597 }, options), true, 'spacingBox');
12598 }
12599
12600 if (!chart.isResizing) {
12601 this.positionCheckboxes();
12602 }
12603 },
12604
12605 /**
12606 * Set up the overflow handling by adding navigation with up and down arrows below the
12607 * legend.
12608 */
12609 handleOverflow: function(legendHeight) {
12610 var legend = this,
12611 chart = this.chart,
12612 renderer = chart.renderer,
12613 options = this.options,
12614 optionsY = options.y,
12615 alignTop = options.verticalAlign === 'top',
12616 spaceHeight = chart.spacingBox.height + (alignTop ? -optionsY : optionsY) - this.padding,
12617 maxHeight = options.maxHeight,
12618 clipHeight,
12619 clipRect = this.clipRect,
12620 navOptions = options.navigation,
12621 animation = pick(navOptions.animation, true),
12622 arrowSize = navOptions.arrowSize || 12,
12623 nav = this.nav,
12624 pages = this.pages,
12625 padding = this.padding,
12626 lastY,
12627 allItems = this.allItems,
12628 clipToHeight = function(height) {
12629 clipRect.attr({
12630 height: height
12631 });
12632
12633 // useHTML
12634 if (legend.contentGroup.div) {
12635 legend.contentGroup.div.style.clip = 'rect(' + padding + 'px,9999px,' + (padding + height) + 'px,0)';
12636 }
12637 };
12638
12639
12640 // Adjust the height
12641 if (options.layout === 'horizontal') {
12642 spaceHeight /= 2;
12643 }
12644 if (maxHeight) {
12645 spaceHeight = Math.min(spaceHeight, maxHeight);
12646 }
12647
12648 // Reset the legend height and adjust the clipping rectangle
12649 pages.length = 0;
12650 if (legendHeight > spaceHeight && navOptions.enabled !== false) {
12651
12652 this.clipHeight = clipHeight = Math.max(spaceHeight - 20 - this.titleHeight - padding, 0);
12653 this.currentPage = pick(this.currentPage, 1);
12654 this.fullHeight = legendHeight;
12655
12656 // Fill pages with Y positions so that the top of each a legend item defines
12657 // the scroll top for each page (#2098)
12658 each(allItems, function(item, i) {
12659 var y = item._legendItemPos[1],
12660 h = Math.round(item.legendItem.getBBox().height),
12661 len = pages.length;
12662
12663 if (!len || (y - pages[len - 1] > clipHeight && (lastY || y) !== pages[len - 1])) {
12664 pages.push(lastY || y);
12665 len++;
12666 }
12667
12668 if (i === allItems.length - 1 && y + h - pages[len - 1] > clipHeight) {
12669 pages.push(y);
12670 }
12671 if (y !== lastY) {
12672 lastY = y;
12673 }
12674 });
12675
12676 // Only apply clipping if needed. Clipping causes blurred legend in PDF export (#1787)
12677 if (!clipRect) {
12678 clipRect = legend.clipRect = renderer.clipRect(0, padding, 9999, 0);
12679 legend.contentGroup.clip(clipRect);
12680 }
12681
12682 clipToHeight(clipHeight);
12683
12684 // Add navigation elements
12685 if (!nav) {
12686 this.nav = nav = renderer.g().attr({
12687 zIndex: 1
12688 }).add(this.group);
12689 this.up = renderer.symbol('triangle', 0, 0, arrowSize, arrowSize)
12690 .on('click', function() {
12691 legend.scroll(-1, animation);
12692 })
12693 .add(nav);
12694 this.pager = renderer.text('', 15, 10)
12695 .addClass('highcharts-legend-navigation')
12696
12697 .css(navOptions.style)
12698
12699 .add(nav);
12700 this.down = renderer.symbol('triangle-down', 0, 0, arrowSize, arrowSize)
12701 .on('click', function() {
12702 legend.scroll(1, animation);
12703 })
12704 .add(nav);
12705 }
12706
12707 // Set initial position
12708 legend.scroll(0);
12709
12710 legendHeight = spaceHeight;
12711
12712 } else if (nav) {
12713 clipToHeight(chart.chartHeight);
12714 nav.hide();
12715 this.scrollGroup.attr({
12716 translateY: 1
12717 });
12718 this.clipHeight = 0; // #1379
12719 }
12720
12721 return legendHeight;
12722 },
12723
12724 /**
12725 * Scroll the legend by a number of pages
12726 * @param {Object} scrollBy
12727 * @param {Object} animation
12728 */
12729 scroll: function(scrollBy, animation) {
12730 var pages = this.pages,
12731 pageCount = pages.length,
12732 currentPage = this.currentPage + scrollBy,
12733 clipHeight = this.clipHeight,
12734 navOptions = this.options.navigation,
12735 pager = this.pager,
12736 padding = this.padding,
12737 scrollOffset;
12738
12739 // When resizing while looking at the last page
12740 if (currentPage > pageCount) {
12741 currentPage = pageCount;
12742 }
12743
12744 if (currentPage > 0) {
12745
12746 if (animation !== undefined) {
12747 setAnimation(animation, this.chart);
12748 }
12749
12750 this.nav.attr({
12751 translateX: padding,
12752 translateY: clipHeight + this.padding + 7 + this.titleHeight,
12753 visibility: 'visible'
12754 });
12755 this.up.attr({
12756 'class': currentPage === 1 ? 'highcharts-legend-nav-inactive' : 'highcharts-legend-nav-active'
12757 });
12758 pager.attr({
12759 text: currentPage + '/' + pageCount
12760 });
12761 this.down.attr({
12762 'x': 18 + this.pager.getBBox().width, // adjust to text width
12763 'class': currentPage === pageCount ? 'highcharts-legend-nav-inactive' : 'highcharts-legend-nav-active'
12764 });
12765
12766
12767 this.up
12768 .attr({
12769 fill: currentPage === 1 ? navOptions.inactiveColor : navOptions.activeColor
12770 })
12771 .css({
12772 cursor: currentPage === 1 ? 'default' : 'pointer'
12773 });
12774 this.down
12775 .attr({
12776 fill: currentPage === pageCount ? navOptions.inactiveColor : navOptions.activeColor
12777 })
12778 .css({
12779 cursor: currentPage === pageCount ? 'default' : 'pointer'
12780 });
12781
12782
12783 scrollOffset = -pages[currentPage - 1] + this.initialItemY;
12784
12785 this.scrollGroup.animate({
12786 translateY: scrollOffset
12787 });
12788
12789 this.currentPage = currentPage;
12790 this.positionCheckboxes(scrollOffset);
12791 }
12792
12793 }
12794
12795 };
12796
12797 /*
12798 * LegendSymbolMixin
12799 */
12800
12801 H.LegendSymbolMixin = {
12802
12803 /**
12804 * Get the series' symbol in the legend
12805 *
12806 * @param {Object} legend The legend object
12807 * @param {Object} item The series (this) or point
12808 */
12809 drawRectangle: function(legend, item) {
12810 var options = legend.options,
12811 symbolHeight = options.symbolHeight || legend.fontMetrics.f,
12812 square = options.squareSymbol,
12813 symbolWidth = square ? symbolHeight : legend.symbolWidth; // docs: square
12814
12815 item.legendSymbol = this.chart.renderer.rect(
12816 square ? (legend.symbolWidth - symbolHeight) / 2 : 0,
12817 legend.baseline - symbolHeight + 1, // #3988
12818 symbolWidth,
12819 symbolHeight,
12820 pick(legend.options.symbolRadius, symbolHeight / 2) // docs: new default
12821 )
12822 .addClass('highcharts-point')
12823 .attr({
12824 zIndex: 3
12825 }).add(item.legendGroup);
12826
12827 },
12828
12829 /**
12830 * Get the series' symbol in the legend. This method should be overridable to create custom
12831 * symbols through Highcharts.seriesTypes[type].prototype.drawLegendSymbols.
12832 *
12833 * @param {Object} legend The legend object
12834 */
12835 drawLineMarker: function(legend) {
12836
12837 var options = this.options,
12838 markerOptions = options.marker,
12839 radius,
12840 legendSymbol,
12841 symbolWidth = legend.symbolWidth,
12842 renderer = this.chart.renderer,
12843 legendItemGroup = this.legendGroup,
12844 verticalCenter = legend.baseline - Math.round(legend.fontMetrics.b * 0.3),
12845 attr = {};
12846
12847 // Draw the line
12848
12849 attr = {
12850 'stroke-width': options.lineWidth || 0
12851 };
12852 if (options.dashStyle) {
12853 attr.dashstyle = options.dashStyle;
12854 }
12855
12856
12857 this.legendLine = renderer.path([
12858 'M',
12859 0,
12860 verticalCenter,
12861 'L',
12862 symbolWidth,
12863 verticalCenter
12864 ])
12865 .addClass('highcharts-graph')
12866 .attr(attr)
12867 .add(legendItemGroup);
12868
12869 // Draw the marker
12870 if (markerOptions && markerOptions.enabled !== false) {
12871 radius = markerOptions.radius;
12872 this.legendSymbol = legendSymbol = renderer.symbol(
12873 this.symbol,
12874 (symbolWidth / 2) - radius,
12875 verticalCenter - radius,
12876 2 * radius,
12877 2 * radius,
12878 markerOptions
12879 )
12880 .addClass('highcharts-point')
12881 .add(legendItemGroup);
12882 legendSymbol.isMarker = true;
12883 }
12884 }
12885 };
12886
12887 // Workaround for #2030, horizontal legend items not displaying in IE11 Preview,
12888 // and for #2580, a similar drawing flaw in Firefox 26.
12889 // Explore if there's a general cause for this. The problem may be related
12890 // to nested group elements, as the legend item texts are within 4 group elements.
12891 if (/Trident\/7\.0/.test(win.navigator.userAgent) || isFirefox) {
12892 wrap(Legend.prototype, 'positionItem', function(proceed, item) {
12893 var legend = this,
12894 runPositionItem = function() { // If chart destroyed in sync, this is undefined (#2030)
12895 if (item._legendItemPos) {
12896 proceed.call(legend, item);
12897 }
12898 };
12899
12900 // Do it now, for export and to get checkbox placement
12901 runPositionItem();
12902
12903 // Do it after to work around the core issue
12904 setTimeout(runPositionItem);
12905 });
12906 }
12907
12908 }(Highcharts));
12909 (function(H) {
12910 /**
12911 * (c) 2010-2016 Torstein Honsi
12912 *
12913 * License: www.highcharts.com/license
12914 */
12915 'use strict';
12916 var addEvent = H.addEvent,
12917 animate = H.animate,
12918 animObject = H.animObject,
12919 attr = H.attr,
12920 doc = H.doc,
12921 Axis = H.Axis, // @todo add as requirement
12922 createElement = H.createElement,
12923 defaultOptions = H.defaultOptions,
12924 discardElement = H.discardElement,
12925 charts = H.charts,
12926 css = H.css,
12927 defined = H.defined,
12928 each = H.each,
12929 error = H.error,
12930 extend = H.extend,
12931 fireEvent = H.fireEvent,
12932 getStyle = H.getStyle,
12933 grep = H.grep,
12934 isNumber = H.isNumber,
12935 isObject = H.isObject,
12936 isString = H.isString,
12937 Legend = H.Legend, // @todo add as requirement
12938 marginNames = H.marginNames,
12939 merge = H.merge,
12940 Pointer = H.Pointer, // @todo add as requirement
12941 pick = H.pick,
12942 pInt = H.pInt,
12943 removeEvent = H.removeEvent,
12944 seriesTypes = H.seriesTypes,
12945 splat = H.splat,
12946 svg = H.svg,
12947 syncTimeout = H.syncTimeout,
12948 win = H.win,
12949 Renderer = H.Renderer;
12950 /**
12951 * The Chart class
12952 * @param {String|Object} renderTo The DOM element to render to, or its id
12953 * @param {Object} options
12954 * @param {Function} callback Function to run when the chart has loaded
12955 */
12956 var Chart = H.Chart = function() {
12957 this.getArgs.apply(this, arguments);
12958 };
12959
12960 H.chart = function(a, b, c) {
12961 return new Chart(a, b, c);
12962 };
12963
12964 Chart.prototype = {
12965
12966 /**
12967 * Hook for modules
12968 */
12969 callbacks: [],
12970
12971 /**
12972 * Handle the arguments passed to the constructor
12973 * @returns {Array} Arguments without renderTo
12974 */
12975 getArgs: function() {
12976 var args = [].slice.call(arguments);
12977
12978 // Remove the optional first argument, renderTo, and
12979 // set it on this.
12980 if (isString(args[0]) || args[0].nodeName) {
12981 this.renderTo = args.shift();
12982 }
12983 this.init(args[0], args[1]);
12984 },
12985
12986 /**
12987 * Initialize the chart
12988 */
12989 init: function(userOptions, callback) {
12990
12991 // Handle regular options
12992 var options,
12993 seriesOptions = userOptions.series; // skip merging data points to increase performance
12994
12995 userOptions.series = null;
12996 options = merge(defaultOptions, userOptions); // do the merge
12997 options.series = userOptions.series = seriesOptions; // set back the series data
12998 this.userOptions = userOptions;
12999 this.respRules = [];
13000
13001 var optionsChart = options.chart;
13002
13003 var chartEvents = optionsChart.events;
13004
13005 this.margin = [];
13006 this.spacing = [];
13007
13008 //this.runChartClick = chartEvents && !!chartEvents.click;
13009 this.bounds = {
13010 h: {},
13011 v: {}
13012 }; // Pixel data bounds for touch zoom
13013
13014 this.callback = callback;
13015 this.isResizing = 0;
13016 this.options = options;
13017 //chartTitleOptions = undefined;
13018 //chartSubtitleOptions = undefined;
13019
13020 this.axes = [];
13021 this.series = [];
13022 this.hasCartesianSeries = optionsChart.showAxes;
13023 //this.axisOffset = undefined;
13024 //this.inverted = undefined;
13025 //this.loadingShown = undefined;
13026 //this.container = undefined;
13027 //this.chartWidth = undefined;
13028 //this.chartHeight = undefined;
13029 //this.marginRight = undefined;
13030 //this.marginBottom = undefined;
13031 //this.containerWidth = undefined;
13032 //this.containerHeight = undefined;
13033 //this.oldChartWidth = undefined;
13034 //this.oldChartHeight = undefined;
13035
13036 //this.renderTo = undefined;
13037 //this.renderToClone = undefined;
13038
13039 //this.spacingBox = undefined
13040
13041 //this.legend = undefined;
13042
13043 // Elements
13044 //this.chartBackground = undefined;
13045 //this.plotBackground = undefined;
13046 //this.plotBGImage = undefined;
13047 //this.plotBorder = undefined;
13048 //this.loadingDiv = undefined;
13049 //this.loadingSpan = undefined;
13050
13051 var chart = this,
13052 eventType;
13053
13054 // Add the chart to the global lookup
13055 chart.index = charts.length;
13056 charts.push(chart);
13057 H.chartCount++;
13058
13059 // Chart event handlers
13060 if (chartEvents) {
13061 for (eventType in chartEvents) {
13062 addEvent(chart, eventType, chartEvents[eventType]);
13063 }
13064 }
13065
13066 chart.xAxis = [];
13067 chart.yAxis = [];
13068
13069 chart.pointCount = chart.colorCounter = chart.symbolCounter = 0;
13070
13071 chart.firstRender();
13072 },
13073
13074 /**
13075 * Initialize an individual series, called internally before render time
13076 */
13077 initSeries: function(options) {
13078 var chart = this,
13079 optionsChart = chart.options.chart,
13080 type = options.type || optionsChart.type || optionsChart.defaultSeriesType,
13081 series,
13082 Constr = seriesTypes[type];
13083
13084 // No such series type
13085 if (!Constr) {
13086 error(17, true);
13087 }
13088
13089 series = new Constr();
13090 series.init(this, options);
13091 return series;
13092 },
13093
13094 /**
13095 * Check whether a given point is within the plot area
13096 *
13097 * @param {Number} plotX Pixel x relative to the plot area
13098 * @param {Number} plotY Pixel y relative to the plot area
13099 * @param {Boolean} inverted Whether the chart is inverted
13100 */
13101 isInsidePlot: function(plotX, plotY, inverted) {
13102 var x = inverted ? plotY : plotX,
13103 y = inverted ? plotX : plotY;
13104
13105 return x >= 0 &&
13106 x <= this.plotWidth &&
13107 y >= 0 &&
13108 y <= this.plotHeight;
13109 },
13110
13111 /**
13112 * Redraw legend, axes or series based on updated data
13113 *
13114 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
13115 * configuration
13116 */
13117 redraw: function(animation) {
13118 var chart = this,
13119 axes = chart.axes,
13120 series = chart.series,
13121 pointer = chart.pointer,
13122 legend = chart.legend,
13123 redrawLegend = chart.isDirtyLegend,
13124 hasStackedSeries,
13125 hasDirtyStacks,
13126 hasCartesianSeries = chart.hasCartesianSeries,
13127 isDirtyBox = chart.isDirtyBox,
13128 seriesLength = series.length,
13129 i = seriesLength,
13130 serie,
13131 renderer = chart.renderer,
13132 isHiddenChart = renderer.isHidden(),
13133 afterRedraw = [];
13134
13135 H.setAnimation(animation, chart);
13136
13137 if (isHiddenChart) {
13138 chart.cloneRenderTo();
13139 }
13140
13141 // Adjust title layout (reflow multiline text)
13142 chart.layOutTitles();
13143
13144 // link stacked series
13145 while (i--) {
13146 serie = series[i];
13147
13148 if (serie.options.stacking) {
13149 hasStackedSeries = true;
13150
13151 if (serie.isDirty) {
13152 hasDirtyStacks = true;
13153 break;
13154 }
13155 }
13156 }
13157 if (hasDirtyStacks) { // mark others as dirty
13158 i = seriesLength;
13159 while (i--) {
13160 serie = series[i];
13161 if (serie.options.stacking) {
13162 serie.isDirty = true;
13163 }
13164 }
13165 }
13166
13167 // Handle updated data in the series
13168 each(series, function(serie) {
13169 if (serie.isDirty) {
13170 if (serie.options.legendType === 'point') {
13171 if (serie.updateTotals) {
13172 serie.updateTotals();
13173 }
13174 redrawLegend = true;
13175 }
13176 }
13177 if (serie.isDirtyData) {
13178 fireEvent(serie, 'updatedData');
13179 }
13180 });
13181
13182 // handle added or removed series
13183 if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed
13184 // draw legend graphics
13185 legend.render();
13186
13187 chart.isDirtyLegend = false;
13188 }
13189
13190 // reset stacks
13191 if (hasStackedSeries) {
13192 chart.getStacks();
13193 }
13194
13195
13196 if (hasCartesianSeries) {
13197 // set axes scales
13198 each(axes, function(axis) {
13199 axis.updateNames();
13200 axis.setScale();
13201 });
13202 }
13203
13204 chart.getMargins(); // #3098
13205
13206 if (hasCartesianSeries) {
13207 // If one axis is dirty, all axes must be redrawn (#792, #2169)
13208 each(axes, function(axis) {
13209 if (axis.isDirty) {
13210 isDirtyBox = true;
13211 }
13212 });
13213
13214 // redraw axes
13215 each(axes, function(axis) {
13216
13217 // Fire 'afterSetExtremes' only if extremes are set
13218 var key = axis.min + ',' + axis.max;
13219 if (axis.extKey !== key) { // #821, #4452
13220 axis.extKey = key;
13221 afterRedraw.push(function() { // prevent a recursive call to chart.redraw() (#1119)
13222 fireEvent(axis, 'afterSetExtremes', extend(axis.eventArgs, axis.getExtremes())); // #747, #751
13223 delete axis.eventArgs;
13224 });
13225 }
13226 if (isDirtyBox || hasStackedSeries) {
13227 axis.redraw();
13228 }
13229 });
13230 }
13231
13232 // the plot areas size has changed
13233 if (isDirtyBox) {
13234 chart.drawChartBox();
13235 }
13236
13237
13238 // redraw affected series
13239 each(series, function(serie) {
13240 if (serie.isDirty && serie.visible &&
13241 (!serie.isCartesian || serie.xAxis)) { // issue #153
13242 serie.redraw();
13243 }
13244 });
13245
13246 // move tooltip or reset
13247 if (pointer) {
13248 pointer.reset(true);
13249 }
13250
13251 // redraw if canvas
13252 renderer.draw();
13253
13254 // fire the event
13255 fireEvent(chart, 'redraw');
13256
13257 if (isHiddenChart) {
13258 chart.cloneRenderTo(true);
13259 }
13260
13261 // Fire callbacks that are put on hold until after the redraw
13262 each(afterRedraw, function(callback) {
13263 callback.call();
13264 });
13265 },
13266
13267 /**
13268 * Get an axis, series or point object by id.
13269 * @param id {String} The id as given in the configuration options
13270 */
13271 get: function(id) {
13272 var chart = this,
13273 axes = chart.axes,
13274 series = chart.series;
13275
13276 var i,
13277 j,
13278 points;
13279
13280 // search axes
13281 for (i = 0; i < axes.length; i++) {
13282 if (axes[i].options.id === id) {
13283 return axes[i];
13284 }
13285 }
13286
13287 // search series
13288 for (i = 0; i < series.length; i++) {
13289 if (series[i].options.id === id) {
13290 return series[i];
13291 }
13292 }
13293
13294 // search points
13295 for (i = 0; i < series.length; i++) {
13296 points = series[i].points || [];
13297 for (j = 0; j < points.length; j++) {
13298 if (points[j].id === id) {
13299 return points[j];
13300 }
13301 }
13302 }
13303 return null;
13304 },
13305
13306 /**
13307 * Create the Axis instances based on the config options
13308 */
13309 getAxes: function() {
13310 var chart = this,
13311 options = this.options,
13312 xAxisOptions = options.xAxis = splat(options.xAxis || {}),
13313 yAxisOptions = options.yAxis = splat(options.yAxis || {}),
13314 optionsArray;
13315
13316 // make sure the options are arrays and add some members
13317 each(xAxisOptions, function(axis, i) {
13318 axis.index = i;
13319 axis.isX = true;
13320 });
13321
13322 each(yAxisOptions, function(axis, i) {
13323 axis.index = i;
13324 });
13325
13326 // concatenate all axis options into one array
13327 optionsArray = xAxisOptions.concat(yAxisOptions);
13328
13329 each(optionsArray, function(axisOptions) {
13330 new Axis(chart, axisOptions); // eslint-disable-line no-new
13331 });
13332 },
13333
13334
13335 /**
13336 * Get the currently selected points from all series
13337 */
13338 getSelectedPoints: function() {
13339 var points = [];
13340 each(this.series, function(serie) {
13341 points = points.concat(grep(serie.points || [], function(point) {
13342 return point.selected;
13343 }));
13344 });
13345 return points;
13346 },
13347
13348 /**
13349 * Get the currently selected series
13350 */
13351 getSelectedSeries: function() {
13352 return grep(this.series, function(serie) {
13353 return serie.selected;
13354 });
13355 },
13356
13357 /**
13358 * Show the title and subtitle of the chart
13359 *
13360 * @param titleOptions {Object} New title options
13361 * @param subtitleOptions {Object} New subtitle options
13362 *
13363 */
13364 setTitle: function(titleOptions, subtitleOptions, redraw) {
13365 var chart = this,
13366 options = chart.options,
13367 chartTitleOptions,
13368 chartSubtitleOptions;
13369
13370 chartTitleOptions = options.title = merge(options.title, titleOptions);
13371 chartSubtitleOptions = options.subtitle = merge(options.subtitle, subtitleOptions);
13372
13373 // add title and subtitle
13374 each([
13375 ['title', titleOptions, chartTitleOptions],
13376 ['subtitle', subtitleOptions, chartSubtitleOptions]
13377 ], function(arr, i) {
13378 var name = arr[0],
13379 title = chart[name],
13380 titleOptions = arr[1],
13381 chartTitleOptions = arr[2];
13382
13383 if (title && titleOptions) {
13384 chart[name] = title = title.destroy(); // remove old
13385 }
13386
13387 if (chartTitleOptions && chartTitleOptions.text && !title) {
13388 chart[name] = chart.renderer.text(
13389 chartTitleOptions.text,
13390 0,
13391 0,
13392 chartTitleOptions.useHTML
13393 )
13394 .attr({
13395 align: chartTitleOptions.align,
13396 'class': 'highcharts-' + name,
13397 zIndex: chartTitleOptions.zIndex || 4
13398 })
13399 .add();
13400
13401 // Update methods, shortcut to Chart.setTitle
13402 chart[name].update = function(o) {
13403 chart.setTitle(!i && o, i && o);
13404 };
13405
13406
13407 // Presentational
13408 chart[name].css(chartTitleOptions.style);
13409
13410
13411 }
13412 });
13413 chart.layOutTitles(redraw);
13414 },
13415
13416 /**
13417 * Lay out the chart titles and cache the full offset height for use in getMargins
13418 */
13419 layOutTitles: function(redraw) {
13420 var titleOffset = 0,
13421 requiresDirtyBox,
13422 renderer = this.renderer,
13423 spacingBox = this.spacingBox;
13424
13425 // Lay out the title and the subtitle respectively
13426 each(['title', 'subtitle'], function(key) {
13427 var title = this[key],
13428 titleOptions = this.options[key],
13429 titleSize;
13430
13431 if (title) {
13432
13433 titleSize = titleOptions.style.fontSize;
13434
13435 titleSize = renderer.fontMetrics(titleSize, title).b;
13436
13437 title
13438 .css({
13439 width: (titleOptions.width || spacingBox.width + titleOptions.widthAdjust) + 'px'
13440 })
13441 .align(extend({
13442 y: titleOffset + titleSize + (key === 'title' ? -3 : 2)
13443 }, titleOptions), false, 'spacingBox');
13444
13445 if (!titleOptions.floating && !titleOptions.verticalAlign) {
13446 titleOffset = Math.ceil(titleOffset + title.getBBox().height);
13447 }
13448 }
13449 }, this);
13450
13451 requiresDirtyBox = this.titleOffset !== titleOffset;
13452 this.titleOffset = titleOffset; // used in getMargins
13453
13454 if (!this.isDirtyBox && requiresDirtyBox) {
13455 this.isDirtyBox = requiresDirtyBox;
13456 // Redraw if necessary (#2719, #2744)
13457 if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) {
13458 this.redraw();
13459 }
13460 }
13461 },
13462
13463 /**
13464 * Get chart width and height according to options and container size
13465 */
13466 getChartSize: function() {
13467 var chart = this,
13468 optionsChart = chart.options.chart,
13469 widthOption = optionsChart.width,
13470 heightOption = optionsChart.height,
13471 renderTo = chart.renderToClone || chart.renderTo;
13472
13473 // Get inner width and height
13474 if (!defined(widthOption)) {
13475 chart.containerWidth = getStyle(renderTo, 'width');
13476 }
13477 if (!defined(heightOption)) {
13478 chart.containerHeight = getStyle(renderTo, 'height');
13479 }
13480
13481 chart.chartWidth = Math.max(0, widthOption || chart.containerWidth || 600); // #1393, 1460
13482 chart.chartHeight = Math.max(0, pick(heightOption,
13483 // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7:
13484 chart.containerHeight > 19 ? chart.containerHeight : 400));
13485 },
13486
13487 /**
13488 * Create a clone of the chart's renderTo div and place it outside the viewport to allow
13489 * size computation on chart.render and chart.redraw
13490 */
13491 cloneRenderTo: function(revert) {
13492 var clone = this.renderToClone,
13493 container = this.container;
13494
13495 // Destroy the clone and bring the container back to the real renderTo div
13496 if (revert) {
13497 if (clone) {
13498 while (clone.childNodes.length) { // #5231
13499 this.renderTo.appendChild(clone.firstChild);
13500 }
13501 discardElement(clone);
13502 delete this.renderToClone;
13503 }
13504
13505 // Set up the clone
13506 } else {
13507 if (container && container.parentNode === this.renderTo) {
13508 this.renderTo.removeChild(container); // do not clone this
13509 }
13510 this.renderToClone = clone = this.renderTo.cloneNode(0);
13511 css(clone, {
13512 position: 'absolute',
13513 top: '-9999px',
13514 display: 'block' // #833
13515 });
13516 if (clone.style.setProperty) { // #2631
13517 clone.style.setProperty('display', 'block', 'important');
13518 }
13519 doc.body.appendChild(clone);
13520 if (container) {
13521 clone.appendChild(container);
13522 }
13523 }
13524 },
13525
13526 /**
13527 * Setter for the chart class name
13528 */
13529 setClassName: function(className) {
13530 this.container.className = 'highcharts-container ' + (className || '');
13531 },
13532
13533 /**
13534 * Get the containing element, determine the size and create the inner container
13535 * div to hold the chart
13536 */
13537 getContainer: function() {
13538 var chart = this,
13539 container,
13540 options = chart.options,
13541 optionsChart = options.chart,
13542 chartWidth,
13543 chartHeight,
13544 renderTo = chart.renderTo,
13545 indexAttrName = 'data-highcharts-chart',
13546 oldChartIndex,
13547 Ren,
13548 containerId = 'highcharts-' + H.idCounter++,
13549 containerStyle,
13550 key;
13551
13552 if (!renderTo) {
13553 chart.renderTo = renderTo = optionsChart.renderTo;
13554 }
13555
13556 if (isString(renderTo)) {
13557 chart.renderTo = renderTo = doc.getElementById(renderTo);
13558 }
13559
13560 // Display an error if the renderTo is wrong
13561 if (!renderTo) {
13562 error(13, true);
13563 }
13564
13565 // If the container already holds a chart, destroy it. The check for hasRendered is there
13566 // because web pages that are saved to disk from the browser, will preserve the data-highcharts-chart
13567 // attribute and the SVG contents, but not an interactive chart. So in this case,
13568 // charts[oldChartIndex] will point to the wrong chart if any (#2609).
13569 oldChartIndex = pInt(attr(renderTo, indexAttrName));
13570 if (isNumber(oldChartIndex) && charts[oldChartIndex] && charts[oldChartIndex].hasRendered) {
13571 charts[oldChartIndex].destroy();
13572 }
13573
13574 // Make a reference to the chart from the div
13575 attr(renderTo, indexAttrName, chart.index);
13576
13577 // remove previous chart
13578 renderTo.innerHTML = '';
13579
13580 // If the container doesn't have an offsetWidth, it has or is a child of a node
13581 // that has display:none. We need to temporarily move it out to a visible
13582 // state to determine the size, else the legend and tooltips won't render
13583 // properly. The allowClone option is used in sparklines as a micro optimization,
13584 // saving about 1-2 ms each chart.
13585 if (!optionsChart.skipClone && !renderTo.offsetWidth) {
13586 chart.cloneRenderTo();
13587 }
13588
13589 // get the width and height
13590 chart.getChartSize();
13591 chartWidth = chart.chartWidth;
13592 chartHeight = chart.chartHeight;
13593
13594 // Create the inner container
13595
13596 containerStyle = extend({
13597 position: 'relative',
13598 overflow: 'hidden', // needed for context menu (avoid scrollbars) and
13599 // content overflow in IE
13600 width: chartWidth + 'px',
13601 height: chartHeight + 'px',
13602 textAlign: 'left',
13603 lineHeight: 'normal', // #427
13604 zIndex: 0, // #1072
13605 '-webkit-tap-highlight-color': 'rgba(0,0,0,0)'
13606 });
13607
13608 chart.container = container = createElement('div', {
13609 id: containerId
13610 },
13611 containerStyle,
13612 chart.renderToClone || renderTo
13613 );
13614
13615 // cache the cursor (#1650)
13616 chart._cursor = container.style.cursor;
13617
13618 // Initialize the renderer
13619 Ren = H[optionsChart.renderer] || Renderer;
13620 chart.renderer = new Ren(
13621 container,
13622 chartWidth,
13623 chartHeight,
13624 null,
13625 optionsChart.forExport,
13626 options.exporting && options.exporting.allowHTML
13627 );
13628
13629
13630 chart.setClassName(optionsChart.className);
13631
13632 chart.renderer.setStyle(optionsChart.style);
13633
13634
13635 // Add a reference to the charts index
13636 chart.renderer.chartIndex = chart.index;
13637 },
13638
13639 /**
13640 * Calculate margins by rendering axis labels in a preliminary position. Title,
13641 * subtitle and legend have already been rendered at this stage, but will be
13642 * moved into their final positions
13643 */
13644 getMargins: function(skipAxes) {
13645 var chart = this,
13646 spacing = chart.spacing,
13647 margin = chart.margin,
13648 titleOffset = chart.titleOffset;
13649
13650 chart.resetMargins();
13651
13652 // Adjust for title and subtitle
13653 if (titleOffset && !defined(margin[0])) {
13654 chart.plotTop = Math.max(chart.plotTop, titleOffset + chart.options.title.margin + spacing[0]);
13655 }
13656
13657 // Adjust for legend
13658 if (chart.legend.display) {
13659 chart.legend.adjustMargins(margin, spacing);
13660 }
13661
13662 // adjust for scroller
13663 if (chart.extraBottomMargin) {
13664 chart.marginBottom += chart.extraBottomMargin;
13665 }
13666 if (chart.extraTopMargin) {
13667 chart.plotTop += chart.extraTopMargin;
13668 }
13669 if (!skipAxes) {
13670 this.getAxisMargins();
13671 }
13672 },
13673
13674 getAxisMargins: function() {
13675
13676 var chart = this,
13677 axisOffset = chart.axisOffset = [0, 0, 0, 0], // top, right, bottom, left
13678 margin = chart.margin;
13679
13680 // pre-render axes to get labels offset width
13681 if (chart.hasCartesianSeries) {
13682 each(chart.axes, function(axis) {
13683 if (axis.visible) {
13684 axis.getOffset();
13685 }
13686 });
13687 }
13688
13689 // Add the axis offsets
13690 each(marginNames, function(m, side) {
13691 if (!defined(margin[side])) {
13692 chart[m] += axisOffset[side];
13693 }
13694 });
13695
13696 chart.setChartSize();
13697
13698 },
13699
13700 /**
13701 * Resize the chart to its container if size is not explicitly set
13702 */
13703 reflow: function(e) {
13704 var chart = this,
13705 optionsChart = chart.options.chart,
13706 renderTo = chart.renderTo,
13707 hasUserWidth = defined(optionsChart.width),
13708 width = optionsChart.width || getStyle(renderTo, 'width'),
13709 height = optionsChart.height || getStyle(renderTo, 'height'),
13710 target = e ? e.target : win;
13711
13712 // Width and height checks for display:none. Target is doc in IE8 and Opera,
13713 // win in Firefox, Chrome and IE9.
13714 if (!hasUserWidth && !chart.isPrinting && width && height && (target === win || target === doc)) { // #1093
13715 if (width !== chart.containerWidth || height !== chart.containerHeight) {
13716 clearTimeout(chart.reflowTimeout);
13717 // When called from window.resize, e is set, else it's called directly (#2224)
13718 chart.reflowTimeout = syncTimeout(function() {
13719 if (chart.container) { // It may have been destroyed in the meantime (#1257)
13720 chart.setSize(undefined, undefined, false);
13721 }
13722 }, e ? 100 : 0);
13723 }
13724 chart.containerWidth = width;
13725 chart.containerHeight = height;
13726 }
13727 },
13728
13729 /**
13730 * Add the event handlers necessary for auto resizing
13731 */
13732 initReflow: function() {
13733 var chart = this,
13734 reflow = function(e) {
13735 chart.reflow(e);
13736 };
13737
13738
13739 addEvent(win, 'resize', reflow);
13740 addEvent(chart, 'destroy', function() {
13741 removeEvent(win, 'resize', reflow);
13742 });
13743 },
13744
13745 /**
13746 * Resize the chart to a given width and height
13747 * @param {Number} width
13748 * @param {Number} height
13749 * @param {Object|Boolean} animation
13750 */
13751 setSize: function(width, height, animation) {
13752 var chart = this,
13753 renderer = chart.renderer,
13754 globalAnimation;
13755
13756 // Handle the isResizing counter
13757 chart.isResizing += 1;
13758
13759 // set the animation for the current process
13760 H.setAnimation(animation, chart);
13761
13762 chart.oldChartHeight = chart.chartHeight;
13763 chart.oldChartWidth = chart.chartWidth;
13764 if (width !== undefined) {
13765 chart.options.chart.width = width;
13766 }
13767 if (height !== undefined) {
13768 chart.options.chart.height = height;
13769 }
13770 chart.getChartSize();
13771
13772 // Resize the container with the global animation applied if enabled (#2503)
13773
13774 globalAnimation = renderer.globalAnimation;
13775 (globalAnimation ? animate : css)(chart.container, {
13776 width: chart.chartWidth + 'px',
13777 height: chart.chartHeight + 'px'
13778 }, globalAnimation);
13779
13780
13781 chart.setChartSize(true);
13782 renderer.setSize(chart.chartWidth, chart.chartHeight, animation);
13783
13784 // handle axes
13785 each(chart.axes, function(axis) {
13786 axis.isDirty = true;
13787 axis.setScale();
13788 });
13789
13790 // make sure non-cartesian series are also handled
13791 each(chart.series, function(serie) {
13792 serie.isDirty = true;
13793 });
13794
13795 chart.isDirtyLegend = true; // force legend redraw
13796 chart.isDirtyBox = true; // force redraw of plot and chart border
13797
13798 chart.layOutTitles(); // #2857
13799 chart.getMargins();
13800
13801 if (chart.setResponsive) {
13802 chart.setResponsive(false);
13803 }
13804 chart.redraw(animation);
13805
13806
13807 chart.oldChartHeight = null;
13808 fireEvent(chart, 'resize');
13809
13810 // Fire endResize and set isResizing back. If animation is disabled, fire without delay
13811 syncTimeout(function() {
13812 if (chart) {
13813 fireEvent(chart, 'endResize', null, function() {
13814 chart.isResizing -= 1;
13815 });
13816 }
13817 }, animObject(globalAnimation).duration);
13818 },
13819
13820 /**
13821 * Set the public chart properties. This is done before and after the pre-render
13822 * to determine margin sizes
13823 */
13824 setChartSize: function(skipAxes) {
13825 var chart = this,
13826 inverted = chart.inverted,
13827 renderer = chart.renderer,
13828 chartWidth = chart.chartWidth,
13829 chartHeight = chart.chartHeight,
13830 optionsChart = chart.options.chart,
13831 spacing = chart.spacing,
13832 clipOffset = chart.clipOffset,
13833 clipX,
13834 clipY,
13835 plotLeft,
13836 plotTop,
13837 plotWidth,
13838 plotHeight,
13839 plotBorderWidth;
13840
13841 chart.plotLeft = plotLeft = Math.round(chart.plotLeft);
13842 chart.plotTop = plotTop = Math.round(chart.plotTop);
13843 chart.plotWidth = plotWidth = Math.max(0, Math.round(chartWidth - plotLeft - chart.marginRight));
13844 chart.plotHeight = plotHeight = Math.max(0, Math.round(chartHeight - plotTop - chart.marginBottom));
13845
13846 chart.plotSizeX = inverted ? plotHeight : plotWidth;
13847 chart.plotSizeY = inverted ? plotWidth : plotHeight;
13848
13849 chart.plotBorderWidth = optionsChart.plotBorderWidth || 0;
13850
13851 // Set boxes used for alignment
13852 chart.spacingBox = renderer.spacingBox = {
13853 x: spacing[3],
13854 y: spacing[0],
13855 width: chartWidth - spacing[3] - spacing[1],
13856 height: chartHeight - spacing[0] - spacing[2]
13857 };
13858 chart.plotBox = renderer.plotBox = {
13859 x: plotLeft,
13860 y: plotTop,
13861 width: plotWidth,
13862 height: plotHeight
13863 };
13864
13865 plotBorderWidth = 2 * Math.floor(chart.plotBorderWidth / 2);
13866 clipX = Math.ceil(Math.max(plotBorderWidth, clipOffset[3]) / 2);
13867 clipY = Math.ceil(Math.max(plotBorderWidth, clipOffset[0]) / 2);
13868 chart.clipBox = {
13869 x: clipX,
13870 y: clipY,
13871 width: Math.floor(chart.plotSizeX - Math.max(plotBorderWidth, clipOffset[1]) / 2 - clipX),
13872 height: Math.max(0, Math.floor(chart.plotSizeY - Math.max(plotBorderWidth, clipOffset[2]) / 2 - clipY))
13873 };
13874
13875 if (!skipAxes) {
13876 each(chart.axes, function(axis) {
13877 axis.setAxisSize();
13878 axis.setAxisTranslation();
13879 });
13880 }
13881 },
13882
13883 /**
13884 * Initial margins before auto size margins are applied
13885 */
13886 resetMargins: function() {
13887 var chart = this,
13888 chartOptions = chart.options.chart;
13889
13890 // Create margin and spacing array
13891 each(['margin', 'spacing'], function splashArrays(target) {
13892 var value = chartOptions[target],
13893 values = isObject(value) ? value : [value, value, value, value];
13894
13895 each(['Top', 'Right', 'Bottom', 'Left'], function(sideName, side) {
13896 chart[target][side] = pick(chartOptions[target + sideName], values[side]);
13897 });
13898 });
13899
13900 // Set margin names like chart.plotTop, chart.plotLeft, chart.marginRight, chart.marginBottom.
13901 each(marginNames, function(m, side) {
13902 chart[m] = pick(chart.margin[side], chart.spacing[side]);
13903 });
13904 chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left
13905 chart.clipOffset = [0, 0, 0, 0];
13906 },
13907
13908 /**
13909 * Draw the borders and backgrounds for chart and plot area
13910 */
13911 drawChartBox: function() {
13912 var chart = this,
13913 optionsChart = chart.options.chart,
13914 renderer = chart.renderer,
13915 chartWidth = chart.chartWidth,
13916 chartHeight = chart.chartHeight,
13917 chartBackground = chart.chartBackground,
13918 plotBackground = chart.plotBackground,
13919 plotBorder = chart.plotBorder,
13920 chartBorderWidth,
13921
13922 plotBGImage = chart.plotBGImage,
13923 chartBackgroundColor = optionsChart.backgroundColor,
13924 plotBackgroundColor = optionsChart.plotBackgroundColor,
13925 plotBackgroundImage = optionsChart.plotBackgroundImage,
13926
13927 mgn,
13928 bgAttr,
13929 plotLeft = chart.plotLeft,
13930 plotTop = chart.plotTop,
13931 plotWidth = chart.plotWidth,
13932 plotHeight = chart.plotHeight,
13933 plotBox = chart.plotBox,
13934 clipRect = chart.clipRect,
13935 clipBox = chart.clipBox,
13936 verb = 'animate';
13937
13938 // Chart area
13939 if (!chartBackground) {
13940 chart.chartBackground = chartBackground = renderer.rect()
13941 .addClass('highcharts-background')
13942 .add();
13943 verb = 'attr';
13944 }
13945
13946
13947 // Presentational
13948 chartBorderWidth = optionsChart.borderWidth || 0;
13949 mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0);
13950
13951 bgAttr = {
13952 fill: chartBackgroundColor || 'none'
13953 };
13954
13955 if (chartBorderWidth || chartBackground['stroke-width']) { // #980
13956 bgAttr.stroke = optionsChart.borderColor;
13957 bgAttr['stroke-width'] = chartBorderWidth;
13958 }
13959 chartBackground
13960 .attr(bgAttr)
13961 .shadow(optionsChart.shadow);
13962
13963 chartBackground[verb]({
13964 x: mgn / 2,
13965 y: mgn / 2,
13966 width: chartWidth - mgn - chartBorderWidth % 2,
13967 height: chartHeight - mgn - chartBorderWidth % 2,
13968 r: optionsChart.borderRadius
13969 });
13970
13971 // Plot background
13972 verb = 'animate';
13973 if (!plotBackground) {
13974 verb = 'attr';
13975 chart.plotBackground = plotBackground = renderer.rect()
13976 .addClass('highcharts-plot-background')
13977 .add();
13978 }
13979 plotBackground[verb](plotBox);
13980
13981
13982 // Presentational attributes for the background
13983 plotBackground
13984 .attr({
13985 fill: plotBackgroundColor || 'none'
13986 })
13987 .shadow(optionsChart.plotShadow);
13988
13989 // Create the background image
13990 if (plotBackgroundImage) {
13991 if (!plotBGImage) {
13992 chart.plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight)
13993 .add();
13994 } else {
13995 plotBGImage.animate(plotBox);
13996 }
13997 }
13998
13999
14000 // Plot clip
14001 if (!clipRect) {
14002 chart.clipRect = renderer.clipRect(clipBox);
14003 } else {
14004 clipRect.animate({
14005 width: clipBox.width,
14006 height: clipBox.height
14007 });
14008 }
14009
14010 // Plot area border
14011 verb = 'animate';
14012 if (!plotBorder) {
14013 verb = 'attr';
14014 chart.plotBorder = plotBorder = renderer.rect()
14015 .addClass('highcharts-plot-border')
14016 .attr({
14017 zIndex: 1 // Above the grid
14018 })
14019 .add();
14020 }
14021
14022
14023 // Presentational
14024 plotBorder.attr({
14025 stroke: optionsChart.plotBorderColor,
14026 'stroke-width': optionsChart.plotBorderWidth || 0,
14027 fill: 'none'
14028 });
14029
14030
14031 plotBorder[verb](plotBorder.crisp({
14032 x: plotLeft,
14033 y: plotTop,
14034 width: plotWidth,
14035 height: plotHeight
14036 }, -plotBorder.strokeWidth())); //#3282 plotBorder should be negative;
14037
14038 // reset
14039 chart.isDirtyBox = false;
14040 },
14041
14042 /**
14043 * Detect whether a certain chart property is needed based on inspecting its options
14044 * and series. This mainly applies to the chart.inverted property, and in extensions to
14045 * the chart.angular and chart.polar properties.
14046 */
14047 propFromSeries: function() {
14048 var chart = this,
14049 optionsChart = chart.options.chart,
14050 klass,
14051 seriesOptions = chart.options.series,
14052 i,
14053 value;
14054
14055
14056 each(['inverted', 'angular', 'polar'], function(key) {
14057
14058 // The default series type's class
14059 klass = seriesTypes[optionsChart.type || optionsChart.defaultSeriesType];
14060
14061 // Get the value from available chart-wide properties
14062 value =
14063 optionsChart[key] || // It is set in the options
14064 (klass && klass.prototype[key]); // The default series class requires it
14065
14066 // 4. Check if any the chart's series require it
14067 i = seriesOptions && seriesOptions.length;
14068 while (!value && i--) {
14069 klass = seriesTypes[seriesOptions[i].type];
14070 if (klass && klass.prototype[key]) {
14071 value = true;
14072 }
14073 }
14074
14075 // Set the chart property
14076 chart[key] = value;
14077 });
14078
14079 },
14080
14081 /**
14082 * Link two or more series together. This is done initially from Chart.render,
14083 * and after Chart.addSeries and Series.remove.
14084 */
14085 linkSeries: function() {
14086 var chart = this,
14087 chartSeries = chart.series;
14088
14089 // Reset links
14090 each(chartSeries, function(series) {
14091 series.linkedSeries.length = 0;
14092 });
14093
14094 // Apply new links
14095 each(chartSeries, function(series) {
14096 var linkedTo = series.options.linkedTo;
14097 if (isString(linkedTo)) {
14098 if (linkedTo === ':previous') {
14099 linkedTo = chart.series[series.index - 1];
14100 } else {
14101 linkedTo = chart.get(linkedTo);
14102 }
14103 if (linkedTo && linkedTo.linkedParent !== series) { // #3341 avoid mutual linking
14104 linkedTo.linkedSeries.push(series);
14105 series.linkedParent = linkedTo;
14106 series.visible = pick(series.options.visible, linkedTo.options.visible, series.visible); // #3879
14107 }
14108 }
14109 });
14110 },
14111
14112 /**
14113 * Render series for the chart
14114 */
14115 renderSeries: function() {
14116 each(this.series, function(serie) {
14117 serie.translate();
14118 serie.render();
14119 });
14120 },
14121
14122 /**
14123 * Render labels for the chart
14124 */
14125 renderLabels: function() {
14126 var chart = this,
14127 labels = chart.options.labels;
14128 if (labels.items) {
14129 each(labels.items, function(label) {
14130 var style = extend(labels.style, label.style),
14131 x = pInt(style.left) + chart.plotLeft,
14132 y = pInt(style.top) + chart.plotTop + 12;
14133
14134 // delete to prevent rewriting in IE
14135 delete style.left;
14136 delete style.top;
14137
14138 chart.renderer.text(
14139 label.html,
14140 x,
14141 y
14142 )
14143 .attr({
14144 zIndex: 2
14145 })
14146 .css(style)
14147 .add();
14148
14149 });
14150 }
14151 },
14152
14153 /**
14154 * Render all graphics for the chart
14155 */
14156 render: function() {
14157 var chart = this,
14158 axes = chart.axes,
14159 renderer = chart.renderer,
14160 options = chart.options,
14161 tempWidth,
14162 tempHeight,
14163 redoHorizontal,
14164 redoVertical;
14165
14166 // Title
14167 chart.setTitle();
14168
14169
14170 // Legend
14171 chart.legend = new Legend(chart, options.legend);
14172
14173 // Get stacks
14174 if (chart.getStacks) {
14175 chart.getStacks();
14176 }
14177
14178 // Get chart margins
14179 chart.getMargins(true);
14180 chart.setChartSize();
14181
14182 // Record preliminary dimensions for later comparison
14183 tempWidth = chart.plotWidth;
14184 tempHeight = chart.plotHeight = chart.plotHeight - 21; // 21 is the most common correction for X axis labels
14185
14186 // Get margins by pre-rendering axes
14187 each(axes, function(axis) {
14188 axis.setScale();
14189 });
14190 chart.getAxisMargins();
14191
14192 // If the plot area size has changed significantly, calculate tick positions again
14193 redoHorizontal = tempWidth / chart.plotWidth > 1.1;
14194 redoVertical = tempHeight / chart.plotHeight > 1.05; // Height is more sensitive
14195
14196 if (redoHorizontal || redoVertical) {
14197
14198 each(axes, function(axis) {
14199 if ((axis.horiz && redoHorizontal) || (!axis.horiz && redoVertical)) {
14200 axis.setTickInterval(true); // update to reflect the new margins
14201 }
14202 });
14203 chart.getMargins(); // second pass to check for new labels
14204 }
14205
14206 // Draw the borders and backgrounds
14207 chart.drawChartBox();
14208
14209
14210 // Axes
14211 if (chart.hasCartesianSeries) {
14212 each(axes, function(axis) {
14213 if (axis.visible) {
14214 axis.render();
14215 }
14216 });
14217 }
14218
14219 // The series
14220 if (!chart.seriesGroup) {
14221 chart.seriesGroup = renderer.g('series-group')
14222 .attr({
14223 zIndex: 3
14224 })
14225 .add();
14226 }
14227 chart.renderSeries();
14228
14229 // Labels
14230 chart.renderLabels();
14231
14232 // Credits
14233 chart.addCredits();
14234
14235 // Handle responsiveness
14236 if (chart.setResponsive) {
14237 chart.setResponsive();
14238 }
14239
14240 // Set flag
14241 chart.hasRendered = true;
14242
14243 },
14244
14245 /**
14246 * Show chart credits based on config options
14247 */
14248 addCredits: function(credits) {
14249 var chart = this;
14250
14251 credits = merge(true, this.options.credits, credits);
14252 if (credits.enabled && !this.credits) {
14253 this.credits = this.renderer.text(
14254 credits.text + (this.mapCredits || ''),
14255 0,
14256 0
14257 )
14258 .addClass('highcharts-credits')
14259 .on('click', function() {
14260 if (credits.href) {
14261 win.location.href = credits.href;
14262 }
14263 })
14264 .attr({
14265 align: credits.position.align,
14266 zIndex: 8
14267 })
14268
14269 .css(credits.style)
14270
14271 .add()
14272 .align(credits.position);
14273
14274 // Dynamically update
14275 this.credits.update = function(options) {
14276 chart.credits = chart.credits.destroy();
14277 chart.addCredits(options);
14278 };
14279 }
14280 },
14281
14282 /**
14283 * Clean up memory usage
14284 */
14285 destroy: function() {
14286 var chart = this,
14287 axes = chart.axes,
14288 series = chart.series,
14289 container = chart.container,
14290 i,
14291 parentNode = container && container.parentNode;
14292
14293 // fire the chart.destoy event
14294 fireEvent(chart, 'destroy');
14295
14296 // Delete the chart from charts lookup array
14297 charts[chart.index] = undefined;
14298 H.chartCount--;
14299 chart.renderTo.removeAttribute('data-highcharts-chart');
14300
14301 // remove events
14302 removeEvent(chart);
14303
14304 // ==== Destroy collections:
14305 // Destroy axes
14306 i = axes.length;
14307 while (i--) {
14308 axes[i] = axes[i].destroy();
14309 }
14310
14311 // Destroy scroller & scroller series before destroying base series
14312 if (this.scroller && this.scroller.destroy) {
14313 this.scroller.destroy();
14314 }
14315
14316 // Destroy each series
14317 i = series.length;
14318 while (i--) {
14319 series[i] = series[i].destroy();
14320 }
14321
14322 // ==== Destroy chart properties:
14323 each(['title', 'subtitle', 'chartBackground', 'plotBackground', 'plotBGImage',
14324 'plotBorder', 'seriesGroup', 'clipRect', 'credits', 'pointer',
14325 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', 'renderer'
14326 ], function(name) {
14327 var prop = chart[name];
14328
14329 if (prop && prop.destroy) {
14330 chart[name] = prop.destroy();
14331 }
14332 });
14333
14334 // remove container and all SVG
14335 if (container) { // can break in IE when destroyed before finished loading
14336 container.innerHTML = '';
14337 removeEvent(container);
14338 if (parentNode) {
14339 discardElement(container);
14340 }
14341
14342 }
14343
14344 // clean it all up
14345 for (i in chart) {
14346 delete chart[i];
14347 }
14348
14349 },
14350
14351
14352 /**
14353 * VML namespaces can't be added until after complete. Listening
14354 * for Perini's doScroll hack is not enough.
14355 */
14356 isReadyToRender: function() {
14357 var chart = this;
14358
14359 // Note: win == win.top is required
14360 if ((!svg && (win == win.top && doc.readyState !== 'complete'))) { // eslint-disable-line eqeqeq
14361 doc.attachEvent('onreadystatechange', function() {
14362 doc.detachEvent('onreadystatechange', chart.firstRender);
14363 if (doc.readyState === 'complete') {
14364 chart.firstRender();
14365 }
14366 });
14367 return false;
14368 }
14369 return true;
14370 },
14371
14372 /**
14373 * Prepare for first rendering after all data are loaded
14374 */
14375 firstRender: function() {
14376 var chart = this,
14377 options = chart.options;
14378
14379 // Check whether the chart is ready to render
14380 if (!chart.isReadyToRender()) {
14381 return;
14382 }
14383
14384 // Create the container
14385 chart.getContainer();
14386
14387 // Run an early event after the container and renderer are established
14388 fireEvent(chart, 'init');
14389
14390
14391 chart.resetMargins();
14392 chart.setChartSize();
14393
14394 // Set the common chart properties (mainly invert) from the given series
14395 chart.propFromSeries();
14396
14397 // get axes
14398 chart.getAxes();
14399
14400 // Initialize the series
14401 each(options.series || [], function(serieOptions) {
14402 chart.initSeries(serieOptions);
14403 });
14404
14405 chart.linkSeries();
14406
14407 // Run an event after axes and series are initialized, but before render. At this stage,
14408 // the series data is indexed and cached in the xData and yData arrays, so we can access
14409 // those before rendering. Used in Highstock.
14410 fireEvent(chart, 'beforeRender');
14411
14412 // depends on inverted and on margins being set
14413 if (Pointer) {
14414 chart.pointer = new Pointer(chart, options);
14415 }
14416
14417 chart.render();
14418
14419 // add canvas
14420 chart.renderer.draw();
14421
14422 // Fire the load event if there are no external images
14423 if (!chart.renderer.imgCount && chart.onload) {
14424 chart.onload();
14425 }
14426
14427 // If the chart was rendered outside the top container, put it back in (#3679)
14428 chart.cloneRenderTo(true);
14429
14430 },
14431
14432 /**
14433 * On chart load
14434 */
14435 onload: function() {
14436
14437 // Run callbacks
14438 each([this.callback].concat(this.callbacks), function(fn) {
14439 if (fn && this.index !== undefined) { // Chart destroyed in its own callback (#3600)
14440 fn.apply(this, [this]);
14441 }
14442 }, this);
14443
14444 fireEvent(this, 'load');
14445
14446 // Set up auto resize
14447 this.initReflow();
14448
14449 // Don't run again
14450 this.onload = null;
14451 }
14452
14453 }; // end Chart
14454
14455 }(Highcharts));
14456 (function(H) {
14457 /**
14458 * (c) 2010-2016 Torstein Honsi
14459 *
14460 * License: www.highcharts.com/license
14461 */
14462 'use strict';
14463 var Point,
14464
14465 each = H.each,
14466 extend = H.extend,
14467 erase = H.erase,
14468 fireEvent = H.fireEvent,
14469 format = H.format,
14470 isArray = H.isArray,
14471 isNumber = H.isNumber,
14472 pick = H.pick,
14473 removeEvent = H.removeEvent;
14474
14475 /**
14476 * The Point object and prototype. Inheritable and used as base for PiePoint
14477 */
14478 Point = H.Point = function() {};
14479 Point.prototype = {
14480
14481 /**
14482 * Initialize the point
14483 * @param {Object} series The series object containing this point
14484 * @param {Object} options The data in either number, array or object format
14485 */
14486 init: function(series, options, x) {
14487
14488 var point = this,
14489 colors,
14490 colorCount = series.chart.options.chart.colorCount,
14491 colorIndex;
14492
14493 point.series = series;
14494
14495 point.color = series.color; // #3445
14496
14497 point.applyOptions(options, x);
14498
14499 if (series.options.colorByPoint) {
14500
14501 colors = series.options.colors || series.chart.options.colors;
14502 point.color = point.color || colors[series.colorCounter];
14503 colorCount = colors.length;
14504
14505 colorIndex = series.colorCounter;
14506 series.colorCounter++;
14507 // loop back to zero
14508 if (series.colorCounter === colorCount) {
14509 series.colorCounter = 0;
14510 }
14511 } else {
14512 colorIndex = series.colorIndex;
14513 }
14514 point.colorIndex = pick(point.colorIndex, colorIndex);
14515
14516 series.chart.pointCount++;
14517 return point;
14518 },
14519 /**
14520 * Apply the options containing the x and y data and possible some extra properties.
14521 * This is called on point init or from point.update.
14522 *
14523 * @param {Object} options
14524 */
14525 applyOptions: function(options, x) {
14526 var point = this,
14527 series = point.series,
14528 pointValKey = series.options.pointValKey || series.pointValKey;
14529
14530 options = Point.prototype.optionsToObject.call(this, options);
14531
14532 // copy options directly to point
14533 extend(point, options);
14534 point.options = point.options ? extend(point.options, options) : options;
14535
14536 // Since options are copied into the Point instance, some accidental options must be shielded (#5681)
14537 if (options.group) {
14538 delete point.group;
14539 }
14540
14541 // For higher dimension series types. For instance, for ranges, point.y is mapped to point.low.
14542 if (pointValKey) {
14543 point.y = point[pointValKey];
14544 }
14545 point.isNull = pick(
14546 point.isValid && !point.isValid(),
14547 point.x === null || !isNumber(point.y, true)
14548 ); // #3571, check for NaN
14549
14550 // If no x is set by now, get auto incremented value. All points must have an
14551 // x value, however the y value can be null to create a gap in the series
14552 if ('name' in point && x === undefined && series.xAxis && series.xAxis.hasNames) {
14553 point.x = series.xAxis.nameToX(point);
14554 }
14555 if (point.x === undefined && series) {
14556 if (x === undefined) {
14557 point.x = series.autoIncrement(point);
14558 } else {
14559 point.x = x;
14560 }
14561 }
14562
14563 return point;
14564 },
14565
14566 /**
14567 * Transform number or array configs into objects
14568 */
14569 optionsToObject: function(options) {
14570 var ret = {},
14571 series = this.series,
14572 keys = series.options.keys,
14573 pointArrayMap = keys || series.pointArrayMap || ['y'],
14574 valueCount = pointArrayMap.length,
14575 firstItemType,
14576 i = 0,
14577 j = 0;
14578
14579 if (isNumber(options) || options === null) {
14580 ret[pointArrayMap[0]] = options;
14581
14582 } else if (isArray(options)) {
14583 // with leading x value
14584 if (!keys && options.length > valueCount) {
14585 firstItemType = typeof options[0];
14586 if (firstItemType === 'string') {
14587 ret.name = options[0];
14588 } else if (firstItemType === 'number') {
14589 ret.x = options[0];
14590 }
14591 i++;
14592 }
14593 while (j < valueCount) {
14594 if (!keys || options[i] !== undefined) { // Skip undefined positions for keys
14595 ret[pointArrayMap[j]] = options[i];
14596 }
14597 i++;
14598 j++;
14599 }
14600 } else if (typeof options === 'object') {
14601 ret = options;
14602
14603 // This is the fastest way to detect if there are individual point dataLabels that need
14604 // to be considered in drawDataLabels. These can only occur in object configs.
14605 if (options.dataLabels) {
14606 series._hasPointLabels = true;
14607 }
14608
14609 // Same approach as above for markers
14610 if (options.marker) {
14611 series._hasPointMarkers = true;
14612 }
14613 }
14614 return ret;
14615 },
14616
14617 /**
14618 * Get the CSS class names for individual points
14619 * @returns {String} The class name
14620 */
14621 getClassName: function() {
14622 return 'highcharts-point' +
14623 (this.selected ? ' highcharts-point-select' : '') +
14624 (this.negative ? ' highcharts-negative' : '') +
14625 (this.colorIndex !== undefined ? ' highcharts-color-' + this.colorIndex : '') +
14626 (this.options.className ? ' ' + this.options.className : '');
14627 },
14628
14629 /**
14630 * Return the zone that the point belongs to
14631 */
14632 getZone: function() {
14633 var series = this.series,
14634 zones = series.zones,
14635 zoneAxis = series.zoneAxis || 'y',
14636 i = 0,
14637 zone;
14638
14639 zone = zones[i];
14640 while (this[zoneAxis] >= zone.value) {
14641 zone = zones[++i];
14642 }
14643
14644 if (zone && zone.color && !this.options.color) {
14645 this.color = zone.color;
14646 }
14647
14648 return zone;
14649 },
14650
14651 /**
14652 * Destroy a point to clear memory. Its reference still stays in series.data.
14653 */
14654 destroy: function() {
14655 var point = this,
14656 series = point.series,
14657 chart = series.chart,
14658 hoverPoints = chart.hoverPoints,
14659 prop;
14660
14661 chart.pointCount--;
14662
14663 if (hoverPoints) {
14664 point.setState();
14665 erase(hoverPoints, point);
14666 if (!hoverPoints.length) {
14667 chart.hoverPoints = null;
14668 }
14669
14670 }
14671 if (point === chart.hoverPoint) {
14672 point.onMouseOut();
14673 }
14674
14675 // remove all events
14676 if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive
14677 removeEvent(point);
14678 point.destroyElements();
14679 }
14680
14681 if (point.legendItem) { // pies have legend items
14682 chart.legend.destroyItem(point);
14683 }
14684
14685 for (prop in point) {
14686 point[prop] = null;
14687 }
14688
14689
14690 },
14691
14692 /**
14693 * Destroy SVG elements associated with the point
14694 */
14695 destroyElements: function() {
14696 var point = this,
14697 props = ['graphic', 'dataLabel', 'dataLabelUpper', 'connector', 'shadowGroup'],
14698 prop,
14699 i = 6;
14700 while (i--) {
14701 prop = props[i];
14702 if (point[prop]) {
14703 point[prop] = point[prop].destroy();
14704 }
14705 }
14706 },
14707
14708 /**
14709 * Return the configuration hash needed for the data label and tooltip formatters
14710 */
14711 getLabelConfig: function() {
14712 return {
14713 x: this.category,
14714 y: this.y,
14715 color: this.color,
14716 key: this.name || this.category,
14717 series: this.series,
14718 point: this,
14719 percentage: this.percentage,
14720 total: this.total || this.stackTotal
14721 };
14722 },
14723
14724 /**
14725 * Extendable method for formatting each point's tooltip line
14726 *
14727 * @return {String} A string to be concatenated in to the common tooltip text
14728 */
14729 tooltipFormatter: function(pointFormat) {
14730
14731 // Insert options for valueDecimals, valuePrefix, and valueSuffix
14732 var series = this.series,
14733 seriesTooltipOptions = series.tooltipOptions,
14734 valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''),
14735 valuePrefix = seriesTooltipOptions.valuePrefix || '',
14736 valueSuffix = seriesTooltipOptions.valueSuffix || '';
14737
14738 // Loop over the point array map and replace unformatted values with sprintf formatting markup
14739 each(series.pointArrayMap || ['y'], function(key) {
14740 key = '{point.' + key; // without the closing bracket
14741 if (valuePrefix || valueSuffix) {
14742 pointFormat = pointFormat.replace(key + '}', valuePrefix + key + '}' + valueSuffix);
14743 }
14744 pointFormat = pointFormat.replace(key + '}', key + ':,.' + valueDecimals + 'f}');
14745 });
14746
14747 return format(pointFormat, {
14748 point: this,
14749 series: this.series
14750 });
14751 },
14752
14753 /**
14754 * Fire an event on the Point object.
14755 * @param {String} eventType
14756 * @param {Object} eventArgs Additional event arguments
14757 * @param {Function} defaultFunction Default event handler
14758 */
14759 firePointEvent: function(eventType, eventArgs, defaultFunction) {
14760 var point = this,
14761 series = this.series,
14762 seriesOptions = series.options;
14763
14764 // load event handlers on demand to save time on mouseover/out
14765 if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) {
14766 this.importEvents();
14767 }
14768
14769 // add default handler if in selection mode
14770 if (eventType === 'click' && seriesOptions.allowPointSelect) {
14771 defaultFunction = function(event) {
14772 // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera
14773 if (point.select) { // Could be destroyed by prior event handlers (#2911)
14774 point.select(null, event.ctrlKey || event.metaKey || event.shiftKey);
14775 }
14776 };
14777 }
14778
14779 fireEvent(this, eventType, eventArgs, defaultFunction);
14780 },
14781 visible: true
14782 };
14783
14784 }(Highcharts));
14785 (function(H) {
14786 /**
14787 * (c) 2010-2016 Torstein Honsi
14788 *
14789 * License: www.highcharts.com/license
14790 */
14791 'use strict';
14792 var addEvent = H.addEvent,
14793 animObject = H.animObject,
14794 arrayMax = H.arrayMax,
14795 arrayMin = H.arrayMin,
14796 correctFloat = H.correctFloat,
14797 Date = H.Date,
14798 defaultOptions = H.defaultOptions,
14799 defaultPlotOptions = H.defaultPlotOptions,
14800 defined = H.defined,
14801 each = H.each,
14802 erase = H.erase,
14803 error = H.error,
14804 extend = H.extend,
14805 fireEvent = H.fireEvent,
14806 grep = H.grep,
14807 isArray = H.isArray,
14808 isNumber = H.isNumber,
14809 isString = H.isString,
14810 LegendSymbolMixin = H.LegendSymbolMixin, // @todo add as a requirement
14811 merge = H.merge,
14812 pick = H.pick,
14813 Point = H.Point, // @todo add as a requirement
14814 removeEvent = H.removeEvent,
14815 splat = H.splat,
14816 stableSort = H.stableSort,
14817 SVGElement = H.SVGElement,
14818 syncTimeout = H.syncTimeout,
14819 win = H.win;
14820
14821 /**
14822 * @classDescription The base function which all other series types inherit from. The data in the series is stored
14823 * in various arrays.
14824 *
14825 * - First, series.options.data contains all the original config options for
14826 * each point whether added by options or methods like series.addPoint.
14827 * - Next, series.data contains those values converted to points, but in case the series data length
14828 * exceeds the cropThreshold, or if the data is grouped, series.data doesn't contain all the points. It
14829 * only contains the points that have been created on demand.
14830 * - Then there's series.points that contains all currently visible point objects. In case of cropping,
14831 * the cropped-away points are not part of this array. The series.points array starts at series.cropStart
14832 * compared to series.data and series.options.data. If however the series data is grouped, these can't
14833 * be correlated one to one.
14834 * - series.xData and series.processedXData contain clean x values, equivalent to series.data and series.points.
14835 * - series.yData and series.processedYData contain clean x values, equivalent to series.data and series.points.
14836 *
14837 * @param {Object} chart
14838 * @param {Object} options
14839 */
14840 H.Series = H.seriesType('line', null, { // base series options
14841
14842 //cursor: 'default',
14843 //dashStyle: null,
14844 //linecap: 'round',
14845 lineWidth: 2,
14846 //shadow: false,
14847
14848 allowPointSelect: false,
14849 showCheckbox: false,
14850 animation: {
14851 duration: 1000
14852 },
14853 //clip: true,
14854 //connectNulls: false,
14855 //enableMouseTracking: true,
14856 events: {},
14857 //legendIndex: 0,
14858 // stacking: null,
14859 marker: {
14860
14861 lineWidth: 0,
14862 lineColor: '#ffffff',
14863 //fillColor: null,
14864
14865 //enabled: true,
14866 //symbol: null,
14867 radius: 4,
14868 states: { // states for a single point
14869 hover: {
14870 enabled: true,
14871 radiusPlus: 2,
14872
14873 lineWidthPlus: 1
14874
14875 },
14876
14877 select: {
14878 fillColor: '#cccccc',
14879 lineColor: '#000000',
14880 lineWidth: 2
14881 }
14882
14883 }
14884 },
14885 point: {
14886 events: {}
14887 },
14888 dataLabels: {
14889 align: 'center',
14890 // defer: true,
14891 // enabled: false,
14892 formatter: function() {
14893 return this.y === null ? '' : H.numberFormat(this.y, -1);
14894 },
14895
14896 style: {
14897 fontSize: '11px',
14898 fontWeight: 'bold',
14899 color: 'contrast',
14900 textShadow: '1px 1px contrast, -1px -1px contrast, -1px 1px contrast, 1px -1px contrast'
14901 },
14902 // backgroundColor: undefined,
14903 // borderColor: undefined,
14904 // borderWidth: undefined,
14905 // shadow: false
14906
14907 verticalAlign: 'bottom', // above singular point
14908 x: 0,
14909 y: 0,
14910 // borderRadius: undefined,
14911 padding: 5
14912 },
14913 cropThreshold: 300, // draw points outside the plot area when the number of points is less than this
14914 pointRange: 0,
14915 //pointStart: 0,
14916 //pointInterval: 1,
14917 //showInLegend: null, // auto: true for standalone series, false for linked series
14918 softThreshold: true,
14919 states: { // states for the entire series
14920 hover: {
14921 //enabled: false,
14922 lineWidthPlus: 1,
14923 marker: {
14924 // lineWidth: base + 1,
14925 // radius: base + 1
14926 },
14927 halo: {
14928 size: 10,
14929
14930 opacity: 0.25
14931
14932 }
14933 },
14934 select: {
14935 marker: {}
14936 }
14937 },
14938 stickyTracking: true,
14939 //tooltip: {
14940 //pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b>'
14941 //valueDecimals: null,
14942 //xDateFormat: '%A, %b %e, %Y',
14943 //valuePrefix: '',
14944 //ySuffix: ''
14945 //}
14946 turboThreshold: 1000
14947 // zIndex: null
14948 },
14949
14950 // Prototype properties
14951 {
14952 isCartesian: true,
14953 pointClass: Point,
14954 sorted: true, // requires the data to be sorted
14955 requireSorting: true,
14956 directTouch: false,
14957 axisTypes: ['xAxis', 'yAxis'],
14958 colorCounter: 0,
14959 parallelArrays: ['x', 'y'], // each point's x and y values are stored in this.xData and this.yData
14960 coll: 'series',
14961 init: function(chart, options) {
14962 var series = this,
14963 eventType,
14964 events,
14965 chartSeries = chart.series,
14966 sortByIndex = function(a, b) {
14967 return pick(a.options.index, a._i) - pick(b.options.index, b._i);
14968 };
14969
14970 series.chart = chart;
14971 series.options = options = series.setOptions(options); // merge with plotOptions
14972 series.linkedSeries = [];
14973
14974 // bind the axes
14975 series.bindAxes();
14976
14977 // set some variables
14978 extend(series, {
14979 name: options.name,
14980 state: '',
14981 visible: options.visible !== false, // true by default
14982 selected: options.selected === true // false by default
14983 });
14984
14985 // register event listeners
14986 events = options.events;
14987 for (eventType in events) {
14988 addEvent(series, eventType, events[eventType]);
14989 }
14990 if (
14991 (events && events.click) ||
14992 (options.point && options.point.events && options.point.events.click) ||
14993 options.allowPointSelect
14994 ) {
14995 chart.runTrackerClick = true;
14996 }
14997
14998 series.getColor();
14999 series.getSymbol();
15000
15001 // Set the data
15002 each(series.parallelArrays, function(key) {
15003 series[key + 'Data'] = [];
15004 });
15005 series.setData(options.data, false);
15006
15007 // Mark cartesian
15008 if (series.isCartesian) {
15009 chart.hasCartesianSeries = true;
15010 }
15011
15012 // Register it in the chart
15013 chartSeries.push(series);
15014 series._i = chartSeries.length - 1;
15015
15016 // Sort series according to index option (#248, #1123, #2456)
15017 stableSort(chartSeries, sortByIndex);
15018 if (this.yAxis) {
15019 stableSort(this.yAxis.series, sortByIndex);
15020 }
15021
15022 each(chartSeries, function(series, i) {
15023 series.index = i;
15024 series.name = series.name || 'Series ' + (i + 1);
15025 });
15026
15027 },
15028
15029 /**
15030 * Set the xAxis and yAxis properties of cartesian series, and register the series
15031 * in the axis.series array
15032 */
15033 bindAxes: function() {
15034 var series = this,
15035 seriesOptions = series.options,
15036 chart = series.chart,
15037 axisOptions;
15038
15039 each(series.axisTypes || [], function(AXIS) { // repeat for xAxis and yAxis
15040
15041 each(chart[AXIS], function(axis) { // loop through the chart's axis objects
15042 axisOptions = axis.options;
15043
15044 // apply if the series xAxis or yAxis option mathches the number of the
15045 // axis, or if undefined, use the first axis
15046 if ((seriesOptions[AXIS] === axisOptions.index) ||
15047 (seriesOptions[AXIS] !== undefined && seriesOptions[AXIS] === axisOptions.id) ||
15048 (seriesOptions[AXIS] === undefined && axisOptions.index === 0)) {
15049
15050 // register this series in the axis.series lookup
15051 axis.series.push(series);
15052
15053 // set this series.xAxis or series.yAxis reference
15054 series[AXIS] = axis;
15055
15056 // mark dirty for redraw
15057 axis.isDirty = true;
15058 }
15059 });
15060
15061 // The series needs an X and an Y axis
15062 if (!series[AXIS] && series.optionalAxis !== AXIS) {
15063 error(18, true);
15064 }
15065
15066 });
15067 },
15068
15069 /**
15070 * For simple series types like line and column, the data values are held in arrays like
15071 * xData and yData for quick lookup to find extremes and more. For multidimensional series
15072 * like bubble and map, this can be extended with arrays like zData and valueData by
15073 * adding to the series.parallelArrays array.
15074 */
15075 updateParallelArrays: function(point, i) {
15076 var series = point.series,
15077 args = arguments,
15078 fn = isNumber(i) ?
15079 // Insert the value in the given position
15080 function(key) {
15081 var val = key === 'y' && series.toYData ? series.toYData(point) : point[key];
15082 series[key + 'Data'][i] = val;
15083 } :
15084 // Apply the method specified in i with the following arguments as arguments
15085 function(key) {
15086 Array.prototype[i].apply(series[key + 'Data'], Array.prototype.slice.call(args, 2));
15087 };
15088
15089 each(series.parallelArrays, fn);
15090 },
15091
15092 /**
15093 * Return an auto incremented x value based on the pointStart and pointInterval options.
15094 * This is only used if an x value is not given for the point that calls autoIncrement.
15095 */
15096 autoIncrement: function() {
15097
15098 var options = this.options,
15099 xIncrement = this.xIncrement,
15100 date,
15101 pointInterval,
15102 pointIntervalUnit = options.pointIntervalUnit;
15103
15104 xIncrement = pick(xIncrement, options.pointStart, 0);
15105
15106 this.pointInterval = pointInterval = pick(this.pointInterval, options.pointInterval, 1);
15107
15108 // Added code for pointInterval strings
15109 if (pointIntervalUnit) {
15110 date = new Date(xIncrement);
15111
15112 if (pointIntervalUnit === 'day') {
15113 date = +date[Date.hcSetDate](date[Date.hcGetDate]() + pointInterval);
15114 } else if (pointIntervalUnit === 'month') {
15115 date = +date[Date.hcSetMonth](date[Date.hcGetMonth]() + pointInterval);
15116 } else if (pointIntervalUnit === 'year') {
15117 date = +date[Date.hcSetFullYear](date[Date.hcGetFullYear]() + pointInterval);
15118 }
15119 pointInterval = date - xIncrement;
15120
15121 }
15122
15123 this.xIncrement = xIncrement + pointInterval;
15124 return xIncrement;
15125 },
15126
15127 /**
15128 * Set the series options by merging from the options tree
15129 * @param {Object} itemOptions
15130 */
15131 setOptions: function(itemOptions) {
15132 var chart = this.chart,
15133 chartOptions = chart.options,
15134 plotOptions = chartOptions.plotOptions,
15135 userOptions = chart.userOptions || {},
15136 userPlotOptions = userOptions.plotOptions || {},
15137 typeOptions = plotOptions[this.type],
15138 options,
15139 zones;
15140
15141 this.userOptions = itemOptions;
15142
15143 // General series options take precedence over type options because otherwise, default
15144 // type options like column.animation would be overwritten by the general option.
15145 // But issues have been raised here (#3881), and the solution may be to distinguish
15146 // between default option and userOptions like in the tooltip below.
15147 options = merge(
15148 typeOptions,
15149 plotOptions.series,
15150 itemOptions
15151 );
15152
15153 // The tooltip options are merged between global and series specific options
15154 this.tooltipOptions = merge(
15155 defaultOptions.tooltip,
15156 defaultOptions.plotOptions[this.type].tooltip,
15157 userOptions.tooltip,
15158 userPlotOptions.series && userPlotOptions.series.tooltip,
15159 userPlotOptions[this.type] && userPlotOptions[this.type].tooltip,
15160 itemOptions.tooltip
15161 );
15162
15163 // Delete marker object if not allowed (#1125)
15164 if (typeOptions.marker === null) {
15165 delete options.marker;
15166 }
15167
15168 // Handle color zones
15169 this.zoneAxis = options.zoneAxis;
15170 zones = this.zones = (options.zones || []).slice();
15171 if ((options.negativeColor || options.negativeFillColor) && !options.zones) {
15172 zones.push({
15173 value: options[this.zoneAxis + 'Threshold'] || options.threshold || 0,
15174 className: 'highcharts-negative',
15175
15176 color: options.negativeColor,
15177 fillColor: options.negativeFillColor
15178
15179 });
15180 }
15181 if (zones.length) { // Push one extra zone for the rest
15182 if (defined(zones[zones.length - 1].value)) {
15183 zones.push({
15184
15185 color: this.color,
15186 fillColor: this.fillColor
15187
15188 });
15189 }
15190 }
15191 return options;
15192 },
15193
15194 getCyclic: function(prop, value, defaults) {
15195 var i,
15196 userOptions = this.userOptions,
15197 indexName = prop + 'Index',
15198 counterName = prop + 'Counter',
15199 len = defaults ? defaults.length : pick(this.chart.options.chart[prop + 'Count'], this.chart[prop + 'Count']),
15200 setting;
15201
15202 if (!value) {
15203 // Pick up either the colorIndex option, or the _colorIndex after Series.update()
15204 setting = pick(userOptions[indexName], userOptions['_' + indexName]);
15205 if (defined(setting)) { // after Series.update()
15206 i = setting;
15207 } else {
15208 userOptions['_' + indexName] = i = this.chart[counterName] % len;
15209 this.chart[counterName] += 1;
15210 }
15211 if (defaults) {
15212 value = defaults[i];
15213 }
15214 }
15215 // Set the colorIndex
15216 if (i !== undefined) {
15217 this[indexName] = i;
15218 }
15219 this[prop] = value;
15220 },
15221
15222 /**
15223 * Get the series' color
15224 */
15225
15226 getColor: function() {
15227 if (this.options.colorByPoint) {
15228 this.options.color = null; // #4359, selected slice got series.color even when colorByPoint was set.
15229 } else {
15230 this.getCyclic('color', this.options.color || defaultPlotOptions[this.type].color, this.chart.options.colors);
15231 }
15232 },
15233
15234 /**
15235 * Get the series' symbol
15236 */
15237 getSymbol: function() {
15238 var seriesMarkerOption = this.options.marker;
15239
15240 this.getCyclic('symbol', seriesMarkerOption.symbol, this.chart.options.symbols);
15241
15242 // don't substract radius in image symbols (#604)
15243 if (/^url/.test(this.symbol)) {
15244 seriesMarkerOption.radius = 0;
15245 }
15246 },
15247
15248 drawLegendSymbol: LegendSymbolMixin.drawLineMarker,
15249
15250 /**
15251 * Replace the series data with a new set of data
15252 * @param {Object} data
15253 * @param {Object} redraw
15254 */
15255 setData: function(data, redraw, animation, updatePoints) {
15256 var series = this,
15257 oldData = series.points,
15258 oldDataLength = (oldData && oldData.length) || 0,
15259 dataLength,
15260 options = series.options,
15261 chart = series.chart,
15262 firstPoint = null,
15263 xAxis = series.xAxis,
15264 i,
15265 turboThreshold = options.turboThreshold,
15266 pt,
15267 xData = this.xData,
15268 yData = this.yData,
15269 pointArrayMap = series.pointArrayMap,
15270 valueCount = pointArrayMap && pointArrayMap.length;
15271
15272 data = data || [];
15273 dataLength = data.length;
15274 redraw = pick(redraw, true);
15275
15276 // If the point count is the same as is was, just run Point.update which is
15277 // cheaper, allows animation, and keeps references to points.
15278 if (updatePoints !== false && dataLength && oldDataLength === dataLength && !series.cropped && !series.hasGroupedData && series.visible) {
15279 each(data, function(point, i) {
15280 // .update doesn't exist on a linked, hidden series (#3709)
15281 if (oldData[i].update && point !== options.data[i]) {
15282 oldData[i].update(point, false, null, false);
15283 }
15284 });
15285
15286 } else {
15287
15288 // Reset properties
15289 series.xIncrement = null;
15290
15291 series.colorCounter = 0; // for series with colorByPoint (#1547)
15292
15293 // Update parallel arrays
15294 each(this.parallelArrays, function(key) {
15295 series[key + 'Data'].length = 0;
15296 });
15297
15298 // In turbo mode, only one- or twodimensional arrays of numbers are allowed. The
15299 // first value is tested, and we assume that all the rest are defined the same
15300 // way. Although the 'for' loops are similar, they are repeated inside each
15301 // if-else conditional for max performance.
15302 if (turboThreshold && dataLength > turboThreshold) {
15303
15304 // find the first non-null point
15305 i = 0;
15306 while (firstPoint === null && i < dataLength) {
15307 firstPoint = data[i];
15308 i++;
15309 }
15310
15311
15312 if (isNumber(firstPoint)) { // assume all points are numbers
15313 for (i = 0; i < dataLength; i++) {
15314 xData[i] = this.autoIncrement();
15315 yData[i] = data[i];
15316 }
15317 } else if (isArray(firstPoint)) { // assume all points are arrays
15318 if (valueCount) { // [x, low, high] or [x, o, h, l, c]
15319 for (i = 0; i < dataLength; i++) {
15320 pt = data[i];
15321 xData[i] = pt[0];
15322 yData[i] = pt.slice(1, valueCount + 1);
15323 }
15324 } else { // [x, y]
15325 for (i = 0; i < dataLength; i++) {
15326 pt = data[i];
15327 xData[i] = pt[0];
15328 yData[i] = pt[1];
15329 }
15330 }
15331 } else {
15332 error(12); // Highcharts expects configs to be numbers or arrays in turbo mode
15333 }
15334 } else {
15335 for (i = 0; i < dataLength; i++) {
15336 if (data[i] !== undefined) { // stray commas in oldIE
15337 pt = {
15338 series: series
15339 };
15340 series.pointClass.prototype.applyOptions.apply(pt, [data[i]]);
15341 series.updateParallelArrays(pt, i);
15342 }
15343 }
15344 }
15345
15346 // Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON
15347 if (isString(yData[0])) {
15348 error(14, true);
15349 }
15350
15351 series.data = [];
15352 series.options.data = series.userOptions.data = data;
15353
15354 // destroy old points
15355 i = oldDataLength;
15356 while (i--) {
15357 if (oldData[i] && oldData[i].destroy) {
15358 oldData[i].destroy();
15359 }
15360 }
15361
15362 // reset minRange (#878)
15363 if (xAxis) {
15364 xAxis.minRange = xAxis.userMinRange;
15365 }
15366
15367 // redraw
15368 series.isDirty = chart.isDirtyBox = true;
15369 series.isDirtyData = !!oldData;
15370 animation = false;
15371 }
15372
15373 // Typically for pie series, points need to be processed and generated
15374 // prior to rendering the legend
15375 if (options.legendType === 'point') {
15376 this.processData();
15377 this.generatePoints();
15378 }
15379
15380 if (redraw) {
15381 chart.redraw(animation);
15382 }
15383 },
15384
15385 /**
15386 * Process the data by cropping away unused data points if the series is longer
15387 * than the crop threshold. This saves computing time for lage series.
15388 */
15389 processData: function(force) {
15390 var series = this,
15391 processedXData = series.xData, // copied during slice operation below
15392 processedYData = series.yData,
15393 dataLength = processedXData.length,
15394 croppedData,
15395 cropStart = 0,
15396 cropped,
15397 distance,
15398 closestPointRange,
15399 xAxis = series.xAxis,
15400 i, // loop variable
15401 options = series.options,
15402 cropThreshold = options.cropThreshold,
15403 getExtremesFromAll = series.getExtremesFromAll || options.getExtremesFromAll, // #4599
15404 isCartesian = series.isCartesian,
15405 xExtremes,
15406 val2lin = xAxis && xAxis.val2lin,
15407 isLog = xAxis && xAxis.isLog,
15408 min,
15409 max;
15410
15411 // If the series data or axes haven't changed, don't go through this. Return false to pass
15412 // the message on to override methods like in data grouping.
15413 if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) {
15414 return false;
15415 }
15416
15417 if (xAxis) {
15418 xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053)
15419 min = xExtremes.min;
15420 max = xExtremes.max;
15421 }
15422
15423 // optionally filter out points outside the plot area
15424 if (isCartesian && series.sorted && !getExtremesFromAll && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) {
15425
15426 // it's outside current extremes
15427 if (processedXData[dataLength - 1] < min || processedXData[0] > max) {
15428 processedXData = [];
15429 processedYData = [];
15430
15431 // only crop if it's actually spilling out
15432 } else if (processedXData[0] < min || processedXData[dataLength - 1] > max) {
15433 croppedData = this.cropData(series.xData, series.yData, min, max);
15434 processedXData = croppedData.xData;
15435 processedYData = croppedData.yData;
15436 cropStart = croppedData.start;
15437 cropped = true;
15438 }
15439 }
15440
15441
15442 // Find the closest distance between processed points
15443 i = processedXData.length || 1;
15444 while (--i) {
15445 distance = isLog ?
15446 val2lin(processedXData[i]) - val2lin(processedXData[i - 1]) :
15447 processedXData[i] - processedXData[i - 1];
15448
15449 if (distance > 0 && (closestPointRange === undefined || distance < closestPointRange)) {
15450 closestPointRange = distance;
15451
15452 // Unsorted data is not supported by the line tooltip, as well as data grouping and
15453 // navigation in Stock charts (#725) and width calculation of columns (#1900)
15454 } else if (distance < 0 && series.requireSorting) {
15455 error(15);
15456 }
15457 }
15458
15459 // Record the properties
15460 series.cropped = cropped; // undefined or true
15461 series.cropStart = cropStart;
15462 series.processedXData = processedXData;
15463 series.processedYData = processedYData;
15464
15465 series.closestPointRange = closestPointRange;
15466
15467 },
15468
15469 /**
15470 * Iterate over xData and crop values between min and max. Returns object containing crop start/end
15471 * cropped xData with corresponding part of yData, dataMin and dataMax within the cropped range
15472 */
15473 cropData: function(xData, yData, min, max) {
15474 var dataLength = xData.length,
15475 cropStart = 0,
15476 cropEnd = dataLength,
15477 cropShoulder = pick(this.cropShoulder, 1), // line-type series need one point outside
15478 i,
15479 j;
15480
15481 // iterate up to find slice start
15482 for (i = 0; i < dataLength; i++) {
15483 if (xData[i] >= min) {
15484 cropStart = Math.max(0, i - cropShoulder);
15485 break;
15486 }
15487 }
15488
15489 // proceed to find slice end
15490 for (j = i; j < dataLength; j++) {
15491 if (xData[j] > max) {
15492 cropEnd = j + cropShoulder;
15493 break;
15494 }
15495 }
15496
15497 return {
15498 xData: xData.slice(cropStart, cropEnd),
15499 yData: yData.slice(cropStart, cropEnd),
15500 start: cropStart,
15501 end: cropEnd
15502 };
15503 },
15504
15505
15506 /**
15507 * Generate the data point after the data has been processed by cropping away
15508 * unused points and optionally grouped in Highcharts Stock.
15509 */
15510 generatePoints: function() {
15511 var series = this,
15512 options = series.options,
15513 dataOptions = options.data,
15514 data = series.data,
15515 dataLength,
15516 processedXData = series.processedXData,
15517 processedYData = series.processedYData,
15518 PointClass = series.pointClass,
15519 processedDataLength = processedXData.length,
15520 cropStart = series.cropStart || 0,
15521 cursor,
15522 hasGroupedData = series.hasGroupedData,
15523 point,
15524 points = [],
15525 i;
15526
15527 if (!data && !hasGroupedData) {
15528 var arr = [];
15529 arr.length = dataOptions.length;
15530 data = series.data = arr;
15531 }
15532
15533 for (i = 0; i < processedDataLength; i++) {
15534 cursor = cropStart + i;
15535 if (!hasGroupedData) {
15536 if (data[cursor]) {
15537 point = data[cursor];
15538 } else if (dataOptions[cursor] !== undefined) { // #970
15539 data[cursor] = point = (new PointClass()).init(series, dataOptions[cursor], processedXData[i]);
15540 }
15541 points[i] = point;
15542 } else {
15543 // splat the y data in case of ohlc data array
15544 points[i] = (new PointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i])));
15545 points[i].dataGroup = series.groupMap[i];
15546 }
15547 points[i].index = cursor; // For faster access in Point.update
15548 }
15549
15550 // Hide cropped-away points - this only runs when the number of points is above cropThreshold, or when
15551 // swithching view from non-grouped data to grouped data (#637)
15552 if (data && (processedDataLength !== (dataLength = data.length) || hasGroupedData)) {
15553 for (i = 0; i < dataLength; i++) {
15554 if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points
15555 i += processedDataLength;
15556 }
15557 if (data[i]) {
15558 data[i].destroyElements();
15559 data[i].plotX = undefined; // #1003
15560 }
15561 }
15562 }
15563
15564 series.data = data;
15565 series.points = points;
15566 },
15567
15568 /**
15569 * Calculate Y extremes for visible data
15570 */
15571 getExtremes: function(yData) {
15572 var xAxis = this.xAxis,
15573 yAxis = this.yAxis,
15574 xData = this.processedXData,
15575 yDataLength,
15576 activeYData = [],
15577 activeCounter = 0,
15578 xExtremes = xAxis.getExtremes(), // #2117, need to compensate for log X axis
15579 xMin = xExtremes.min,
15580 xMax = xExtremes.max,
15581 validValue,
15582 withinRange,
15583 x,
15584 y,
15585 i,
15586 j;
15587
15588 yData = yData || this.stackedYData || this.processedYData || [];
15589 yDataLength = yData.length;
15590
15591 for (i = 0; i < yDataLength; i++) {
15592
15593 x = xData[i];
15594 y = yData[i];
15595
15596 // For points within the visible range, including the first point outside the
15597 // visible range, consider y extremes
15598 validValue = (isNumber(y, true) || isArray(y)) && (!yAxis.isLog || (y.length || y > 0));
15599 withinRange = this.getExtremesFromAll || this.options.getExtremesFromAll || this.cropped ||
15600 ((xData[i + 1] || x) >= xMin && (xData[i - 1] || x) <= xMax);
15601
15602 if (validValue && withinRange) {
15603
15604 j = y.length;
15605 if (j) { // array, like ohlc or range data
15606 while (j--) {
15607 if (y[j] !== null) {
15608 activeYData[activeCounter++] = y[j];
15609 }
15610 }
15611 } else {
15612 activeYData[activeCounter++] = y;
15613 }
15614 }
15615 }
15616 this.dataMin = arrayMin(activeYData);
15617 this.dataMax = arrayMax(activeYData);
15618 },
15619
15620 /**
15621 * Translate data points from raw data values to chart specific positioning data
15622 * needed later in drawPoints, drawGraph and drawTracker.
15623 */
15624 translate: function() {
15625 if (!this.processedXData) { // hidden series
15626 this.processData();
15627 }
15628 this.generatePoints();
15629 var series = this,
15630 options = series.options,
15631 stacking = options.stacking,
15632 xAxis = series.xAxis,
15633 categories = xAxis.categories,
15634 yAxis = series.yAxis,
15635 points = series.points,
15636 dataLength = points.length,
15637 hasModifyValue = !!series.modifyValue,
15638 i,
15639 pointPlacement = options.pointPlacement,
15640 dynamicallyPlaced = pointPlacement === 'between' || isNumber(pointPlacement),
15641 threshold = options.threshold,
15642 stackThreshold = options.startFromThreshold ? threshold : 0,
15643 plotX,
15644 plotY,
15645 lastPlotX,
15646 stackIndicator,
15647 closestPointRangePx = Number.MAX_VALUE;
15648
15649 // Translate each point
15650 for (i = 0; i < dataLength; i++) {
15651 var point = points[i],
15652 xValue = point.x,
15653 yValue = point.y,
15654 yBottom = point.low,
15655 stack = stacking && yAxis.stacks[(series.negStacks && yValue < (stackThreshold ? 0 : threshold) ? '-' : '') + series.stackKey],
15656 pointStack,
15657 stackValues;
15658
15659 // Discard disallowed y values for log axes (#3434)
15660 if (yAxis.isLog && yValue !== null && yValue <= 0) {
15661 point.isNull = true;
15662 }
15663
15664 // Get the plotX translation
15665 point.plotX = plotX = correctFloat( // #5236
15666 Math.min(Math.max(-1e5, xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement, this.type === 'flags')), 1e5) // #3923
15667 );
15668
15669 // Calculate the bottom y value for stacked series
15670 if (stacking && series.visible && !point.isNull && stack && stack[xValue]) {
15671 stackIndicator = series.getStackIndicator(stackIndicator, xValue, series.index);
15672 pointStack = stack[xValue];
15673 stackValues = pointStack.points[stackIndicator.key];
15674 yBottom = stackValues[0];
15675 yValue = stackValues[1];
15676
15677 if (yBottom === stackThreshold && stackIndicator.key === stack[xValue].base) {
15678 yBottom = pick(threshold, yAxis.min);
15679 }
15680 if (yAxis.isLog && yBottom <= 0) { // #1200, #1232
15681 yBottom = null;
15682 }
15683
15684 point.total = point.stackTotal = pointStack.total;
15685 point.percentage = pointStack.total && (point.y / pointStack.total * 100);
15686 point.stackY = yValue;
15687
15688 // Place the stack label
15689 pointStack.setOffset(series.pointXOffset || 0, series.barW || 0);
15690
15691 }
15692
15693 // Set translated yBottom or remove it
15694 point.yBottom = defined(yBottom) ?
15695 yAxis.translate(yBottom, 0, 1, 0, 1) :
15696 null;
15697
15698 // general hook, used for Highstock compare mode
15699 if (hasModifyValue) {
15700 yValue = series.modifyValue(yValue, point);
15701 }
15702
15703 // Set the the plotY value, reset it for redraws
15704 point.plotY = plotY = (typeof yValue === 'number' && yValue !== Infinity) ?
15705 Math.min(Math.max(-1e5, yAxis.translate(yValue, 0, 1, 0, 1)), 1e5) : // #3201
15706 undefined;
15707 point.isInside = plotY !== undefined && plotY >= 0 && plotY <= yAxis.len && // #3519
15708 plotX >= 0 && plotX <= xAxis.len;
15709
15710
15711 // Set client related positions for mouse tracking
15712 point.clientX = dynamicallyPlaced ? correctFloat(xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement)) : plotX; // #1514, #5383, #5518
15713
15714 point.negative = point.y < (threshold || 0);
15715
15716 // some API data
15717 point.category = categories && categories[point.x] !== undefined ?
15718 categories[point.x] : point.x;
15719
15720 // Determine auto enabling of markers (#3635, #5099)
15721 if (!point.isNull) {
15722 if (lastPlotX !== undefined) {
15723 closestPointRangePx = Math.min(closestPointRangePx, Math.abs(plotX - lastPlotX));
15724 }
15725 lastPlotX = plotX;
15726 }
15727
15728 }
15729 series.closestPointRangePx = closestPointRangePx;
15730 },
15731
15732 /**
15733 * Return the series points with null points filtered out
15734 */
15735 getValidPoints: function(points, insideOnly) {
15736 var chart = this.chart;
15737 return grep(points || this.points || [], function isValidPoint(point) { // #3916, #5029
15738 if (insideOnly && !chart.isInsidePlot(point.plotX, point.plotY, chart.inverted)) { // #5085
15739 return false;
15740 }
15741 return !point.isNull;
15742 });
15743 },
15744
15745 /**
15746 * Set the clipping for the series. For animated series it is called twice, first to initiate
15747 * animating the clip then the second time without the animation to set the final clip.
15748 */
15749 setClip: function(animation) {
15750 var chart = this.chart,
15751 options = this.options,
15752 renderer = chart.renderer,
15753 inverted = chart.inverted,
15754 seriesClipBox = this.clipBox,
15755 clipBox = seriesClipBox || chart.clipBox,
15756 sharedClipKey = this.sharedClipKey || ['_sharedClip', animation && animation.duration, animation && animation.easing, clipBox.height, options.xAxis, options.yAxis].join(','), // #4526
15757 clipRect = chart[sharedClipKey],
15758 markerClipRect = chart[sharedClipKey + 'm'];
15759
15760 // If a clipping rectangle with the same properties is currently present in the chart, use that.
15761 if (!clipRect) {
15762
15763 // When animation is set, prepare the initial positions
15764 if (animation) {
15765 clipBox.width = 0;
15766
15767 chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect(-99, // include the width of the first marker
15768 inverted ? -chart.plotLeft : -chart.plotTop,
15769 99,
15770 inverted ? chart.chartWidth : chart.chartHeight
15771 );
15772 }
15773 chart[sharedClipKey] = clipRect = renderer.clipRect(clipBox);
15774 // Create hashmap for series indexes
15775 clipRect.count = {
15776 length: 0
15777 };
15778
15779 }
15780 if (animation) {
15781 if (!clipRect.count[this.index]) {
15782 clipRect.count[this.index] = true;
15783 clipRect.count.length += 1;
15784 }
15785 }
15786
15787 if (options.clip !== false) {
15788 this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect);
15789 this.markerGroup.clip(markerClipRect);
15790 this.sharedClipKey = sharedClipKey;
15791 }
15792
15793 // Remove the shared clipping rectangle when all series are shown
15794 if (!animation) {
15795 if (clipRect.count[this.index]) {
15796 delete clipRect.count[this.index];
15797 clipRect.count.length -= 1;
15798 }
15799
15800 if (clipRect.count.length === 0 && sharedClipKey && chart[sharedClipKey]) {
15801 if (!seriesClipBox) {
15802 chart[sharedClipKey] = chart[sharedClipKey].destroy();
15803 }
15804 if (chart[sharedClipKey + 'm']) {
15805 chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy();
15806 }
15807 }
15808 }
15809 },
15810
15811 /**
15812 * Animate in the series
15813 */
15814 animate: function(init) {
15815 var series = this,
15816 chart = series.chart,
15817 clipRect,
15818 animation = animObject(series.options.animation),
15819 sharedClipKey;
15820
15821 // Initialize the animation. Set up the clipping rectangle.
15822 if (init) {
15823
15824 series.setClip(animation);
15825
15826 // Run the animation
15827 } else {
15828 sharedClipKey = this.sharedClipKey;
15829 clipRect = chart[sharedClipKey];
15830 if (clipRect) {
15831 clipRect.animate({
15832 width: chart.plotSizeX
15833 }, animation);
15834 }
15835 if (chart[sharedClipKey + 'm']) {
15836 chart[sharedClipKey + 'm'].animate({
15837 width: chart.plotSizeX + 99
15838 }, animation);
15839 }
15840
15841 // Delete this function to allow it only once
15842 series.animate = null;
15843
15844 }
15845 },
15846
15847 /**
15848 * This runs after animation to land on the final plot clipping
15849 */
15850 afterAnimate: function() {
15851 this.setClip();
15852 fireEvent(this, 'afterAnimate');
15853 },
15854
15855 /**
15856 * Draw the markers
15857 */
15858 drawPoints: function() {
15859 var series = this,
15860 points = series.points,
15861 chart = series.chart,
15862 plotX,
15863 plotY,
15864 i,
15865 point,
15866 radius,
15867 symbol,
15868 isImage,
15869 graphic,
15870 options = series.options,
15871 seriesMarkerOptions = options.marker,
15872 pointMarkerOptions,
15873 hasPointMarker,
15874 enabled,
15875 isInside,
15876 markerGroup = series.markerGroup,
15877 xAxis = series.xAxis,
15878 globallyEnabled = pick(
15879 seriesMarkerOptions.enabled,
15880 xAxis.isRadial ? true : null,
15881 series.closestPointRangePx > 2 * seriesMarkerOptions.radius
15882 );
15883
15884 if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) {
15885
15886 i = points.length;
15887 while (i--) {
15888 point = points[i];
15889 plotX = Math.floor(point.plotX); // #1843
15890 plotY = point.plotY;
15891 graphic = point.graphic;
15892 pointMarkerOptions = point.marker || {};
15893 hasPointMarker = !!point.marker;
15894 enabled = (globallyEnabled && pointMarkerOptions.enabled === undefined) || pointMarkerOptions.enabled;
15895 isInside = point.isInside;
15896
15897 // only draw the point if y is defined
15898 if (enabled && isNumber(plotY) && point.y !== null) {
15899
15900 // Shortcuts
15901 radius = seriesMarkerOptions.radius;
15902 symbol = pick(pointMarkerOptions.symbol, series.symbol);
15903 isImage = symbol.indexOf('url') === 0;
15904
15905 if (graphic) { // update
15906 graphic[isInside ? 'show' : 'hide'](true) // Since the marker group isn't clipped, each individual marker must be toggled
15907 //.attr(pointAttr) // #4759
15908 .animate(extend({
15909 x: plotX - radius,
15910 y: plotY - radius
15911 }, graphic.symbolName ? { // don't apply to image symbols #507
15912 width: 2 * radius,
15913 height: 2 * radius
15914 } : {}));
15915 } else if (isInside && (radius > 0 || isImage)) {
15916 point.graphic = graphic = chart.renderer.symbol(
15917 symbol,
15918 plotX - radius,
15919 plotY - radius,
15920 2 * radius,
15921 2 * radius,
15922 hasPointMarker ? pointMarkerOptions : seriesMarkerOptions
15923 )
15924 .attr({
15925 r: radius
15926 })
15927 .add(markerGroup);
15928 }
15929
15930
15931 // Presentational attributes
15932 if (graphic) {
15933 graphic.attr(series.pointAttribs(point, point.selected && 'select'));
15934 }
15935
15936
15937 if (graphic) {
15938 graphic.addClass(point.getClassName(), true);
15939 }
15940
15941 } else if (graphic) {
15942 point.graphic = graphic.destroy(); // #1269
15943 }
15944 }
15945 }
15946
15947 },
15948
15949
15950 /**
15951 * Get presentational attributes for marker-based series (line, spline, scatter, bubble, mappoint...)
15952 */
15953 pointAttribs: function(point, state) {
15954 var seriesMarkerOptions = this.options.marker,
15955 seriesStateOptions,
15956 pointOptions = point && point.options,
15957 pointMarkerOptions = (pointOptions && pointOptions.marker) || {},
15958 pointStateOptions,
15959 strokeWidth = seriesMarkerOptions.lineWidth,
15960 color = this.color,
15961 pointColorOption = pointOptions && pointOptions.color,
15962 pointColor = point && point.color,
15963 zoneColor,
15964 fill,
15965 stroke,
15966 zone;
15967
15968 if (point && this.zones.length) {
15969 zone = point.getZone();
15970 if (zone && zone.color) {
15971 zoneColor = zone.color;
15972 }
15973 }
15974
15975 color = pointColorOption || zoneColor || pointColor || color;
15976 fill = pointMarkerOptions.fillColor || seriesMarkerOptions.fillColor || color;
15977 stroke = pointMarkerOptions.lineColor || seriesMarkerOptions.lineColor || color;
15978
15979 // Handle hover and select states
15980 if (state) {
15981 seriesStateOptions = seriesMarkerOptions.states[state];
15982 pointStateOptions = (pointMarkerOptions.states && pointMarkerOptions.states[state]) || {};
15983 strokeWidth = seriesStateOptions.lineWidth || strokeWidth + seriesStateOptions.lineWidthPlus;
15984 fill = pointStateOptions.fillColor || seriesStateOptions.fillColor || fill;
15985 stroke = pointStateOptions.lineColor || seriesStateOptions.lineColor || stroke;
15986 }
15987
15988 return {
15989 'stroke': stroke,
15990 'stroke-width': strokeWidth,
15991 'fill': fill
15992 };
15993 },
15994
15995 /**
15996 * Clear DOM objects and free up memory
15997 */
15998 destroy: function() {
15999 var series = this,
16000 chart = series.chart,
16001 issue134 = /AppleWebKit\/533/.test(win.navigator.userAgent),
16002 destroy,
16003 i,
16004 data = series.data || [],
16005 point,
16006 prop,
16007 axis;
16008
16009 // add event hook
16010 fireEvent(series, 'destroy');
16011
16012 // remove all events
16013 removeEvent(series);
16014
16015 // erase from axes
16016 each(series.axisTypes || [], function(AXIS) {
16017 axis = series[AXIS];
16018 if (axis && axis.series) {
16019 erase(axis.series, series);
16020 axis.isDirty = axis.forceRedraw = true;
16021 }
16022 });
16023
16024 // remove legend items
16025 if (series.legendItem) {
16026 series.chart.legend.destroyItem(series);
16027 }
16028
16029 // destroy all points with their elements
16030 i = data.length;
16031 while (i--) {
16032 point = data[i];
16033 if (point && point.destroy) {
16034 point.destroy();
16035 }
16036 }
16037 series.points = null;
16038
16039 // Clear the animation timeout if we are destroying the series during initial animation
16040 clearTimeout(series.animationTimeout);
16041
16042 // Destroy all SVGElements associated to the series
16043 for (prop in series) {
16044 if (series[prop] instanceof SVGElement && !series[prop].survive) { // Survive provides a hook for not destroying
16045
16046 // issue 134 workaround
16047 destroy = issue134 && prop === 'group' ?
16048 'hide' :
16049 'destroy';
16050
16051 series[prop][destroy]();
16052 }
16053 }
16054
16055 // remove from hoverSeries
16056 if (chart.hoverSeries === series) {
16057 chart.hoverSeries = null;
16058 }
16059 erase(chart.series, series);
16060
16061 // clear all members
16062 for (prop in series) {
16063 delete series[prop];
16064 }
16065 },
16066
16067 /**
16068 * Get the graph path
16069 */
16070 getGraphPath: function(points, nullsAsZeroes, connectCliffs) {
16071 var series = this,
16072 options = series.options,
16073 step = options.step,
16074 reversed,
16075 graphPath = [],
16076 xMap = [],
16077 gap;
16078
16079 points = points || series.points;
16080
16081 // Bottom of a stack is reversed
16082 reversed = points.reversed;
16083 if (reversed) {
16084 points.reverse();
16085 }
16086 // Reverse the steps (#5004)
16087 step = {
16088 right: 1,
16089 center: 2
16090 }[step] || (step && 3);
16091 if (step && reversed) {
16092 step = 4 - step;
16093 }
16094
16095 // Remove invalid points, especially in spline (#5015)
16096 if (options.connectNulls && !nullsAsZeroes && !connectCliffs) {
16097 points = this.getValidPoints(points);
16098 }
16099
16100 // Build the line
16101 each(points, function(point, i) {
16102
16103 var plotX = point.plotX,
16104 plotY = point.plotY,
16105 lastPoint = points[i - 1],
16106 pathToPoint; // the path to this point from the previous
16107
16108 if ((point.leftCliff || (lastPoint && lastPoint.rightCliff)) && !connectCliffs) {
16109 gap = true; // ... and continue
16110 }
16111
16112 // Line series, nullsAsZeroes is not handled
16113 if (point.isNull && !defined(nullsAsZeroes) && i > 0) {
16114 gap = !options.connectNulls;
16115
16116 // Area series, nullsAsZeroes is set
16117 } else if (point.isNull && !nullsAsZeroes) {
16118 gap = true;
16119
16120 } else {
16121
16122 if (i === 0 || gap) {
16123 pathToPoint = ['M', point.plotX, point.plotY];
16124
16125 } else if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object
16126
16127 pathToPoint = series.getPointSpline(points, point, i);
16128
16129 } else if (step) {
16130
16131 if (step === 1) { // right
16132 pathToPoint = [
16133 'L',
16134 lastPoint.plotX,
16135 plotY
16136 ];
16137
16138 } else if (step === 2) { // center
16139 pathToPoint = [
16140 'L',
16141 (lastPoint.plotX + plotX) / 2,
16142 lastPoint.plotY,
16143 'L',
16144 (lastPoint.plotX + plotX) / 2,
16145 plotY
16146 ];
16147
16148 } else {
16149 pathToPoint = [
16150 'L',
16151 plotX,
16152 lastPoint.plotY
16153 ];
16154 }
16155 pathToPoint.push('L', plotX, plotY);
16156
16157 } else {
16158 // normal line to next point
16159 pathToPoint = [
16160 'L',
16161 plotX,
16162 plotY
16163 ];
16164 }
16165
16166 // Prepare for animation. When step is enabled, there are two path nodes for each x value.
16167 xMap.push(point.x);
16168 if (step) {
16169 xMap.push(point.x);
16170 }
16171
16172 graphPath.push.apply(graphPath, pathToPoint);
16173 gap = false;
16174 }
16175 });
16176
16177 graphPath.xMap = xMap;
16178 series.graphPath = graphPath;
16179
16180 return graphPath;
16181
16182 },
16183
16184 /**
16185 * Draw the actual graph
16186 */
16187 drawGraph: function() {
16188 var series = this,
16189 options = this.options,
16190 graphPath = (this.gappedPath || this.getGraphPath).call(this),
16191 props = [
16192 [
16193 'graph',
16194 'highcharts-graph',
16195
16196 options.lineColor || this.color,
16197 options.dashStyle
16198
16199 ]
16200 ];
16201
16202 // Add the zone properties if any
16203 each(this.zones, function(zone, i) {
16204 props.push([
16205 'zone-graph-' + i,
16206 'highcharts-graph highcharts-zone-graph-' + i + ' ' + (zone.className || ''),
16207
16208 zone.color || series.color,
16209 zone.dashStyle || options.dashStyle
16210
16211 ]);
16212 });
16213
16214 // Draw the graph
16215 each(props, function(prop, i) {
16216 var graphKey = prop[0],
16217 graph = series[graphKey],
16218 attribs;
16219
16220 if (graph) {
16221 graph.endX = graphPath.xMap;
16222 graph.animate({
16223 d: graphPath
16224 });
16225
16226 } else if (graphPath.length) { // #1487
16227
16228 series[graphKey] = series.chart.renderer.path(graphPath)
16229 .addClass(prop[1])
16230 .attr({
16231 zIndex: 1
16232 }) // #1069
16233 .add(series.group);
16234
16235
16236 attribs = {
16237 'stroke': prop[2],
16238 'stroke-width': options.lineWidth,
16239 'fill': (series.fillGraph && series.color) || 'none' // Polygon series use filled graph
16240 };
16241
16242 if (prop[3]) {
16243 attribs.dashstyle = prop[3];
16244 } else if (options.linecap !== 'square') {
16245 attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round';
16246 }
16247
16248 graph = series[graphKey]
16249 .attr(attribs)
16250 .shadow((i < 2) && options.shadow); // add shadow to normal series (0) or to first zone (1) #3932
16251
16252 }
16253
16254 // Helpers for animation
16255 if (graph) {
16256 graph.startX = graphPath.xMap;
16257 //graph.shiftUnit = options.step ? 2 : 1;
16258 graph.isArea = graphPath.isArea; // For arearange animation
16259 }
16260 });
16261 },
16262
16263 /**
16264 * Clip the graphs into the positive and negative coloured graphs
16265 */
16266 applyZones: function() {
16267 var series = this,
16268 chart = this.chart,
16269 renderer = chart.renderer,
16270 zones = this.zones,
16271 translatedFrom,
16272 translatedTo,
16273 clips = this.clips || [],
16274 clipAttr,
16275 graph = this.graph,
16276 area = this.area,
16277 chartSizeMax = Math.max(chart.chartWidth, chart.chartHeight),
16278 axis = this[(this.zoneAxis || 'y') + 'Axis'],
16279 extremes,
16280 reversed,
16281 inverted = chart.inverted,
16282 horiz,
16283 pxRange,
16284 pxPosMin,
16285 pxPosMax,
16286 ignoreZones = false;
16287
16288 if (zones.length && (graph || area) && axis && axis.min !== undefined) {
16289 reversed = axis.reversed;
16290 horiz = axis.horiz;
16291 // The use of the Color Threshold assumes there are no gaps
16292 // so it is safe to hide the original graph and area
16293 if (graph) {
16294 graph.hide();
16295 }
16296 if (area) {
16297 area.hide();
16298 }
16299
16300 // Create the clips
16301 extremes = axis.getExtremes();
16302 each(zones, function(threshold, i) {
16303
16304 translatedFrom = reversed ?
16305 (horiz ? chart.plotWidth : 0) :
16306 (horiz ? 0 : axis.toPixels(extremes.min));
16307 translatedFrom = Math.min(Math.max(pick(translatedTo, translatedFrom), 0), chartSizeMax);
16308 translatedTo = Math.min(Math.max(Math.round(axis.toPixels(pick(threshold.value, extremes.max), true)), 0), chartSizeMax);
16309
16310 if (ignoreZones) {
16311 translatedFrom = translatedTo = axis.toPixels(extremes.max);
16312 }
16313
16314 pxRange = Math.abs(translatedFrom - translatedTo);
16315 pxPosMin = Math.min(translatedFrom, translatedTo);
16316 pxPosMax = Math.max(translatedFrom, translatedTo);
16317 if (axis.isXAxis) {
16318 clipAttr = {
16319 x: inverted ? pxPosMax : pxPosMin,
16320 y: 0,
16321 width: pxRange,
16322 height: chartSizeMax
16323 };
16324 if (!horiz) {
16325 clipAttr.x = chart.plotHeight - clipAttr.x;
16326 }
16327 } else {
16328 clipAttr = {
16329 x: 0,
16330 y: inverted ? pxPosMax : pxPosMin,
16331 width: chartSizeMax,
16332 height: pxRange
16333 };
16334 if (horiz) {
16335 clipAttr.y = chart.plotWidth - clipAttr.y;
16336 }
16337 }
16338
16339
16340 /// VML SUPPPORT
16341 if (inverted && renderer.isVML) {
16342 if (axis.isXAxis) {
16343 clipAttr = {
16344 x: 0,
16345 y: reversed ? pxPosMin : pxPosMax,
16346 height: clipAttr.width,
16347 width: chart.chartWidth
16348 };
16349 } else {
16350 clipAttr = {
16351 x: clipAttr.y - chart.plotLeft - chart.spacingBox.x,
16352 y: 0,
16353 width: clipAttr.height,
16354 height: chart.chartHeight
16355 };
16356 }
16357 }
16358 /// END OF VML SUPPORT
16359
16360
16361 if (clips[i]) {
16362 clips[i].animate(clipAttr);
16363 } else {
16364 clips[i] = renderer.clipRect(clipAttr);
16365
16366 if (graph) {
16367 series['zone-graph-' + i].clip(clips[i]);
16368 }
16369
16370 if (area) {
16371 series['zone-area-' + i].clip(clips[i]);
16372 }
16373 }
16374 // if this zone extends out of the axis, ignore the others
16375 ignoreZones = threshold.value > extremes.max;
16376 });
16377 this.clips = clips;
16378 }
16379 },
16380
16381 /**
16382 * Initialize and perform group inversion on series.group and series.markerGroup
16383 */
16384 invertGroups: function(inverted) {
16385 var series = this,
16386 chart = series.chart;
16387
16388 // Pie, go away (#1736)
16389 if (!series.xAxis) {
16390 return;
16391 }
16392
16393 // A fixed size is needed for inversion to work
16394 function setInvert() {
16395 var size = {
16396 width: series.yAxis.len,
16397 height: series.xAxis.len
16398 };
16399
16400 each(['group', 'markerGroup'], function(groupName) {
16401 if (series[groupName]) {
16402 series[groupName].attr(size).invert(inverted);
16403 }
16404 });
16405 }
16406
16407 addEvent(chart, 'resize', setInvert); // do it on resize
16408 addEvent(series, 'destroy', function() {
16409 removeEvent(chart, 'resize', setInvert);
16410 });
16411
16412 // Do it now
16413 setInvert(inverted); // do it now
16414
16415 // On subsequent render and redraw, just do setInvert without setting up events again
16416 series.invertGroups = setInvert;
16417 },
16418
16419 /**
16420 * General abstraction for creating plot groups like series.group, series.dataLabelsGroup and
16421 * series.markerGroup. On subsequent calls, the group will only be adjusted to the updated plot size.
16422 */
16423 plotGroup: function(prop, name, visibility, zIndex, parent) {
16424 var group = this[prop],
16425 isNew = !group;
16426
16427 // Generate it on first call
16428 if (isNew) {
16429 this[prop] = group = this.chart.renderer.g(name)
16430 .attr({
16431 zIndex: zIndex || 0.1 // IE8 and pointer logic use this
16432 })
16433 .add(parent);
16434
16435 group.addClass('highcharts-series-' + this.index + ' highcharts-' + this.type + '-series highcharts-color-' + this.colorIndex +
16436 ' ' + (this.options.className || ''));
16437 }
16438
16439 // Place it on first and subsequent (redraw) calls
16440 group.attr({
16441 visibility: visibility
16442 })[isNew ? 'attr' : 'animate'](this.getPlotBox());
16443 return group;
16444 },
16445
16446 /**
16447 * Get the translation and scale for the plot area of this series
16448 */
16449 getPlotBox: function() {
16450 var chart = this.chart,
16451 xAxis = this.xAxis,
16452 yAxis = this.yAxis;
16453
16454 // Swap axes for inverted (#2339)
16455 if (chart.inverted) {
16456 xAxis = yAxis;
16457 yAxis = this.xAxis;
16458 }
16459 return {
16460 translateX: xAxis ? xAxis.left : chart.plotLeft,
16461 translateY: yAxis ? yAxis.top : chart.plotTop,
16462 scaleX: 1, // #1623
16463 scaleY: 1
16464 };
16465 },
16466
16467 /**
16468 * Render the graph and markers
16469 */
16470 render: function() {
16471 var series = this,
16472 chart = series.chart,
16473 group,
16474 options = series.options,
16475 // Animation doesn't work in IE8 quirks when the group div is hidden,
16476 // and looks bad in other oldIE
16477 animDuration = !!series.animate && chart.renderer.isSVG && animObject(options.animation).duration,
16478 visibility = series.visible ? 'inherit' : 'hidden', // #2597
16479 zIndex = options.zIndex,
16480 hasRendered = series.hasRendered,
16481 chartSeriesGroup = chart.seriesGroup,
16482 inverted = chart.inverted;
16483
16484 // the group
16485 group = series.plotGroup(
16486 'group',
16487 'series',
16488 visibility,
16489 zIndex,
16490 chartSeriesGroup
16491 );
16492
16493 series.markerGroup = series.plotGroup(
16494 'markerGroup',
16495 'markers',
16496 visibility,
16497 zIndex,
16498 chartSeriesGroup
16499 );
16500
16501 // initiate the animation
16502 if (animDuration) {
16503 series.animate(true);
16504 }
16505
16506 // SVGRenderer needs to know this before drawing elements (#1089, #1795)
16507 group.inverted = series.isCartesian ? inverted : false;
16508
16509 // draw the graph if any
16510 if (series.drawGraph) {
16511 series.drawGraph();
16512 series.applyZones();
16513 }
16514
16515 /* each(series.points, function (point) {
16516 if (point.redraw) {
16517 point.redraw();
16518 }
16519 });*/
16520
16521 // draw the data labels (inn pies they go before the points)
16522 if (series.drawDataLabels) {
16523 series.drawDataLabels();
16524 }
16525
16526 // draw the points
16527 if (series.visible) {
16528 series.drawPoints();
16529 }
16530
16531
16532 // draw the mouse tracking area
16533 if (series.drawTracker && series.options.enableMouseTracking !== false) {
16534 series.drawTracker();
16535 }
16536
16537 // Handle inverted series and tracker groups
16538 series.invertGroups(inverted);
16539
16540 // Initial clipping, must be defined after inverting groups for VML. Applies to columns etc. (#3839).
16541 if (options.clip !== false && !series.sharedClipKey && !hasRendered) {
16542 group.clip(chart.clipRect);
16543 }
16544
16545 // Run the animation
16546 if (animDuration) {
16547 series.animate();
16548 }
16549
16550 // Call the afterAnimate function on animation complete (but don't overwrite the animation.complete option
16551 // which should be available to the user).
16552 if (!hasRendered) {
16553 series.animationTimeout = syncTimeout(function() {
16554 series.afterAnimate();
16555 }, animDuration);
16556 }
16557
16558 series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
16559 // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
16560 series.hasRendered = true;
16561 },
16562
16563 /**
16564 * Redraw the series after an update in the axes.
16565 */
16566 redraw: function() {
16567 var series = this,
16568 chart = series.chart,
16569 wasDirty = series.isDirty || series.isDirtyData, // cache it here as it is set to false in render, but used after
16570 group = series.group,
16571 xAxis = series.xAxis,
16572 yAxis = series.yAxis;
16573
16574 // reposition on resize
16575 if (group) {
16576 if (chart.inverted) {
16577 group.attr({
16578 width: chart.plotWidth,
16579 height: chart.plotHeight
16580 });
16581 }
16582
16583 group.animate({
16584 translateX: pick(xAxis && xAxis.left, chart.plotLeft),
16585 translateY: pick(yAxis && yAxis.top, chart.plotTop)
16586 });
16587 }
16588
16589 series.translate();
16590 series.render();
16591 if (wasDirty) { // #3868, #3945
16592 delete this.kdTree;
16593 }
16594 },
16595
16596 /**
16597 * KD Tree && PointSearching Implementation
16598 */
16599
16600 kdDimensions: 1,
16601 kdAxisArray: ['clientX', 'plotY'],
16602
16603 searchPoint: function(e, compareX) {
16604 var series = this,
16605 xAxis = series.xAxis,
16606 yAxis = series.yAxis,
16607 inverted = series.chart.inverted;
16608
16609 return this.searchKDTree({
16610 clientX: inverted ? xAxis.len - e.chartY + xAxis.pos : e.chartX - xAxis.pos,
16611 plotY: inverted ? yAxis.len - e.chartX + yAxis.pos : e.chartY - yAxis.pos
16612 }, compareX);
16613 },
16614
16615 buildKDTree: function() {
16616 var series = this,
16617 dimensions = series.kdDimensions;
16618
16619 // Internal function
16620 function _kdtree(points, depth, dimensions) {
16621 var axis,
16622 median,
16623 length = points && points.length;
16624
16625 if (length) {
16626
16627 // alternate between the axis
16628 axis = series.kdAxisArray[depth % dimensions];
16629
16630 // sort point array
16631 points.sort(function(a, b) {
16632 return a[axis] - b[axis];
16633 });
16634
16635 median = Math.floor(length / 2);
16636
16637 // build and return nod
16638 return {
16639 point: points[median],
16640 left: _kdtree(points.slice(0, median), depth + 1, dimensions),
16641 right: _kdtree(points.slice(median + 1), depth + 1, dimensions)
16642 };
16643
16644 }
16645 }
16646
16647 // Start the recursive build process with a clone of the points array and null points filtered out (#3873)
16648 function startRecursive() {
16649 series.kdTree = _kdtree(
16650 series.getValidPoints(
16651 null, !series.directTouch // For line-type series restrict to plot area, but column-type series not (#3916, #4511)
16652 ),
16653 dimensions,
16654 dimensions
16655 );
16656 }
16657 delete series.kdTree;
16658
16659 // For testing tooltips, don't build async
16660 syncTimeout(startRecursive, series.options.kdNow ? 0 : 1);
16661 },
16662
16663 searchKDTree: function(point, compareX) {
16664 var series = this,
16665 kdX = this.kdAxisArray[0],
16666 kdY = this.kdAxisArray[1],
16667 kdComparer = compareX ? 'distX' : 'dist';
16668
16669 // Set the one and two dimensional distance on the point object
16670 function setDistance(p1, p2) {
16671 var x = (defined(p1[kdX]) && defined(p2[kdX])) ? Math.pow(p1[kdX] - p2[kdX], 2) : null,
16672 y = (defined(p1[kdY]) && defined(p2[kdY])) ? Math.pow(p1[kdY] - p2[kdY], 2) : null,
16673 r = (x || 0) + (y || 0);
16674
16675 p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE;
16676 p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE;
16677 }
16678
16679 function _search(search, tree, depth, dimensions) {
16680 var point = tree.point,
16681 axis = series.kdAxisArray[depth % dimensions],
16682 tdist,
16683 sideA,
16684 sideB,
16685 ret = point,
16686 nPoint1,
16687 nPoint2;
16688
16689 setDistance(search, point);
16690
16691 // Pick side based on distance to splitting point
16692 tdist = search[axis] - point[axis];
16693 sideA = tdist < 0 ? 'left' : 'right';
16694 sideB = tdist < 0 ? 'right' : 'left';
16695
16696 // End of tree
16697 if (tree[sideA]) {
16698 nPoint1 = _search(search, tree[sideA], depth + 1, dimensions);
16699
16700 ret = (nPoint1[kdComparer] < ret[kdComparer] ? nPoint1 : point);
16701 }
16702 if (tree[sideB]) {
16703 // compare distance to current best to splitting point to decide wether to check side B or not
16704 if (Math.sqrt(tdist * tdist) < ret[kdComparer]) {
16705 nPoint2 = _search(search, tree[sideB], depth + 1, dimensions);
16706 ret = (nPoint2[kdComparer] < ret[kdComparer] ? nPoint2 : ret);
16707 }
16708 }
16709
16710 return ret;
16711 }
16712
16713 if (!this.kdTree) {
16714 this.buildKDTree();
16715 }
16716
16717 if (this.kdTree) {
16718 return _search(point,
16719 this.kdTree, this.kdDimensions, this.kdDimensions);
16720 }
16721 }
16722
16723 }); // end Series prototype
16724
16725 }(Highcharts));
16726 (function(H) {
16727 /**
16728 * (c) 2010-2016 Torstein Honsi
16729 *
16730 * License: www.highcharts.com/license
16731 */
16732 'use strict';
16733 var Axis = H.Axis,
16734 Chart = H.Chart,
16735 correctFloat = H.correctFloat,
16736 defined = H.defined,
16737 destroyObjectProperties = H.destroyObjectProperties,
16738 each = H.each,
16739 format = H.format,
16740 pick = H.pick,
16741 Series = H.Series;
16742 /**
16743 * The class for stack items
16744 */
16745 function StackItem(axis, options, isNegative, x, stackOption) {
16746
16747 var inverted = axis.chart.inverted;
16748
16749 this.axis = axis;
16750
16751 // Tells if the stack is negative
16752 this.isNegative = isNegative;
16753
16754 // Save the options to be able to style the label
16755 this.options = options;
16756
16757 // Save the x value to be able to position the label later
16758 this.x = x;
16759
16760 // Initialize total value
16761 this.total = null;
16762
16763 // This will keep each points' extremes stored by series.index and point index
16764 this.points = {};
16765
16766 // Save the stack option on the series configuration object, and whether to treat it as percent
16767 this.stack = stackOption;
16768 this.leftCliff = 0;
16769 this.rightCliff = 0;
16770
16771 // The align options and text align varies on whether the stack is negative and
16772 // if the chart is inverted or not.
16773 // First test the user supplied value, then use the dynamic.
16774 this.alignOptions = {
16775 align: options.align || (inverted ? (isNegative ? 'left' : 'right') : 'center'),
16776 verticalAlign: options.verticalAlign || (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')),
16777 y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)),
16778 x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0)
16779 };
16780
16781 this.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center');
16782 }
16783
16784 StackItem.prototype = {
16785 destroy: function() {
16786 destroyObjectProperties(this, this.axis);
16787 },
16788
16789 /**
16790 * Renders the stack total label and adds it to the stack label group.
16791 */
16792 render: function(group) {
16793 var options = this.options,
16794 formatOption = options.format,
16795 str = formatOption ?
16796 format(formatOption, this) :
16797 options.formatter.call(this); // format the text in the label
16798
16799 // Change the text to reflect the new total and set visibility to hidden in case the serie is hidden
16800 if (this.label) {
16801 this.label.attr({
16802 text: str,
16803 visibility: 'hidden'
16804 });
16805 // Create new label
16806 } else {
16807 this.label =
16808 this.axis.chart.renderer.text(str, null, null, options.useHTML) // dummy positions, actual position updated with setOffset method in columnseries
16809 .css(options.style) // apply style
16810 .attr({
16811 align: this.textAlign, // fix the text-anchor
16812 rotation: options.rotation, // rotation
16813 visibility: 'hidden' // hidden until setOffset is called
16814 })
16815 .add(group); // add to the labels-group
16816 }
16817 },
16818
16819 /**
16820 * Sets the offset that the stack has from the x value and repositions the label.
16821 */
16822 setOffset: function(xOffset, xWidth) {
16823 var stackItem = this,
16824 axis = stackItem.axis,
16825 chart = axis.chart,
16826 inverted = chart.inverted,
16827 reversed = axis.reversed,
16828 neg = (this.isNegative && !reversed) || (!this.isNegative && reversed), // #4056
16829 y = axis.translate(axis.usePercentage ? 100 : this.total, 0, 0, 0, 1), // stack value translated mapped to chart coordinates
16830 yZero = axis.translate(0), // stack origin
16831 h = Math.abs(y - yZero), // stack height
16832 x = chart.xAxis[0].translate(this.x) + xOffset, // stack x position
16833 plotHeight = chart.plotHeight,
16834 stackBox = { // this is the box for the complete stack
16835 x: inverted ? (neg ? y : y - h) : x,
16836 y: inverted ? plotHeight - x - xWidth : (neg ? (plotHeight - y - h) : plotHeight - y),
16837 width: inverted ? h : xWidth,
16838 height: inverted ? xWidth : h
16839 },
16840 label = this.label,
16841 alignAttr;
16842
16843 if (label) {
16844 label.align(this.alignOptions, null, stackBox); // align the label to the box
16845
16846 // Set visibility (#678)
16847 alignAttr = label.alignAttr;
16848 label[this.options.crop === false || chart.isInsidePlot(alignAttr.x, alignAttr.y) ? 'show' : 'hide'](true);
16849 }
16850 }
16851 };
16852
16853 /**
16854 * Generate stacks for each series and calculate stacks total values
16855 */
16856 Chart.prototype.getStacks = function() {
16857 var chart = this;
16858
16859 // reset stacks for each yAxis
16860 each(chart.yAxis, function(axis) {
16861 if (axis.stacks && axis.hasVisibleSeries) {
16862 axis.oldStacks = axis.stacks;
16863 }
16864 });
16865
16866 each(chart.series, function(series) {
16867 if (series.options.stacking && (series.visible === true || chart.options.chart.ignoreHiddenSeries === false)) {
16868 series.stackKey = series.type + pick(series.options.stack, '');
16869 }
16870 });
16871 };
16872
16873
16874 // Stacking methods defined on the Axis prototype
16875
16876 /**
16877 * Build the stacks from top down
16878 */
16879 Axis.prototype.buildStacks = function() {
16880 var axisSeries = this.series,
16881 series,
16882 reversedStacks = pick(this.options.reversedStacks, true),
16883 len = axisSeries.length,
16884 i;
16885 if (!this.isXAxis) {
16886 this.usePercentage = false;
16887 i = len;
16888 while (i--) {
16889 axisSeries[reversedStacks ? i : len - i - 1].setStackedPoints();
16890 }
16891
16892 i = len;
16893 while (i--) {
16894 series = axisSeries[reversedStacks ? i : len - i - 1];
16895 if (series.setStackCliffs) {
16896 series.setStackCliffs();
16897 }
16898 }
16899 // Loop up again to compute percent stack
16900 if (this.usePercentage) {
16901 for (i = 0; i < len; i++) {
16902 axisSeries[i].setPercentStacks();
16903 }
16904 }
16905 }
16906 };
16907
16908 Axis.prototype.renderStackTotals = function() {
16909 var axis = this,
16910 chart = axis.chart,
16911 renderer = chart.renderer,
16912 stacks = axis.stacks,
16913 stackKey,
16914 oneStack,
16915 stackCategory,
16916 stackTotalGroup = axis.stackTotalGroup;
16917
16918 // Create a separate group for the stack total labels
16919 if (!stackTotalGroup) {
16920 axis.stackTotalGroup = stackTotalGroup =
16921 renderer.g('stack-labels')
16922 .attr({
16923 visibility: 'visible',
16924 zIndex: 6
16925 })
16926 .add();
16927 }
16928
16929 // plotLeft/Top will change when y axis gets wider so we need to translate the
16930 // stackTotalGroup at every render call. See bug #506 and #516
16931 stackTotalGroup.translate(chart.plotLeft, chart.plotTop);
16932
16933 // Render each stack total
16934 for (stackKey in stacks) {
16935 oneStack = stacks[stackKey];
16936 for (stackCategory in oneStack) {
16937 oneStack[stackCategory].render(stackTotalGroup);
16938 }
16939 }
16940 };
16941
16942 /**
16943 * Set all the stacks to initial states and destroy unused ones.
16944 */
16945 Axis.prototype.resetStacks = function() {
16946 var stacks = this.stacks,
16947 type,
16948 i;
16949 if (!this.isXAxis) {
16950 for (type in stacks) {
16951 for (i in stacks[type]) {
16952
16953 // Clean up memory after point deletion (#1044, #4320)
16954 if (stacks[type][i].touched < this.stacksTouched) {
16955 stacks[type][i].destroy();
16956 delete stacks[type][i];
16957
16958 // Reset stacks
16959 } else {
16960 stacks[type][i].total = null;
16961 stacks[type][i].cum = 0;
16962 }
16963 }
16964 }
16965 }
16966 };
16967
16968 Axis.prototype.cleanStacks = function() {
16969 var stacks, type, i;
16970
16971 if (!this.isXAxis) {
16972 if (this.oldStacks) {
16973 stacks = this.stacks = this.oldStacks;
16974 }
16975
16976 // reset stacks
16977 for (type in stacks) {
16978 for (i in stacks[type]) {
16979 stacks[type][i].cum = stacks[type][i].total;
16980 }
16981 }
16982 }
16983 };
16984
16985
16986 // Stacking methods defnied for Series prototype
16987
16988 /**
16989 * Adds series' points value to corresponding stack
16990 */
16991 Series.prototype.setStackedPoints = function() {
16992 if (!this.options.stacking || (this.visible !== true && this.chart.options.chart.ignoreHiddenSeries !== false)) {
16993 return;
16994 }
16995
16996 var series = this,
16997 xData = series.processedXData,
16998 yData = series.processedYData,
16999 stackedYData = [],
17000 yDataLength = yData.length,
17001 seriesOptions = series.options,
17002 threshold = seriesOptions.threshold,
17003 stackThreshold = seriesOptions.startFromThreshold ? threshold : 0,
17004 stackOption = seriesOptions.stack,
17005 stacking = seriesOptions.stacking,
17006 stackKey = series.stackKey,
17007 negKey = '-' + stackKey,
17008 negStacks = series.negStacks,
17009 yAxis = series.yAxis,
17010 stacks = yAxis.stacks,
17011 oldStacks = yAxis.oldStacks,
17012 stackIndicator,
17013 isNegative,
17014 stack,
17015 other,
17016 key,
17017 pointKey,
17018 i,
17019 x,
17020 y;
17021
17022
17023 yAxis.stacksTouched += 1;
17024
17025 // loop over the non-null y values and read them into a local array
17026 for (i = 0; i < yDataLength; i++) {
17027 x = xData[i];
17028 y = yData[i];
17029 stackIndicator = series.getStackIndicator(stackIndicator, x, series.index);
17030 pointKey = stackIndicator.key;
17031 // Read stacked values into a stack based on the x value,
17032 // the sign of y and the stack key. Stacking is also handled for null values (#739)
17033 isNegative = negStacks && y < (stackThreshold ? 0 : threshold);
17034 key = isNegative ? negKey : stackKey;
17035
17036 // Create empty object for this stack if it doesn't exist yet
17037 if (!stacks[key]) {
17038 stacks[key] = {};
17039 }
17040
17041 // Initialize StackItem for this x
17042 if (!stacks[key][x]) {
17043 if (oldStacks[key] && oldStacks[key][x]) {
17044 stacks[key][x] = oldStacks[key][x];
17045 stacks[key][x].total = null;
17046 } else {
17047 stacks[key][x] = new StackItem(yAxis, yAxis.options.stackLabels, isNegative, x, stackOption);
17048 }
17049 }
17050
17051 // If the StackItem doesn't exist, create it first
17052 stack = stacks[key][x];
17053 if (y !== null) {
17054 stack.points[pointKey] = stack.points[series.index] = [pick(stack.cum, stackThreshold)];
17055
17056 // Record the base of the stack
17057 if (!defined(stack.cum)) {
17058 stack.base = pointKey;
17059 }
17060 stack.touched = yAxis.stacksTouched;
17061
17062
17063 // In area charts, if there are multiple points on the same X value, let the
17064 // area fill the full span of those points
17065 if (stackIndicator.index > 0 && series.singleStacks === false) {
17066 stack.points[pointKey][0] = stack.points[series.index + ',' + x + ',0'][0];
17067 }
17068 }
17069
17070 // Add value to the stack total
17071 if (stacking === 'percent') {
17072
17073 // Percent stacked column, totals are the same for the positive and negative stacks
17074 other = isNegative ? stackKey : negKey;
17075 if (negStacks && stacks[other] && stacks[other][x]) {
17076 other = stacks[other][x];
17077 stack.total = other.total = Math.max(other.total, stack.total) + Math.abs(y) || 0;
17078
17079 // Percent stacked areas
17080 } else {
17081 stack.total = correctFloat(stack.total + (Math.abs(y) || 0));
17082 }
17083 } else {
17084 stack.total = correctFloat(stack.total + (y || 0));
17085 }
17086
17087 stack.cum = pick(stack.cum, stackThreshold) + (y || 0);
17088
17089 if (y !== null) {
17090 stack.points[pointKey].push(stack.cum);
17091 stackedYData[i] = stack.cum;
17092 }
17093
17094 }
17095
17096 if (stacking === 'percent') {
17097 yAxis.usePercentage = true;
17098 }
17099
17100 this.stackedYData = stackedYData; // To be used in getExtremes
17101
17102 // Reset old stacks
17103 yAxis.oldStacks = {};
17104 };
17105
17106 /**
17107 * Iterate over all stacks and compute the absolute values to percent
17108 */
17109 Series.prototype.setPercentStacks = function() {
17110 var series = this,
17111 stackKey = series.stackKey,
17112 stacks = series.yAxis.stacks,
17113 processedXData = series.processedXData,
17114 stackIndicator;
17115
17116 each([stackKey, '-' + stackKey], function(key) {
17117 var i = processedXData.length,
17118 x,
17119 stack,
17120 pointExtremes,
17121 totalFactor;
17122
17123 while (i--) {
17124 x = processedXData[i];
17125 stackIndicator = series.getStackIndicator(stackIndicator, x, series.index);
17126 stack = stacks[key] && stacks[key][x];
17127 pointExtremes = stack && stack.points[stackIndicator.key];
17128 if (pointExtremes) {
17129 totalFactor = stack.total ? 100 / stack.total : 0;
17130 pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor); // Y bottom value
17131 pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor); // Y value
17132 series.stackedYData[i] = pointExtremes[1];
17133 }
17134 }
17135 });
17136 };
17137
17138 /**
17139 * Get stack indicator, according to it's x-value, to determine points with the same x-value
17140 */
17141 Series.prototype.getStackIndicator = function(stackIndicator, x, index) {
17142 if (!defined(stackIndicator) || stackIndicator.x !== x) {
17143 stackIndicator = {
17144 x: x,
17145 index: 0
17146 };
17147 } else {
17148 stackIndicator.index++;
17149 }
17150
17151 stackIndicator.key = [index, x, stackIndicator.index].join(',');
17152
17153 return stackIndicator;
17154 };
17155
17156 }(Highcharts));
17157 (function(H) {
17158 /**
17159 * (c) 2010-2016 Torstein Honsi
17160 *
17161 * License: www.highcharts.com/license
17162 */
17163 'use strict';
17164 var addEvent = H.addEvent,
17165 animate = H.animate,
17166 Axis = H.Axis,
17167 Chart = H.Chart,
17168 createElement = H.createElement,
17169 css = H.css,
17170 defined = H.defined,
17171 each = H.each,
17172 erase = H.erase,
17173 extend = H.extend,
17174 fireEvent = H.fireEvent,
17175 inArray = H.inArray,
17176 isObject = H.isObject,
17177 merge = H.merge,
17178 pick = H.pick,
17179 Point = H.Point,
17180 Series = H.Series,
17181 seriesTypes = H.seriesTypes,
17182 setAnimation = H.setAnimation,
17183 splat = H.splat;
17184
17185 // Extend the Chart prototype for dynamic methods
17186 extend(Chart.prototype, {
17187
17188 /**
17189 * Add a series dynamically after time
17190 *
17191 * @param {Object} options The config options
17192 * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true.
17193 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
17194 * configuration
17195 *
17196 * @return {Object} series The newly created series object
17197 */
17198 addSeries: function(options, redraw, animation) {
17199 var series,
17200 chart = this;
17201
17202 if (options) {
17203 redraw = pick(redraw, true); // defaults to true
17204
17205 fireEvent(chart, 'addSeries', {
17206 options: options
17207 }, function() {
17208 series = chart.initSeries(options);
17209
17210 chart.isDirtyLegend = true; // the series array is out of sync with the display
17211 chart.linkSeries();
17212 if (redraw) {
17213 chart.redraw(animation);
17214 }
17215 });
17216 }
17217
17218 return series;
17219 },
17220
17221 /**
17222 * Add an axis to the chart
17223 * @param {Object} options The axis option
17224 * @param {Boolean} isX Whether it is an X axis or a value axis
17225 */
17226 addAxis: function(options, isX, redraw, animation) {
17227 var key = isX ? 'xAxis' : 'yAxis',
17228 chartOptions = this.options,
17229 userOptions = merge(options, {
17230 index: this[key].length,
17231 isX: isX
17232 });
17233
17234 new Axis(this, userOptions); // eslint-disable-line no-new
17235
17236 // Push the new axis options to the chart options
17237 chartOptions[key] = splat(chartOptions[key] || {});
17238 chartOptions[key].push(userOptions);
17239
17240 if (pick(redraw, true)) {
17241 this.redraw(animation);
17242 }
17243 },
17244
17245 /**
17246 * Dim the chart and show a loading text or symbol
17247 * @param {String} str An optional text to show in the loading label instead of the default one
17248 */
17249 showLoading: function(str) {
17250 var chart = this,
17251 options = chart.options,
17252 loadingDiv = chart.loadingDiv,
17253 loadingOptions = options.loading,
17254 setLoadingSize = function() {
17255 if (loadingDiv) {
17256 css(loadingDiv, {
17257 left: chart.plotLeft + 'px',
17258 top: chart.plotTop + 'px',
17259 width: chart.plotWidth + 'px',
17260 height: chart.plotHeight + 'px'
17261 });
17262 }
17263 };
17264
17265 // create the layer at the first call
17266 if (!loadingDiv) {
17267 chart.loadingDiv = loadingDiv = createElement('div', {
17268 className: 'highcharts-loading highcharts-loading-hidden'
17269 }, null, chart.container);
17270
17271 chart.loadingSpan = createElement(
17272 'span', {
17273 className: 'highcharts-loading-inner'
17274 },
17275 null,
17276 loadingDiv
17277 );
17278 addEvent(chart, 'redraw', setLoadingSize); // #1080
17279 }
17280 setTimeout(function() {
17281 loadingDiv.className = 'highcharts-loading';
17282 });
17283
17284 // Update text
17285 chart.loadingSpan.innerHTML = str || options.lang.loading;
17286
17287
17288 // Update visuals
17289 css(loadingDiv, extend(loadingOptions.style, {
17290 zIndex: 10
17291 }));
17292 css(chart.loadingSpan, loadingOptions.labelStyle);
17293
17294 // Show it
17295 if (!chart.loadingShown) {
17296 css(loadingDiv, {
17297 opacity: 0,
17298 display: ''
17299 });
17300 animate(loadingDiv, {
17301 opacity: loadingOptions.style.opacity || 0.5
17302 }, {
17303 duration: loadingOptions.showDuration || 0
17304 });
17305 }
17306
17307
17308 chart.loadingShown = true;
17309 setLoadingSize();
17310 },
17311
17312 /**
17313 * Hide the loading layer
17314 */
17315 hideLoading: function() {
17316 var options = this.options,
17317 loadingDiv = this.loadingDiv;
17318
17319 if (loadingDiv) {
17320 loadingDiv.className = 'highcharts-loading highcharts-loading-hidden';
17321
17322 animate(loadingDiv, {
17323 opacity: 0
17324 }, {
17325 duration: options.loading.hideDuration || 100,
17326 complete: function() {
17327 css(loadingDiv, {
17328 display: 'none'
17329 });
17330 }
17331 });
17332
17333 }
17334 this.loadingShown = false;
17335 },
17336
17337 /**
17338 * These properties cause isDirtyBox to be set to true when updating. Can be extended from plugins.
17339 */
17340 propsRequireDirtyBox: ['backgroundColor', 'borderColor', 'borderWidth', 'margin', 'marginTop', 'marginRight',
17341 'marginBottom', 'marginLeft', 'spacing', 'spacingTop', 'spacingRight', 'spacingBottom', 'spacingLeft',
17342 'borderRadius', 'plotBackgroundColor', 'plotBackgroundImage', 'plotBorderColor', 'plotBorderWidth',
17343 'plotShadow', 'shadow'
17344 ],
17345
17346 /**
17347 * These properties cause all series to be updated when updating. Can be extended from plugins.
17348 */
17349 propsRequireUpdateSeries: ['chart.polar', 'chart.ignoreHiddenSeries', 'chart.type', 'colors', 'plotOptions'],
17350
17351 /**
17352 * Chart.update function that takes the whole options stucture.
17353 */
17354 update: function(options, redraw) {
17355 var key,
17356 adders = {
17357 credits: 'addCredits',
17358 title: 'setTitle',
17359 subtitle: 'setSubtitle'
17360 },
17361 optionsChart = options.chart,
17362 updateAllAxes,
17363 updateAllSeries;
17364
17365 // If the top-level chart option is present, some special updates are required
17366 if (optionsChart) {
17367 merge(true, this.options.chart, optionsChart);
17368
17369 // Setter function
17370 if ('className' in optionsChart) {
17371 this.setClassName(optionsChart.className);
17372 }
17373
17374 if ('inverted' in optionsChart || 'polar' in optionsChart) {
17375 this.propFromSeries(); // Parses options.chart.inverted and options.chart.polar together with the available series
17376 updateAllAxes = true;
17377 }
17378
17379 for (key in optionsChart) {
17380 if (optionsChart.hasOwnProperty(key)) {
17381 if (inArray('chart.' + key, this.propsRequireUpdateSeries) !== -1) {
17382 updateAllSeries = true;
17383 }
17384 // Only dirty box
17385 if (inArray(key, this.propsRequireDirtyBox) !== -1) {
17386 this.isDirtyBox = true;
17387 }
17388
17389 }
17390 }
17391
17392
17393 if ('style' in optionsChart) {
17394 this.renderer.setStyle(optionsChart.style);
17395 }
17396
17397 }
17398
17399 // Some option stuctures correspond one-to-one to chart objects that have
17400 // update methods, for example
17401 // options.credits => chart.credits
17402 // options.legend => chart.legend
17403 // options.title => chart.title
17404 // options.tooltip => chart.tooltip
17405 // options.subtitle => chart.subtitle
17406 // options.navigator => chart.navigator
17407 // options.scrollbar => chart.scrollbar
17408 for (key in options) {
17409 if (this[key] && typeof this[key].update === 'function') {
17410 this[key].update(options[key], false);
17411
17412 // If a one-to-one object does not exist, look for an adder function
17413 } else if (typeof this[adders[key]] === 'function') {
17414 this[adders[key]](options[key]);
17415 }
17416
17417 if (key !== 'chart' && inArray(key, this.propsRequireUpdateSeries) !== -1) {
17418 updateAllSeries = true;
17419 }
17420 }
17421
17422
17423 if (options.colors) {
17424 this.options.colors = options.colors;
17425 }
17426
17427
17428 if (options.plotOptions) {
17429 merge(true, this.options.plotOptions, options.plotOptions);
17430 }
17431
17432 // Setters for collections. For axes and series, each item is referred by an id. If the
17433 // id is not found, it defaults to the first item in the collection, so setting series
17434 // without an id, will update the first series in the chart.
17435 each(['xAxis', 'yAxis', 'series'], function(coll) {
17436 if (options[coll]) {
17437 each(splat(options[coll]), function(newOptions) {
17438 var item = (defined(newOptions.id) && this.get(newOptions.id)) || this[coll][0];
17439 if (item && item.coll === coll) {
17440 item.update(newOptions, false);
17441 }
17442 }, this);
17443 }
17444 }, this);
17445
17446 if (updateAllAxes) {
17447 each(this.axes, function(axis) {
17448 axis.update({}, false);
17449 });
17450 }
17451
17452 // Certain options require the whole series structure to be thrown away
17453 // and rebuilt
17454 if (updateAllSeries) {
17455 each(this.series, function(series) {
17456 series.update({}, false);
17457 });
17458 }
17459
17460 // For loading, just update the options, do not redraw
17461 if (options.loading) {
17462 merge(true, this.options.loading, options.loading);
17463 }
17464
17465 // Update size. Redraw is forced.
17466 if (optionsChart && ('width' in optionsChart || 'height' in optionsChart)) {
17467 this.setSize(optionsChart.width, optionsChart.height);
17468 } else if (pick(redraw, true)) {
17469 this.redraw();
17470 }
17471 },
17472
17473 /**
17474 * Setter function to allow use from chart.update
17475 */
17476 setSubtitle: function(options) {
17477 this.setTitle(undefined, options);
17478 }
17479
17480
17481 });
17482
17483 // extend the Point prototype for dynamic methods
17484 extend(Point.prototype, {
17485 /**
17486 * Point.update with new options (typically x/y data) and optionally redraw the series.
17487 *
17488 * @param {Object} options Point options as defined in the series.data array
17489 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
17490 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
17491 * configuration
17492 */
17493 update: function(options, redraw, animation, runEvent) {
17494 var point = this,
17495 series = point.series,
17496 graphic = point.graphic,
17497 i,
17498 chart = series.chart,
17499 seriesOptions = series.options;
17500
17501 redraw = pick(redraw, true);
17502
17503 function update() {
17504
17505 point.applyOptions(options);
17506
17507 // Update visuals
17508 if (point.y === null && graphic) { // #4146
17509 point.graphic = graphic.destroy();
17510 }
17511 if (isObject(options, true)) {
17512 // Destroy so we can get new elements
17513 if (graphic && graphic.element) {
17514 if (options && options.marker && options.marker.symbol) {
17515 point.graphic = graphic.destroy();
17516 }
17517 }
17518 if (options && options.dataLabels && point.dataLabel) { // #2468
17519 point.dataLabel = point.dataLabel.destroy();
17520 }
17521 }
17522
17523 // record changes in the parallel arrays
17524 i = point.index;
17525 series.updateParallelArrays(point, i);
17526
17527 // Record the options to options.data. If there is an object from before,
17528 // use point options, otherwise use raw options. (#4701)
17529 seriesOptions.data[i] = isObject(seriesOptions.data[i], true) ? point.options : options;
17530
17531 // redraw
17532 series.isDirty = series.isDirtyData = true;
17533 if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320
17534 chart.isDirtyBox = true;
17535 }
17536
17537 if (seriesOptions.legendType === 'point') { // #1831, #1885
17538 chart.isDirtyLegend = true;
17539 }
17540 if (redraw) {
17541 chart.redraw(animation);
17542 }
17543 }
17544
17545 // Fire the event with a default handler of doing the update
17546 if (runEvent === false) { // When called from setData
17547 update();
17548 } else {
17549 point.firePointEvent('update', {
17550 options: options
17551 }, update);
17552 }
17553 },
17554
17555 /**
17556 * Remove a point and optionally redraw the series and if necessary the axes
17557 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
17558 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
17559 * configuration
17560 */
17561 remove: function(redraw, animation) {
17562 this.series.removePoint(inArray(this, this.series.data), redraw, animation);
17563 }
17564 });
17565
17566 // Extend the series prototype for dynamic methods
17567 extend(Series.prototype, {
17568 /**
17569 * Add a point dynamically after chart load time
17570 * @param {Object} options Point options as given in series.data
17571 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
17572 * @param {Boolean} shift If shift is true, a point is shifted off the start
17573 * of the series as one is appended to the end.
17574 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
17575 * configuration
17576 */
17577 addPoint: function(options, redraw, shift, animation) {
17578 var series = this,
17579 seriesOptions = series.options,
17580 data = series.data,
17581 chart = series.chart,
17582 names = series.xAxis && series.xAxis.names,
17583 dataOptions = seriesOptions.data,
17584 point,
17585 isInTheMiddle,
17586 xData = series.xData,
17587 i,
17588 x;
17589
17590 // Optional redraw, defaults to true
17591 redraw = pick(redraw, true);
17592
17593 // Get options and push the point to xData, yData and series.options. In series.generatePoints
17594 // the Point instance will be created on demand and pushed to the series.data array.
17595 point = {
17596 series: series
17597 };
17598 series.pointClass.prototype.applyOptions.apply(point, [options]);
17599 x = point.x;
17600
17601 // Get the insertion point
17602 i = xData.length;
17603 if (series.requireSorting && x < xData[i - 1]) {
17604 isInTheMiddle = true;
17605 while (i && xData[i - 1] > x) {
17606 i--;
17607 }
17608 }
17609
17610 series.updateParallelArrays(point, 'splice', i, 0, 0); // insert undefined item
17611 series.updateParallelArrays(point, i); // update it
17612
17613 if (names && point.name) {
17614 names[x] = point.name;
17615 }
17616 dataOptions.splice(i, 0, options);
17617
17618 if (isInTheMiddle) {
17619 series.data.splice(i, 0, null);
17620 series.processData();
17621 }
17622
17623 // Generate points to be added to the legend (#1329)
17624 if (seriesOptions.legendType === 'point') {
17625 series.generatePoints();
17626 }
17627
17628 // Shift the first point off the parallel arrays
17629 if (shift) {
17630 if (data[0] && data[0].remove) {
17631 data[0].remove(false);
17632 } else {
17633 data.shift();
17634 series.updateParallelArrays(point, 'shift');
17635
17636 dataOptions.shift();
17637 }
17638 }
17639
17640 // redraw
17641 series.isDirty = true;
17642 series.isDirtyData = true;
17643
17644 if (redraw) {
17645
17646 chart.redraw(animation); // Animation is set anyway on redraw, #5665
17647 }
17648 },
17649
17650 /**
17651 * Remove a point (rendered or not), by index
17652 */
17653 removePoint: function(i, redraw, animation) {
17654
17655 var series = this,
17656 data = series.data,
17657 point = data[i],
17658 points = series.points,
17659 chart = series.chart,
17660 remove = function() {
17661
17662 if (points && points.length === data.length) { // #4935
17663 points.splice(i, 1);
17664 }
17665 data.splice(i, 1);
17666 series.options.data.splice(i, 1);
17667 series.updateParallelArrays(point || {
17668 series: series
17669 }, 'splice', i, 1);
17670
17671 if (point) {
17672 point.destroy();
17673 }
17674
17675 // redraw
17676 series.isDirty = true;
17677 series.isDirtyData = true;
17678 if (redraw) {
17679 chart.redraw();
17680 }
17681 };
17682
17683 setAnimation(animation, chart);
17684 redraw = pick(redraw, true);
17685
17686 // Fire the event with a default handler of removing the point
17687 if (point) {
17688 point.firePointEvent('remove', null, remove);
17689 } else {
17690 remove();
17691 }
17692 },
17693
17694 /**
17695 * Remove a series and optionally redraw the chart
17696 *
17697 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
17698 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
17699 * configuration
17700 */
17701 remove: function(redraw, animation, withEvent) {
17702 var series = this,
17703 chart = series.chart;
17704
17705 function remove() {
17706
17707 // Destroy elements
17708 series.destroy();
17709
17710 // Redraw
17711 chart.isDirtyLegend = chart.isDirtyBox = true;
17712 chart.linkSeries();
17713
17714 if (pick(redraw, true)) {
17715 chart.redraw(animation);
17716 }
17717 }
17718
17719 // Fire the event with a default handler of removing the point
17720 if (withEvent !== false) {
17721 fireEvent(series, 'remove', null, remove);
17722 } else {
17723 remove();
17724 }
17725 },
17726
17727 /**
17728 * Series.update with a new set of options
17729 */
17730 update: function(newOptions, redraw) {
17731 var series = this,
17732 chart = this.chart,
17733 // must use user options when changing type because this.options is merged
17734 // in with type specific plotOptions
17735 oldOptions = this.userOptions,
17736 oldType = this.type,
17737 newType = newOptions.type || oldOptions.type || chart.options.chart.type,
17738 proto = seriesTypes[oldType].prototype,
17739 preserve = ['group', 'markerGroup', 'dataLabelsGroup'],
17740 n;
17741
17742 // If we're changing type or zIndex, create new groups (#3380, #3404)
17743 if ((newType && newType !== oldType) || newOptions.zIndex !== undefined) {
17744 preserve.length = 0;
17745 }
17746
17747 // Make sure groups are not destroyed (#3094)
17748 each(preserve, function(prop) {
17749 preserve[prop] = series[prop];
17750 delete series[prop];
17751 });
17752
17753 // Do the merge, with some forced options
17754 newOptions = merge(oldOptions, {
17755 animation: false,
17756 index: this.index,
17757 pointStart: this.xData[0] // when updating after addPoint
17758 }, {
17759 data: this.options.data
17760 }, newOptions);
17761
17762 // Destroy the series and delete all properties. Reinsert all methods
17763 // and properties from the new type prototype (#2270, #3719)
17764 this.remove(false, null, false);
17765 for (n in proto) {
17766 this[n] = undefined;
17767 }
17768 extend(this, seriesTypes[newType || oldType].prototype);
17769
17770 // Re-register groups (#3094)
17771 each(preserve, function(prop) {
17772 series[prop] = preserve[prop];
17773 });
17774
17775 this.init(chart, newOptions);
17776 chart.linkSeries(); // Links are lost in this.remove (#3028)
17777 if (pick(redraw, true)) {
17778 chart.redraw(false);
17779 }
17780 }
17781 });
17782
17783 // Extend the Axis.prototype for dynamic methods
17784 extend(Axis.prototype, {
17785
17786 /**
17787 * Axis.update with a new options structure
17788 */
17789 update: function(newOptions, redraw) {
17790 var chart = this.chart;
17791
17792 newOptions = chart.options[this.coll][this.options.index] = merge(this.userOptions, newOptions);
17793
17794 this.destroy(true);
17795
17796 this.init(chart, extend(newOptions, {
17797 events: undefined
17798 }));
17799
17800 chart.isDirtyBox = true;
17801 if (pick(redraw, true)) {
17802 chart.redraw();
17803 }
17804 },
17805
17806 /**
17807 * Remove the axis from the chart
17808 */
17809 remove: function(redraw) {
17810 var chart = this.chart,
17811 key = this.coll, // xAxis or yAxis
17812 axisSeries = this.series,
17813 i = axisSeries.length;
17814
17815 // Remove associated series (#2687)
17816 while (i--) {
17817 if (axisSeries[i]) {
17818 axisSeries[i].remove(false);
17819 }
17820 }
17821
17822 // Remove the axis
17823 erase(chart.axes, this);
17824 erase(chart[key], this);
17825 chart.options[key].splice(this.options.index, 1);
17826 each(chart[key], function(axis, i) { // Re-index, #1706
17827 axis.options.index = i;
17828 });
17829 this.destroy();
17830 chart.isDirtyBox = true;
17831
17832 if (pick(redraw, true)) {
17833 chart.redraw();
17834 }
17835 },
17836
17837 /**
17838 * Update the axis title by options
17839 */
17840 setTitle: function(newTitleOptions, redraw) {
17841 this.update({
17842 title: newTitleOptions
17843 }, redraw);
17844 },
17845
17846 /**
17847 * Set new axis categories and optionally redraw
17848 * @param {Array} categories
17849 * @param {Boolean} redraw
17850 */
17851 setCategories: function(categories, redraw) {
17852 this.update({
17853 categories: categories
17854 }, redraw);
17855 }
17856
17857 });
17858
17859 }(Highcharts));
17860 (function(H) {
17861 /**
17862 * (c) 2010-2016 Torstein Honsi
17863 *
17864 * License: www.highcharts.com/license
17865 */
17866 'use strict';
17867 var color = H.color,
17868 each = H.each,
17869 LegendSymbolMixin = H.LegendSymbolMixin,
17870 map = H.map,
17871 pick = H.pick,
17872 Series = H.Series,
17873 seriesType = H.seriesType;
17874 /**
17875 * Area series type
17876 */
17877 seriesType('area', 'line', {
17878 softThreshold: false,
17879 threshold: 0
17880 // trackByArea: false,
17881 // lineColor: null, // overrides color, but lets fillColor be unaltered
17882 // fillOpacity: 0.75,
17883 // fillColor: null
17884 }, {
17885 singleStacks: false,
17886 /**
17887 * Return an array of stacked points, where null and missing points are replaced by
17888 * dummy points in order for gaps to be drawn correctly in stacks.
17889 */
17890 getStackPoints: function() {
17891 var series = this,
17892 segment = [],
17893 keys = [],
17894 xAxis = this.xAxis,
17895 yAxis = this.yAxis,
17896 stack = yAxis.stacks[this.stackKey],
17897 pointMap = {},
17898 points = this.points,
17899 seriesIndex = series.index,
17900 yAxisSeries = yAxis.series,
17901 seriesLength = yAxisSeries.length,
17902 visibleSeries,
17903 upOrDown = pick(yAxis.options.reversedStacks, true) ? 1 : -1,
17904 i,
17905 x;
17906
17907 if (this.options.stacking) {
17908 // Create a map where we can quickly look up the points by their X value.
17909 for (i = 0; i < points.length; i++) {
17910 pointMap[points[i].x] = points[i];
17911 }
17912
17913 // Sort the keys (#1651)
17914 for (x in stack) {
17915 if (stack[x].total !== null) { // nulled after switching between grouping and not (#1651, #2336)
17916 keys.push(x);
17917 }
17918 }
17919 keys.sort(function(a, b) {
17920 return a - b;
17921 });
17922
17923 visibleSeries = map(yAxisSeries, function() {
17924 return this.visible;
17925 });
17926
17927 each(keys, function(x, idx) {
17928 var y = 0,
17929 stackPoint,
17930 stackedValues;
17931
17932 if (pointMap[x] && !pointMap[x].isNull) {
17933 segment.push(pointMap[x]);
17934
17935 // Find left and right cliff. -1 goes left, 1 goes right.
17936 each([-1, 1], function(direction) {
17937 var nullName = direction === 1 ? 'rightNull' : 'leftNull',
17938 cliffName = direction === 1 ? 'rightCliff' : 'leftCliff',
17939 cliff = 0,
17940 otherStack = stack[keys[idx + direction]];
17941
17942 // If there is a stack next to this one, to the left or to the right...
17943 if (otherStack) {
17944 i = seriesIndex;
17945 while (i >= 0 && i < seriesLength) { // Can go either up or down, depending on reversedStacks
17946 stackPoint = otherStack.points[i];
17947 if (!stackPoint) {
17948 // If the next point in this series is missing, mark the point
17949 // with point.leftNull or point.rightNull = true.
17950 if (i === seriesIndex) {
17951 pointMap[x][nullName] = true;
17952
17953 // If there are missing points in the next stack in any of the
17954 // series below this one, we need to substract the missing values
17955 // and add a hiatus to the left or right.
17956 } else if (visibleSeries[i]) {
17957 stackedValues = stack[x].points[i];
17958 if (stackedValues) {
17959 cliff -= stackedValues[1] - stackedValues[0];
17960 }
17961 }
17962 }
17963 // When reversedStacks is true, loop up, else loop down
17964 i += upOrDown;
17965 }
17966 }
17967 pointMap[x][cliffName] = cliff;
17968 });
17969
17970
17971 // There is no point for this X value in this series, so we
17972 // insert a dummy point in order for the areas to be drawn
17973 // correctly.
17974 } else {
17975
17976 // Loop down the stack to find the series below this one that has
17977 // a value (#1991)
17978 i = seriesIndex;
17979 while (i >= 0 && i < seriesLength) {
17980 stackPoint = stack[x].points[i];
17981 if (stackPoint) {
17982 y = stackPoint[1];
17983 break;
17984 }
17985 // When reversedStacks is true, loop up, else loop down
17986 i += upOrDown;
17987 }
17988
17989 y = yAxis.toPixels(y, true);
17990 segment.push({
17991 isNull: true,
17992 plotX: xAxis.toPixels(x, true),
17993 plotY: y,
17994 yBottom: y
17995 });
17996 }
17997 });
17998
17999 }
18000
18001 return segment;
18002 },
18003
18004 getGraphPath: function(points) {
18005 var getGraphPath = Series.prototype.getGraphPath,
18006 graphPath,
18007 options = this.options,
18008 stacking = options.stacking,
18009 yAxis = this.yAxis,
18010 topPath,
18011 //topPoints = [],
18012 bottomPath,
18013 bottomPoints = [],
18014 graphPoints = [],
18015 seriesIndex = this.index,
18016 i,
18017 areaPath,
18018 plotX,
18019 stacks = yAxis.stacks[this.stackKey],
18020 threshold = options.threshold,
18021 translatedThreshold = yAxis.getThreshold(options.threshold),
18022 isNull,
18023 yBottom,
18024 connectNulls = options.connectNulls || stacking === 'percent',
18025 /**
18026 * To display null points in underlying stacked series, this series graph must be
18027 * broken, and the area also fall down to fill the gap left by the null point. #2069
18028 */
18029 addDummyPoints = function(i, otherI, side) {
18030 var point = points[i],
18031 stackedValues = stacking && stacks[point.x].points[seriesIndex],
18032 nullVal = point[side + 'Null'] || 0,
18033 cliffVal = point[side + 'Cliff'] || 0,
18034 top,
18035 bottom,
18036 isNull = true;
18037
18038 if (cliffVal || nullVal) {
18039
18040 top = (nullVal ? stackedValues[0] : stackedValues[1]) + cliffVal;
18041 bottom = stackedValues[0] + cliffVal;
18042 isNull = !!nullVal;
18043
18044 } else if (!stacking && points[otherI] && points[otherI].isNull) {
18045 top = bottom = threshold;
18046 }
18047
18048 // Add to the top and bottom line of the area
18049 if (top !== undefined) {
18050 graphPoints.push({
18051 plotX: plotX,
18052 plotY: top === null ? translatedThreshold : yAxis.getThreshold(top),
18053 isNull: isNull
18054 });
18055 bottomPoints.push({
18056 plotX: plotX,
18057 plotY: bottom === null ? translatedThreshold : yAxis.getThreshold(bottom),
18058 doCurve: false // #1041, gaps in areaspline areas
18059 });
18060 }
18061 };
18062
18063 // Find what points to use
18064 points = points || this.points;
18065
18066 // Fill in missing points
18067 if (stacking) {
18068 points = this.getStackPoints();
18069 }
18070
18071 for (i = 0; i < points.length; i++) {
18072 isNull = points[i].isNull;
18073 plotX = pick(points[i].rectPlotX, points[i].plotX);
18074 yBottom = pick(points[i].yBottom, translatedThreshold);
18075
18076 if (!isNull || connectNulls) {
18077
18078 if (!connectNulls) {
18079 addDummyPoints(i, i - 1, 'left');
18080 }
18081
18082 if (!(isNull && !stacking && connectNulls)) { // Skip null point when stacking is false and connectNulls true
18083 graphPoints.push(points[i]);
18084 bottomPoints.push({
18085 x: i,
18086 plotX: plotX,
18087 plotY: yBottom
18088 });
18089 }
18090
18091 if (!connectNulls) {
18092 addDummyPoints(i, i + 1, 'right');
18093 }
18094 }
18095 }
18096
18097 topPath = getGraphPath.call(this, graphPoints, true, true);
18098
18099 bottomPoints.reversed = true;
18100 bottomPath = getGraphPath.call(this, bottomPoints, true, true);
18101 if (bottomPath.length) {
18102 bottomPath[0] = 'L';
18103 }
18104
18105 areaPath = topPath.concat(bottomPath);
18106 graphPath = getGraphPath.call(this, graphPoints, false, connectNulls); // TODO: don't set leftCliff and rightCliff when connectNulls?
18107
18108 areaPath.xMap = topPath.xMap;
18109 this.areaPath = areaPath;
18110 return graphPath;
18111 },
18112
18113 /**
18114 * Draw the graph and the underlying area. This method calls the Series base
18115 * function and adds the area. The areaPath is calculated in the getSegmentPath
18116 * method called from Series.prototype.drawGraph.
18117 */
18118 drawGraph: function() {
18119
18120 // Define or reset areaPath
18121 this.areaPath = [];
18122
18123 // Call the base method
18124 Series.prototype.drawGraph.apply(this);
18125
18126 // Define local variables
18127 var series = this,
18128 areaPath = this.areaPath,
18129 options = this.options,
18130 zones = this.zones,
18131 props = [
18132 [
18133 'area',
18134 'highcharts-area',
18135
18136 this.color,
18137 options.fillColor
18138
18139 ]
18140 ]; // area name, main color, fill color
18141
18142 each(zones, function(zone, i) {
18143 props.push([
18144 'zone-area-' + i,
18145 'highcharts-area highcharts-zone-area-' + i + ' ' + zone.className,
18146
18147 zone.color || series.color,
18148 zone.fillColor || options.fillColor
18149
18150 ]);
18151 });
18152
18153 each(props, function(prop) {
18154 var areaKey = prop[0],
18155 area = series[areaKey];
18156
18157 // Create or update the area
18158 if (area) { // update
18159 area.endX = areaPath.xMap;
18160 area.animate({
18161 d: areaPath
18162 });
18163
18164 } else { // create
18165 area = series[areaKey] = series.chart.renderer.path(areaPath)
18166 .addClass(prop[1])
18167 .attr({
18168
18169 fill: pick(
18170 prop[3],
18171 color(prop[2]).setOpacity(pick(options.fillOpacity, 0.75)).get()
18172 ),
18173
18174 zIndex: 0 // #1069
18175 }).add(series.group);
18176 area.isArea = true;
18177 }
18178 area.startX = areaPath.xMap;
18179 area.shiftUnit = options.step ? 2 : 1;
18180 });
18181 },
18182
18183 drawLegendSymbol: LegendSymbolMixin.drawRectangle
18184 });
18185
18186 }(Highcharts));
18187 (function(H) {
18188 /**
18189 * (c) 2010-2016 Torstein Honsi
18190 *
18191 * License: www.highcharts.com/license
18192 */
18193 'use strict';
18194 var defaultPlotOptions = H.defaultPlotOptions,
18195 defaultSeriesOptions = H.defaultPlotOptions.line,
18196 extendClass = H.extendClass,
18197 merge = H.merge,
18198 pick = H.pick,
18199 Series = H.Series,
18200 seriesTypes = H.seriesTypes;
18201
18202 /**
18203 * Set the default options for spline
18204 */
18205 defaultPlotOptions.spline = merge(defaultSeriesOptions);
18206
18207 /**
18208 * SplineSeries object
18209 */
18210 seriesTypes.spline = extendClass(Series, {
18211 type: 'spline',
18212
18213 /**
18214 * Get the spline segment from a given point's previous neighbour to the given point
18215 */
18216 getPointSpline: function(points, point, i) {
18217 var smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc
18218 denom = smoothing + 1,
18219 plotX = point.plotX,
18220 plotY = point.plotY,
18221 lastPoint = points[i - 1],
18222 nextPoint = points[i + 1],
18223 leftContX,
18224 leftContY,
18225 rightContX,
18226 rightContY,
18227 ret;
18228
18229 function doCurve(otherPoint) {
18230 return otherPoint && !otherPoint.isNull && otherPoint.doCurve !== false;
18231 }
18232
18233 // Find control points
18234 if (doCurve(lastPoint) && doCurve(nextPoint)) {
18235 var lastX = lastPoint.plotX,
18236 lastY = lastPoint.plotY,
18237 nextX = nextPoint.plotX,
18238 nextY = nextPoint.plotY,
18239 correction = 0;
18240
18241 leftContX = (smoothing * plotX + lastX) / denom;
18242 leftContY = (smoothing * plotY + lastY) / denom;
18243 rightContX = (smoothing * plotX + nextX) / denom;
18244 rightContY = (smoothing * plotY + nextY) / denom;
18245
18246 // Have the two control points make a straight line through main point
18247 if (rightContX !== leftContX) { // #5016, division by zero
18248 correction = ((rightContY - leftContY) * (rightContX - plotX)) /
18249 (rightContX - leftContX) + plotY - rightContY;
18250 }
18251
18252 leftContY += correction;
18253 rightContY += correction;
18254
18255 // to prevent false extremes, check that control points are between
18256 // neighbouring points' y values
18257 if (leftContY > lastY && leftContY > plotY) {
18258 leftContY = Math.max(lastY, plotY);
18259 rightContY = 2 * plotY - leftContY; // mirror of left control point
18260 } else if (leftContY < lastY && leftContY < plotY) {
18261 leftContY = Math.min(lastY, plotY);
18262 rightContY = 2 * plotY - leftContY;
18263 }
18264 if (rightContY > nextY && rightContY > plotY) {
18265 rightContY = Math.max(nextY, plotY);
18266 leftContY = 2 * plotY - rightContY;
18267 } else if (rightContY < nextY && rightContY < plotY) {
18268 rightContY = Math.min(nextY, plotY);
18269 leftContY = 2 * plotY - rightContY;
18270 }
18271
18272 // record for drawing in next point
18273 point.rightContX = rightContX;
18274 point.rightContY = rightContY;
18275
18276
18277 }
18278
18279 // Visualize control points for debugging
18280 /*
18281 if (leftContX) {
18282 this.chart.renderer.circle(leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop, 2)
18283 .attr({
18284 stroke: 'red',
18285 'stroke-width': 2,
18286 fill: 'none',
18287 zIndex: 9
18288 })
18289 .add();
18290 this.chart.renderer.path(['M', leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop,
18291 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop])
18292 .attr({
18293 stroke: 'red',
18294 'stroke-width': 2,
18295 zIndex: 9
18296 })
18297 .add();
18298 }
18299 if (rightContX) {
18300 this.chart.renderer.circle(rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop, 2)
18301 .attr({
18302 stroke: 'green',
18303 'stroke-width': 2,
18304 fill: 'none',
18305 zIndex: 9
18306 })
18307 .add();
18308 this.chart.renderer.path(['M', rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop,
18309 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop])
18310 .attr({
18311 stroke: 'green',
18312 'stroke-width': 2,
18313 zIndex: 9
18314 })
18315 .add();
18316 }
18317 // */
18318 ret = [
18319 'C',
18320 pick(lastPoint.rightContX, lastPoint.plotX),
18321 pick(lastPoint.rightContY, lastPoint.plotY),
18322 pick(leftContX, plotX),
18323 pick(leftContY, plotY),
18324 plotX,
18325 plotY
18326 ];
18327 lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later
18328 return ret;
18329 }
18330 });
18331
18332 }(Highcharts));
18333 (function(H) {
18334 /**
18335 * (c) 2010-2016 Torstein Honsi
18336 *
18337 * License: www.highcharts.com/license
18338 */
18339 'use strict';
18340 var areaProto = H.seriesTypes.area.prototype,
18341 defaultPlotOptions = H.defaultPlotOptions,
18342 LegendSymbolMixin = H.LegendSymbolMixin,
18343 seriesType = H.seriesType;
18344 /**
18345 * AreaSplineSeries object
18346 */
18347 seriesType('areaspline', 'spline', defaultPlotOptions.area, {
18348 getStackPoints: areaProto.getStackPoints,
18349 getGraphPath: areaProto.getGraphPath,
18350 setStackCliffs: areaProto.setStackCliffs,
18351 drawGraph: areaProto.drawGraph,
18352 drawLegendSymbol: LegendSymbolMixin.drawRectangle
18353 });
18354
18355 }(Highcharts));
18356 (function(H) {
18357 /**
18358 * (c) 2010-2016 Torstein Honsi
18359 *
18360 * License: www.highcharts.com/license
18361 */
18362 'use strict';
18363 var animObject = H.animObject,
18364 color = H.color,
18365 each = H.each,
18366 extend = H.extend,
18367 isNumber = H.isNumber,
18368 LegendSymbolMixin = H.LegendSymbolMixin,
18369 merge = H.merge,
18370 noop = H.noop,
18371 pick = H.pick,
18372 Series = H.Series,
18373 seriesType = H.seriesType,
18374 stop = H.stop,
18375 svg = H.svg;
18376 /**
18377 * The column series type
18378 */
18379 seriesType('column', 'line', {
18380 borderRadius: 0,
18381 //colorByPoint: undefined,
18382 groupPadding: 0.2,
18383 //grouping: true,
18384 marker: null, // point options are specified in the base options
18385 pointPadding: 0.1,
18386 //pointWidth: null,
18387 minPointLength: 0,
18388 cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes
18389 pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories
18390 states: {
18391 hover: {
18392 halo: false,
18393
18394 brightness: 0.1,
18395 shadow: false
18396
18397 },
18398
18399 select: {
18400 color: '#cccccc',
18401 borderColor: '#000000',
18402 shadow: false
18403 }
18404
18405 },
18406 dataLabels: {
18407 align: null, // auto
18408 verticalAlign: null, // auto
18409 y: null
18410 },
18411 softThreshold: false,
18412 startFromThreshold: true, // false doesn't work well: http://jsfiddle.net/highcharts/hz8fopan/14/
18413 stickyTracking: false,
18414 tooltip: {
18415 distance: 6
18416 },
18417 threshold: 0,
18418
18419 borderColor: '#ffffff'
18420 // borderWidth: 1
18421
18422
18423 // Prototype members
18424 }, {
18425 cropShoulder: 0,
18426 directTouch: true, // When tooltip is not shared, this series (and derivatives) requires direct touch/hover. KD-tree does not apply.
18427 trackerGroups: ['group', 'dataLabelsGroup'],
18428 negStacks: true, // use separate negative stacks, unlike area stacks where a negative
18429 // point is substracted from previous (#1910)
18430
18431 /**
18432 * Initialize the series
18433 */
18434 init: function() {
18435 Series.prototype.init.apply(this, arguments);
18436
18437 var series = this,
18438 chart = series.chart;
18439
18440 // if the series is added dynamically, force redraw of other
18441 // series affected by a new column
18442 if (chart.hasRendered) {
18443 each(chart.series, function(otherSeries) {
18444 if (otherSeries.type === series.type) {
18445 otherSeries.isDirty = true;
18446 }
18447 });
18448 }
18449 },
18450
18451 /**
18452 * Return the width and x offset of the columns adjusted for grouping, groupPadding, pointPadding,
18453 * pointWidth etc.
18454 */
18455 getColumnMetrics: function() {
18456
18457 var series = this,
18458 options = series.options,
18459 xAxis = series.xAxis,
18460 yAxis = series.yAxis,
18461 reversedXAxis = xAxis.reversed,
18462 stackKey,
18463 stackGroups = {},
18464 columnCount = 0;
18465
18466 // Get the total number of column type series.
18467 // This is called on every series. Consider moving this logic to a
18468 // chart.orderStacks() function and call it on init, addSeries and removeSeries
18469 if (options.grouping === false) {
18470 columnCount = 1;
18471 } else {
18472 each(series.chart.series, function(otherSeries) {
18473 var otherOptions = otherSeries.options,
18474 otherYAxis = otherSeries.yAxis,
18475 columnIndex;
18476 if (otherSeries.type === series.type && otherSeries.visible &&
18477 yAxis.len === otherYAxis.len && yAxis.pos === otherYAxis.pos) { // #642, #2086
18478 if (otherOptions.stacking) {
18479 stackKey = otherSeries.stackKey;
18480 if (stackGroups[stackKey] === undefined) {
18481 stackGroups[stackKey] = columnCount++;
18482 }
18483 columnIndex = stackGroups[stackKey];
18484 } else if (otherOptions.grouping !== false) { // #1162
18485 columnIndex = columnCount++;
18486 }
18487 otherSeries.columnIndex = columnIndex;
18488 }
18489 });
18490 }
18491
18492 var categoryWidth = Math.min(
18493 Math.abs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || xAxis.tickInterval || 1), // #2610
18494 xAxis.len // #1535
18495 ),
18496 groupPadding = categoryWidth * options.groupPadding,
18497 groupWidth = categoryWidth - 2 * groupPadding,
18498 pointOffsetWidth = groupWidth / columnCount,
18499 pointWidth = Math.min(
18500 options.maxPointWidth || xAxis.len,
18501 pick(options.pointWidth, pointOffsetWidth * (1 - 2 * options.pointPadding))
18502 ),
18503 pointPadding = (pointOffsetWidth - pointWidth) / 2,
18504 colIndex = (series.columnIndex || 0) + (reversedXAxis ? 1 : 0), // #1251, #3737
18505 pointXOffset = pointPadding + (groupPadding + colIndex *
18506 pointOffsetWidth - (categoryWidth / 2)) *
18507 (reversedXAxis ? -1 : 1);
18508
18509 // Save it for reading in linked series (Error bars particularly)
18510 series.columnMetrics = {
18511 width: pointWidth,
18512 offset: pointXOffset
18513 };
18514 return series.columnMetrics;
18515
18516 },
18517
18518 /**
18519 * Make the columns crisp. The edges are rounded to the nearest full pixel.
18520 */
18521 crispCol: function(x, y, w, h) {
18522 var chart = this.chart,
18523 borderWidth = this.borderWidth,
18524 xCrisp = -(borderWidth % 2 ? 0.5 : 0),
18525 yCrisp = borderWidth % 2 ? 0.5 : 1,
18526 right,
18527 bottom,
18528 fromTop;
18529
18530 if (chart.inverted && chart.renderer.isVML) {
18531 yCrisp += 1;
18532 }
18533
18534 // Horizontal. We need to first compute the exact right edge, then round it
18535 // and compute the width from there.
18536 right = Math.round(x + w) + xCrisp;
18537 x = Math.round(x) + xCrisp;
18538 w = right - x;
18539
18540 // Vertical
18541 bottom = Math.round(y + h) + yCrisp;
18542 fromTop = Math.abs(y) <= 0.5 && bottom > 0.5; // #4504, #4656
18543 y = Math.round(y) + yCrisp;
18544 h = bottom - y;
18545
18546 // Top edges are exceptions
18547 if (fromTop && h) { // #5146
18548 y -= 1;
18549 h += 1;
18550 }
18551
18552 return {
18553 x: x,
18554 y: y,
18555 width: w,
18556 height: h
18557 };
18558 },
18559
18560 /**
18561 * Translate each point to the plot area coordinate system and find shape positions
18562 */
18563 translate: function() {
18564 var series = this,
18565 chart = series.chart,
18566 options = series.options,
18567 dense = series.dense = series.closestPointRange * series.xAxis.transA < 2,
18568 borderWidth = series.borderWidth = pick(
18569 options.borderWidth,
18570 dense ? 0 : 1 // #3635
18571 ),
18572 yAxis = series.yAxis,
18573 threshold = options.threshold,
18574 translatedThreshold = series.translatedThreshold = yAxis.getThreshold(threshold),
18575 minPointLength = pick(options.minPointLength, 5),
18576 metrics = series.getColumnMetrics(),
18577 pointWidth = metrics.width,
18578 seriesBarW = series.barW = Math.max(pointWidth, 1 + 2 * borderWidth), // postprocessed for border width
18579 pointXOffset = series.pointXOffset = metrics.offset;
18580
18581 if (chart.inverted) {
18582 translatedThreshold -= 0.5; // #3355
18583 }
18584
18585 // When the pointPadding is 0, we want the columns to be packed tightly, so we allow individual
18586 // columns to have individual sizes. When pointPadding is greater, we strive for equal-width
18587 // columns (#2694).
18588 if (options.pointPadding) {
18589 seriesBarW = Math.ceil(seriesBarW);
18590 }
18591
18592 Series.prototype.translate.apply(series);
18593
18594 // Record the new values
18595 each(series.points, function(point) {
18596 var yBottom = pick(point.yBottom, translatedThreshold),
18597 safeDistance = 999 + Math.abs(yBottom),
18598 plotY = Math.min(Math.max(-safeDistance, point.plotY), yAxis.len + safeDistance), // Don't draw too far outside plot area (#1303, #2241, #4264)
18599 barX = point.plotX + pointXOffset,
18600 barW = seriesBarW,
18601 barY = Math.min(plotY, yBottom),
18602 up,
18603 barH = Math.max(plotY, yBottom) - barY;
18604
18605 // Handle options.minPointLength
18606 if (Math.abs(barH) < minPointLength) {
18607 if (minPointLength) {
18608 barH = minPointLength;
18609 up = (!yAxis.reversed && !point.negative) || (yAxis.reversed && point.negative);
18610 barY = Math.abs(barY - translatedThreshold) > minPointLength ? // stacked
18611 yBottom - minPointLength : // keep position
18612 translatedThreshold - (up ? minPointLength : 0); // #1485, #4051
18613 }
18614 }
18615
18616 // Cache for access in polar
18617 point.barX = barX;
18618 point.pointWidth = pointWidth;
18619
18620 // Fix the tooltip on center of grouped columns (#1216, #424, #3648)
18621 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];
18622
18623 // Register shape type and arguments to be used in drawPoints
18624 point.shapeType = 'rect';
18625 point.shapeArgs = series.crispCol.apply(
18626 series,
18627 point.isNull ? [point.plotX, yAxis.len / 2, 0, 0] : // #3169, drilldown from null must have a position to work from
18628 [barX, barY, barW, barH]
18629 );
18630 });
18631
18632 },
18633
18634 getSymbol: noop,
18635
18636 /**
18637 * Use a solid rectangle like the area series types
18638 */
18639 drawLegendSymbol: LegendSymbolMixin.drawRectangle,
18640
18641
18642 /**
18643 * Columns have no graph
18644 */
18645 drawGraph: function() {
18646 this.group[this.dense ? 'addClass' : 'removeClass']('highcharts-dense-data');
18647 },
18648
18649
18650 /**
18651 * Get presentational attributes
18652 */
18653 pointAttribs: function(point, state) {
18654 var options = this.options,
18655 stateOptions,
18656 ret,
18657 p2o = this.pointAttrToOptions || {},
18658 strokeOption = p2o.stroke || 'borderColor',
18659 strokeWidthOption = p2o['stroke-width'] || 'borderWidth',
18660 fill = (point && point.color) || this.color,
18661 stroke = options[strokeOption] || this.color || fill, // set to fill when borderColor = null on pies
18662 dashstyle = options.dashStyle,
18663 zone,
18664 brightness;
18665
18666 // Handle zone colors
18667 if (point && this.zones.length) {
18668 zone = point.getZone();
18669 fill = (zone && zone.color) || point.options.color || this.color; // When zones are present, don't use point.color (#4267)
18670 }
18671
18672 // Select or hover states
18673 if (state) {
18674 stateOptions = options.states[state];
18675 brightness = stateOptions.brightness;
18676 fill = stateOptions.color ||
18677 (brightness !== undefined && color(fill).brighten(stateOptions.brightness).get()) ||
18678 fill;
18679 stroke = stateOptions[strokeOption] || stroke;
18680 dashstyle = stateOptions.dashStyle || dashstyle;
18681 }
18682
18683 ret = {
18684 'fill': fill,
18685 'stroke': stroke,
18686 'stroke-width': point[strokeWidthOption] || options[strokeWidthOption] || this[strokeWidthOption] || 0
18687 };
18688 if (options.borderRadius) {
18689 ret.r = options.borderRadius;
18690 }
18691
18692 if (dashstyle) {
18693 ret.dashstyle = dashstyle;
18694 }
18695
18696 return ret;
18697 },
18698
18699
18700 /**
18701 * Draw the columns. For bars, the series.group is rotated, so the same coordinates
18702 * apply for columns and bars. This method is inherited by scatter series.
18703 *
18704 */
18705 drawPoints: function() {
18706 var series = this,
18707 chart = this.chart,
18708 options = series.options,
18709 renderer = chart.renderer,
18710 animationLimit = options.animationLimit || 250,
18711 shapeArgs;
18712
18713 // draw the columns
18714 each(series.points, function(point) {
18715 var plotY = point.plotY,
18716 graphic = point.graphic;
18717
18718 if (isNumber(plotY) && point.y !== null) {
18719 shapeArgs = point.shapeArgs;
18720
18721 if (graphic) { // update
18722 stop(graphic);
18723 graphic[chart.pointCount < animationLimit ? 'animate' : 'attr'](
18724 merge(shapeArgs)
18725 );
18726
18727 } else {
18728 point.graphic = graphic = renderer[point.shapeType](shapeArgs)
18729 .attr({
18730 'class': point.getClassName()
18731 })
18732 .add(point.group || series.group);
18733 }
18734
18735
18736 // Presentational
18737 graphic
18738 .attr(series.pointAttribs(point, point.selected && 'select'))
18739 .shadow(options.shadow, null, options.stacking && !options.borderRadius);
18740
18741
18742 } else if (graphic) {
18743 point.graphic = graphic.destroy(); // #1269
18744 }
18745 });
18746 },
18747
18748 /**
18749 * Animate the column heights one by one from zero
18750 * @param {Boolean} init Whether to initialize the animation or run it
18751 */
18752 animate: function(init) {
18753 var series = this,
18754 yAxis = this.yAxis,
18755 options = series.options,
18756 inverted = this.chart.inverted,
18757 attr = {},
18758 translatedThreshold;
18759
18760 if (svg) { // VML is too slow anyway
18761 if (init) {
18762 attr.scaleY = 0.001;
18763 translatedThreshold = Math.min(yAxis.pos + yAxis.len, Math.max(yAxis.pos, yAxis.toPixels(options.threshold)));
18764 if (inverted) {
18765 attr.translateX = translatedThreshold - yAxis.len;
18766 } else {
18767 attr.translateY = translatedThreshold;
18768 }
18769 series.group.attr(attr);
18770
18771 } else { // run the animation
18772
18773 attr[inverted ? 'translateX' : 'translateY'] = yAxis.pos;
18774 series.group.animate(attr, extend(animObject(series.options.animation), {
18775 // Do the scale synchronously to ensure smooth updating (#5030)
18776 step: function(val, fx) {
18777 series.group.attr({
18778 scaleY: Math.max(0.001, fx.pos) // #5250
18779 });
18780 }
18781 }));
18782
18783 // delete this function to allow it only once
18784 series.animate = null;
18785 }
18786 }
18787 },
18788
18789 /**
18790 * Remove this series from the chart
18791 */
18792 remove: function() {
18793 var series = this,
18794 chart = series.chart;
18795
18796 // column and bar series affects other series of the same type
18797 // as they are either stacked or grouped
18798 if (chart.hasRendered) {
18799 each(chart.series, function(otherSeries) {
18800 if (otherSeries.type === series.type) {
18801 otherSeries.isDirty = true;
18802 }
18803 });
18804 }
18805
18806 Series.prototype.remove.apply(series, arguments);
18807 }
18808 });
18809
18810 }(Highcharts));
18811 (function(H) {
18812 /**
18813 * (c) 2010-2016 Torstein Honsi
18814 *
18815 * License: www.highcharts.com/license
18816 */
18817 'use strict';
18818 var seriesType = H.seriesType;
18819 /**
18820 * The Bar series class
18821 */
18822 seriesType('bar', 'column', null, {
18823 inverted: true
18824 });
18825
18826 }(Highcharts));
18827 (function(H) {
18828 /**
18829 * (c) 2010-2016 Torstein Honsi
18830 *
18831 * License: www.highcharts.com/license
18832 */
18833 'use strict';
18834 var Series = H.Series,
18835 seriesType = H.seriesType;
18836 /**
18837 * The scatter series type
18838 */
18839 seriesType('scatter', 'line', {
18840 lineWidth: 0,
18841 marker: {
18842 enabled: true // Overrides auto-enabling in line series (#3647)
18843 },
18844 tooltip: {
18845 headerFormat: '<span style="color:{point.color}">\u25CF</span> <span style="font-size: 0.85em"> {series.name}</span><br/>',
18846 pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>'
18847 }
18848
18849 // Prototype members
18850 }, {
18851 sorted: false,
18852 requireSorting: false,
18853 noSharedTooltip: true,
18854 trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
18855 takeOrdinalPosition: false, // #2342
18856 kdDimensions: 2,
18857 drawGraph: function() {
18858 if (this.options.lineWidth) {
18859 Series.prototype.drawGraph.call(this);
18860 }
18861 }
18862 });
18863
18864 }(Highcharts));
18865 (function(H) {
18866 /**
18867 * (c) 2010-2016 Torstein Honsi
18868 *
18869 * License: www.highcharts.com/license
18870 */
18871 'use strict';
18872 var pick = H.pick,
18873 relativeLength = H.relativeLength;
18874
18875 H.CenteredSeriesMixin = {
18876 /**
18877 * Get the center of the pie based on the size and center options relative to the
18878 * plot area. Borrowed by the polar and gauge series types.
18879 */
18880 getCenter: function() {
18881
18882 var options = this.options,
18883 chart = this.chart,
18884 slicingRoom = 2 * (options.slicedOffset || 0),
18885 handleSlicingRoom,
18886 plotWidth = chart.plotWidth - 2 * slicingRoom,
18887 plotHeight = chart.plotHeight - 2 * slicingRoom,
18888 centerOption = options.center,
18889 positions = [pick(centerOption[0], '50%'), pick(centerOption[1], '50%'), options.size || '100%', options.innerSize || 0],
18890 smallestSize = Math.min(plotWidth, plotHeight),
18891 i,
18892 value;
18893
18894 for (i = 0; i < 4; ++i) {
18895 value = positions[i];
18896 handleSlicingRoom = i < 2 || (i === 2 && /%$/.test(value));
18897
18898 // i == 0: centerX, relative to width
18899 // i == 1: centerY, relative to height
18900 // i == 2: size, relative to smallestSize
18901 // i == 3: innerSize, relative to size
18902 positions[i] = relativeLength(value, [plotWidth, plotHeight, smallestSize, positions[2]][i]) +
18903 (handleSlicingRoom ? slicingRoom : 0);
18904
18905 }
18906 // innerSize cannot be larger than size (#3632)
18907 if (positions[3] > positions[2]) {
18908 positions[3] = positions[2];
18909 }
18910 return positions;
18911 }
18912 };
18913
18914 }(Highcharts));
18915 (function(H) {
18916 /**
18917 * (c) 2010-2016 Torstein Honsi
18918 *
18919 * License: www.highcharts.com/license
18920 */
18921 'use strict';
18922 var addEvent = H.addEvent,
18923 CenteredSeriesMixin = H.CenteredSeriesMixin,
18924 defined = H.defined,
18925 each = H.each,
18926 extend = H.extend,
18927 inArray = H.inArray,
18928 LegendSymbolMixin = H.LegendSymbolMixin,
18929 noop = H.noop,
18930 pick = H.pick,
18931 Point = H.Point,
18932 Series = H.Series,
18933 seriesType = H.seriesType,
18934 seriesTypes = H.seriesTypes,
18935 setAnimation = H.setAnimation;
18936 /**
18937 * Pie series type
18938 */
18939 seriesType('pie', 'line', {
18940 center: [null, null],
18941 clip: false,
18942 colorByPoint: true, // always true for pies
18943 dataLabels: {
18944 // align: null,
18945 // connectorWidth: 1,
18946 // connectorColor: point.color,
18947 // connectorPadding: 5,
18948 distance: 30,
18949 enabled: true,
18950 formatter: function() { // #2945
18951 return this.y === null ? undefined : this.point.name;
18952 },
18953 // softConnector: true,
18954 x: 0
18955 // y: 0
18956 },
18957 ignoreHiddenPoint: true,
18958 //innerSize: 0,
18959 legendType: 'point',
18960 marker: null, // point options are specified in the base options
18961 size: null,
18962 showInLegend: false,
18963 slicedOffset: 10,
18964 stickyTracking: false,
18965 tooltip: {
18966 followPointer: true
18967 },
18968
18969 borderColor: '#ffffff',
18970 borderWidth: 1,
18971 states: {
18972 hover: {
18973 brightness: 0.1,
18974 shadow: false
18975 }
18976 }
18977
18978
18979 // Prototype members
18980 }, {
18981 isCartesian: false,
18982 requireSorting: false,
18983 directTouch: true,
18984 noSharedTooltip: true,
18985 trackerGroups: ['group', 'dataLabelsGroup'],
18986 axisTypes: [],
18987 pointAttribs: seriesTypes.column.prototype.pointAttribs,
18988 /**
18989 * Animate the pies in
18990 */
18991 animate: function(init) {
18992 var series = this,
18993 points = series.points,
18994 startAngleRad = series.startAngleRad;
18995
18996 if (!init) {
18997 each(points, function(point) {
18998 var graphic = point.graphic,
18999 args = point.shapeArgs;
19000
19001 if (graphic) {
19002 // start values
19003 graphic.attr({
19004 r: point.startR || (series.center[3] / 2), // animate from inner radius (#779)
19005 start: startAngleRad,
19006 end: startAngleRad
19007 });
19008
19009 // animate
19010 graphic.animate({
19011 r: args.r,
19012 start: args.start,
19013 end: args.end
19014 }, series.options.animation);
19015 }
19016 });
19017
19018 // delete this function to allow it only once
19019 series.animate = null;
19020 }
19021 },
19022
19023 /**
19024 * Recompute total chart sum and update percentages of points.
19025 */
19026 updateTotals: function() {
19027 var i,
19028 total = 0,
19029 points = this.points,
19030 len = points.length,
19031 point,
19032 ignoreHiddenPoint = this.options.ignoreHiddenPoint;
19033
19034 // Get the total sum
19035 for (i = 0; i < len; i++) {
19036 point = points[i];
19037 // Disallow negative values (#1530, #3623, #5322)
19038 if (point.y < 0) {
19039 point.y = null;
19040 }
19041 total += (ignoreHiddenPoint && !point.visible) ? 0 : point.y;
19042 }
19043 this.total = total;
19044
19045 // Set each point's properties
19046 for (i = 0; i < len; i++) {
19047 point = points[i];
19048 point.percentage = (total > 0 && (point.visible || !ignoreHiddenPoint)) ? point.y / total * 100 : 0;
19049 point.total = total;
19050 }
19051 },
19052
19053 /**
19054 * Extend the generatePoints method by adding total and percentage properties to each point
19055 */
19056 generatePoints: function() {
19057 Series.prototype.generatePoints.call(this);
19058 this.updateTotals();
19059 },
19060
19061 /**
19062 * Do translation for pie slices
19063 */
19064 translate: function(positions) {
19065 this.generatePoints();
19066
19067 var series = this,
19068 cumulative = 0,
19069 precision = 1000, // issue #172
19070 options = series.options,
19071 slicedOffset = options.slicedOffset,
19072 connectorOffset = slicedOffset + (options.borderWidth || 0),
19073 start,
19074 end,
19075 angle,
19076 startAngle = options.startAngle || 0,
19077 startAngleRad = series.startAngleRad = Math.PI / 180 * (startAngle - 90),
19078 endAngleRad = series.endAngleRad = Math.PI / 180 * ((pick(options.endAngle, startAngle + 360)) - 90),
19079 circ = endAngleRad - startAngleRad, //2 * Math.PI,
19080 points = series.points,
19081 radiusX, // the x component of the radius vector for a given point
19082 radiusY,
19083 labelDistance = options.dataLabels.distance,
19084 ignoreHiddenPoint = options.ignoreHiddenPoint,
19085 i,
19086 len = points.length,
19087 point;
19088
19089 // Get positions - either an integer or a percentage string must be given.
19090 // If positions are passed as a parameter, we're in a recursive loop for adjusting
19091 // space for data labels.
19092 if (!positions) {
19093 series.center = positions = series.getCenter();
19094 }
19095
19096 // utility for getting the x value from a given y, used for anticollision logic in data labels
19097 series.getX = function(y, left) {
19098
19099 angle = Math.asin(Math.min((y - positions[1]) / (positions[2] / 2 + labelDistance), 1));
19100
19101 return positions[0] +
19102 (left ? -1 : 1) *
19103 (Math.cos(angle) * (positions[2] / 2 + labelDistance));
19104 };
19105
19106 // Calculate the geometry for each point
19107 for (i = 0; i < len; i++) {
19108
19109 point = points[i];
19110
19111 // set start and end angle
19112 start = startAngleRad + (cumulative * circ);
19113 if (!ignoreHiddenPoint || point.visible) {
19114 cumulative += point.percentage / 100;
19115 }
19116 end = startAngleRad + (cumulative * circ);
19117
19118 // set the shape
19119 point.shapeType = 'arc';
19120 point.shapeArgs = {
19121 x: positions[0],
19122 y: positions[1],
19123 r: positions[2] / 2,
19124 innerR: positions[3] / 2,
19125 start: Math.round(start * precision) / precision,
19126 end: Math.round(end * precision) / precision
19127 };
19128
19129 // The angle must stay within -90 and 270 (#2645)
19130 angle = (end + start) / 2;
19131 if (angle > 1.5 * Math.PI) {
19132 angle -= 2 * Math.PI;
19133 } else if (angle < -Math.PI / 2) {
19134 angle += 2 * Math.PI;
19135 }
19136
19137 // Center for the sliced out slice
19138 point.slicedTranslation = {
19139 translateX: Math.round(Math.cos(angle) * slicedOffset),
19140 translateY: Math.round(Math.sin(angle) * slicedOffset)
19141 };
19142
19143 // set the anchor point for tooltips
19144 radiusX = Math.cos(angle) * positions[2] / 2;
19145 radiusY = Math.sin(angle) * positions[2] / 2;
19146 point.tooltipPos = [
19147 positions[0] + radiusX * 0.7,
19148 positions[1] + radiusY * 0.7
19149 ];
19150
19151 point.half = angle < -Math.PI / 2 || angle > Math.PI / 2 ? 1 : 0;
19152 point.angle = angle;
19153
19154 // set the anchor point for data labels
19155 connectorOffset = Math.min(connectorOffset, labelDistance / 5); // #1678
19156 point.labelPos = [
19157 positions[0] + radiusX + Math.cos(angle) * labelDistance, // first break of connector
19158 positions[1] + radiusY + Math.sin(angle) * labelDistance, // a/a
19159 positions[0] + radiusX + Math.cos(angle) * connectorOffset, // second break, right outside pie
19160 positions[1] + radiusY + Math.sin(angle) * connectorOffset, // a/a
19161 positions[0] + radiusX, // landing point for connector
19162 positions[1] + radiusY, // a/a
19163 labelDistance < 0 ? // alignment
19164 'center' :
19165 point.half ? 'right' : 'left', // alignment
19166 angle // center angle
19167 ];
19168
19169 }
19170 },
19171
19172 drawGraph: null,
19173
19174 /**
19175 * Draw the data points
19176 */
19177 drawPoints: function() {
19178 var series = this,
19179 chart = series.chart,
19180 renderer = chart.renderer,
19181 groupTranslation,
19182 //center,
19183 graphic,
19184 //group,
19185 pointAttr,
19186 shapeArgs;
19187
19188
19189 var shadow = series.options.shadow;
19190 if (shadow && !series.shadowGroup) {
19191 series.shadowGroup = renderer.g('shadow')
19192 .add(series.group);
19193 }
19194
19195
19196 // draw the slices
19197 each(series.points, function(point) {
19198 if (point.y !== null) {
19199 graphic = point.graphic;
19200 shapeArgs = point.shapeArgs;
19201
19202
19203 // if the point is sliced, use special translation, else use plot area traslation
19204 groupTranslation = point.sliced ? point.slicedTranslation : {};
19205
19206
19207 // Put the shadow behind all points
19208 var shadowGroup = point.shadowGroup;
19209 if (shadow && !shadowGroup) {
19210 shadowGroup = point.shadowGroup = renderer.g('shadow')
19211 .add(series.shadowGroup);
19212 }
19213
19214 if (shadowGroup) {
19215 shadowGroup.attr(groupTranslation);
19216 }
19217 pointAttr = series.pointAttribs(point, point.selected && 'select');
19218
19219
19220 // Draw the slice
19221 if (graphic) {
19222 graphic
19223 .setRadialReference(series.center)
19224
19225 .attr(pointAttr)
19226
19227 .animate(extend(shapeArgs, groupTranslation));
19228 } else {
19229
19230 point.graphic = graphic = renderer[point.shapeType](shapeArgs)
19231 .addClass(point.getClassName())
19232 .setRadialReference(series.center)
19233 .attr(groupTranslation)
19234 .add(series.group);
19235
19236 if (!point.visible) {
19237 graphic.attr({
19238 visibility: 'hidden'
19239 });
19240 }
19241
19242
19243 graphic
19244 .attr(pointAttr)
19245 .attr({
19246 'stroke-linejoin': 'round'
19247 })
19248 .shadow(shadow, shadowGroup);
19249
19250 }
19251 }
19252 });
19253
19254 },
19255
19256
19257 searchPoint: noop,
19258
19259 /**
19260 * Utility for sorting data labels
19261 */
19262 sortByAngle: function(points, sign) {
19263 points.sort(function(a, b) {
19264 return a.angle !== undefined && (b.angle - a.angle) * sign;
19265 });
19266 },
19267
19268 /**
19269 * Use a simple symbol from LegendSymbolMixin
19270 */
19271 drawLegendSymbol: LegendSymbolMixin.drawRectangle,
19272
19273 /**
19274 * Use the getCenter method from drawLegendSymbol
19275 */
19276 getCenter: CenteredSeriesMixin.getCenter,
19277
19278 /**
19279 * Pies don't have point marker symbols
19280 */
19281 getSymbol: noop
19282
19283 // Point class overrides
19284 }, {
19285 /**
19286 * Initiate the pie slice
19287 */
19288 init: function() {
19289
19290 Point.prototype.init.apply(this, arguments);
19291
19292 var point = this,
19293 toggleSlice;
19294
19295 point.name = pick(point.name, 'Slice');
19296
19297 // add event listener for select
19298 toggleSlice = function(e) {
19299 point.slice(e.type === 'select');
19300 };
19301 addEvent(point, 'select', toggleSlice);
19302 addEvent(point, 'unselect', toggleSlice);
19303
19304 return point;
19305 },
19306
19307 /**
19308 * Toggle the visibility of the pie slice
19309 * @param {Boolean} vis Whether to show the slice or not. If undefined, the
19310 * visibility is toggled
19311 */
19312 setVisible: function(vis, redraw) {
19313 var point = this,
19314 series = point.series,
19315 chart = series.chart,
19316 ignoreHiddenPoint = series.options.ignoreHiddenPoint;
19317
19318 redraw = pick(redraw, ignoreHiddenPoint);
19319
19320 if (vis !== point.visible) {
19321
19322 // If called without an argument, toggle visibility
19323 point.visible = point.options.visible = vis = vis === undefined ? !point.visible : vis;
19324 series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data
19325
19326 // Show and hide associated elements. This is performed regardless of redraw or not,
19327 // because chart.redraw only handles full series.
19328 each(['graphic', 'dataLabel', 'connector', 'shadowGroup'], function(key) {
19329 if (point[key]) {
19330 point[key][vis ? 'show' : 'hide'](true);
19331 }
19332 });
19333
19334 if (point.legendItem) {
19335 chart.legend.colorizeItem(point, vis);
19336 }
19337
19338 // #4170, hide halo after hiding point
19339 if (!vis && point.state === 'hover') {
19340 point.setState('');
19341 }
19342
19343 // Handle ignore hidden slices
19344 if (ignoreHiddenPoint) {
19345 series.isDirty = true;
19346 }
19347
19348 if (redraw) {
19349 chart.redraw();
19350 }
19351 }
19352 },
19353
19354 /**
19355 * Set or toggle whether the slice is cut out from the pie
19356 * @param {Boolean} sliced When undefined, the slice state is toggled
19357 * @param {Boolean} redraw Whether to redraw the chart. True by default.
19358 */
19359 slice: function(sliced, redraw, animation) {
19360 var point = this,
19361 series = point.series,
19362 chart = series.chart,
19363 translation;
19364
19365 setAnimation(animation, chart);
19366
19367 // redraw is true by default
19368 redraw = pick(redraw, true);
19369
19370 // if called without an argument, toggle
19371 point.sliced = point.options.sliced = sliced = defined(sliced) ? sliced : !point.sliced;
19372 series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data
19373
19374 translation = sliced ? point.slicedTranslation : {
19375 translateX: 0,
19376 translateY: 0
19377 };
19378
19379 point.graphic.animate(translation);
19380
19381
19382 if (point.shadowGroup) {
19383 point.shadowGroup.animate(translation);
19384 }
19385
19386 },
19387
19388 haloPath: function(size) {
19389 var shapeArgs = this.shapeArgs,
19390 chart = this.series.chart;
19391
19392 return this.sliced || !this.visible ? [] : this.series.chart.renderer.symbols.arc(chart.plotLeft + shapeArgs.x, chart.plotTop + shapeArgs.y, shapeArgs.r + size, shapeArgs.r + size, {
19393 innerR: this.shapeArgs.r,
19394 start: shapeArgs.start,
19395 end: shapeArgs.end
19396 });
19397 }
19398 });
19399
19400 }(Highcharts));
19401 (function(H) {
19402 /**
19403 * (c) 2010-2016 Torstein Honsi
19404 *
19405 * License: www.highcharts.com/license
19406 */
19407 'use strict';
19408 var addEvent = H.addEvent,
19409 arrayMax = H.arrayMax,
19410 defined = H.defined,
19411 each = H.each,
19412 extend = H.extend,
19413 format = H.format,
19414 map = H.map,
19415 merge = H.merge,
19416 noop = H.noop,
19417 pick = H.pick,
19418 relativeLength = H.relativeLength,
19419 Series = H.Series,
19420 seriesTypes = H.seriesTypes,
19421 stableSort = H.stableSort,
19422 stop = H.stop;
19423
19424
19425 /**
19426 * Generatl distribution algorithm for distributing labels of differing size along a
19427 * confined length in two dimensions. The algorithm takes an array of objects containing
19428 * a size, a target and a rank. It will place the labels as close as possible to their
19429 * targets, skipping the lowest ranked labels if necessary.
19430 */
19431 H.distribute = function(boxes, len) {
19432
19433 var i,
19434 overlapping = true,
19435 origBoxes = boxes, // Original array will be altered with added .pos
19436 restBoxes = [], // The outranked overshoot
19437 box,
19438 target,
19439 total = 0;
19440
19441 function sortByTarget(a, b) {
19442 return a.target - b.target;
19443 }
19444
19445 // If the total size exceeds the len, remove those boxes with the lowest rank
19446 i = boxes.length;
19447 while (i--) {
19448 total += boxes[i].size;
19449 }
19450
19451 // Sort by rank, then slice away overshoot
19452 if (total > len) {
19453 stableSort(boxes, function(a, b) {
19454 return (b.rank || 0) - (a.rank || 0);
19455 });
19456 i = 0;
19457 total = 0;
19458 while (total <= len) {
19459 total += boxes[i].size;
19460 i++;
19461 }
19462 restBoxes = boxes.splice(i - 1, boxes.length);
19463 }
19464
19465 // Order by target
19466 stableSort(boxes, sortByTarget);
19467
19468
19469 // So far we have been mutating the original array. Now
19470 // create a copy with target arrays
19471 boxes = map(boxes, function(box) {
19472 return {
19473 size: box.size,
19474 targets: [box.target]
19475 };
19476 });
19477
19478 while (overlapping) {
19479 // Initial positions: target centered in box
19480 i = boxes.length;
19481 while (i--) {
19482 box = boxes[i];
19483 // Composite box, average of targets
19484 target = (Math.min.apply(0, box.targets) + Math.max.apply(0, box.targets)) / 2;
19485 box.pos = Math.min(Math.max(0, target - box.size / 2), len - box.size);
19486 }
19487
19488 // Detect overlap and join boxes
19489 i = boxes.length;
19490 overlapping = false;
19491 while (i--) {
19492 if (i > 0 && boxes[i - 1].pos + boxes[i - 1].size > boxes[i].pos) { // Overlap
19493 boxes[i - 1].size += boxes[i].size; // Add this size to the previous box
19494 boxes[i - 1].targets = boxes[i - 1].targets.concat(boxes[i].targets);
19495
19496 // Overlapping right, push left
19497 if (boxes[i - 1].pos + boxes[i - 1].size > len) {
19498 boxes[i - 1].pos = len - boxes[i - 1].size;
19499 }
19500 boxes.splice(i, 1); // Remove this item
19501 overlapping = true;
19502 }
19503 }
19504 }
19505
19506 // Now the composite boxes are placed, we need to put the original boxes within them
19507 i = 0;
19508 each(boxes, function(box) {
19509 var posInCompositeBox = 0;
19510 each(box.targets, function() {
19511 origBoxes[i].pos = box.pos + posInCompositeBox;
19512 posInCompositeBox += origBoxes[i].size;
19513 i++;
19514 });
19515 });
19516
19517 // Add the rest (hidden) boxes and sort by target
19518 origBoxes.push.apply(origBoxes, restBoxes);
19519 stableSort(origBoxes, sortByTarget);
19520 };
19521
19522
19523 /**
19524 * Draw the data labels
19525 */
19526 Series.prototype.drawDataLabels = function() {
19527
19528 var series = this,
19529 seriesOptions = series.options,
19530 options = seriesOptions.dataLabels,
19531 points = series.points,
19532 pointOptions,
19533 generalOptions,
19534 hasRendered = series.hasRendered || 0,
19535 str,
19536 dataLabelsGroup,
19537 defer = pick(options.defer, true),
19538 renderer = series.chart.renderer;
19539
19540 if (options.enabled || series._hasPointLabels) {
19541
19542 // Process default alignment of data labels for columns
19543 if (series.dlProcessOptions) {
19544 series.dlProcessOptions(options);
19545 }
19546
19547 // Create a separate group for the data labels to avoid rotation
19548 dataLabelsGroup = series.plotGroup(
19549 'dataLabelsGroup',
19550 'data-labels',
19551 defer && !hasRendered ? 'hidden' : 'visible', // #5133
19552 options.zIndex || 6
19553 );
19554
19555 if (defer) {
19556 dataLabelsGroup.attr({
19557 opacity: +hasRendered
19558 }); // #3300
19559 if (!hasRendered) {
19560 addEvent(series, 'afterAnimate', function() {
19561 if (series.visible) { // #2597, #3023, #3024
19562 dataLabelsGroup.show(true);
19563 }
19564 dataLabelsGroup[seriesOptions.animation ? 'animate' : 'attr']({
19565 opacity: 1
19566 }, {
19567 duration: 200
19568 });
19569 });
19570 }
19571 }
19572
19573 // Make the labels for each point
19574 generalOptions = options;
19575 each(points, function(point) {
19576
19577 var enabled,
19578 dataLabel = point.dataLabel,
19579 labelConfig,
19580 attr,
19581 name,
19582 rotation,
19583 connector = point.connector,
19584 isNew = true,
19585 style,
19586 moreStyle = {};
19587
19588 // Determine if each data label is enabled
19589 pointOptions = point.dlOptions || (point.options && point.options.dataLabels); // dlOptions is used in treemaps
19590 enabled = pick(pointOptions && pointOptions.enabled, generalOptions.enabled) && point.y !== null; // #2282, #4641
19591
19592
19593 // If the point is outside the plot area, destroy it. #678, #820
19594 if (dataLabel && !enabled) {
19595 point.dataLabel = dataLabel.destroy();
19596
19597 // Individual labels are disabled if the are explicitly disabled
19598 // in the point options, or if they fall outside the plot area.
19599 } else if (enabled) {
19600
19601 // Create individual options structure that can be extended without
19602 // affecting others
19603 options = merge(generalOptions, pointOptions);
19604 style = options.style;
19605
19606 rotation = options.rotation;
19607
19608 // Get the string
19609 labelConfig = point.getLabelConfig();
19610 str = options.format ?
19611 format(options.format, labelConfig) :
19612 options.formatter.call(labelConfig, options);
19613
19614
19615 // Determine the color
19616 style.color = pick(options.color, style.color, series.color, '#000000');
19617
19618
19619 // update existing label
19620 if (dataLabel) {
19621
19622 if (defined(str)) {
19623 dataLabel
19624 .attr({
19625 text: str
19626 });
19627 isNew = false;
19628
19629 } else { // #1437 - the label is shown conditionally
19630 point.dataLabel = dataLabel = dataLabel.destroy();
19631 if (connector) {
19632 point.connector = connector.destroy();
19633 }
19634 }
19635
19636 // create new label
19637 } else if (defined(str)) {
19638 attr = {
19639 //align: align,
19640
19641 fill: options.backgroundColor,
19642 stroke: options.borderColor,
19643 'stroke-width': options.borderWidth,
19644
19645 r: options.borderRadius || 0,
19646 rotation: rotation,
19647 padding: options.padding,
19648 zIndex: 1
19649 };
19650
19651
19652 // Get automated contrast color
19653 if (style.color === 'contrast') {
19654 moreStyle.color = options.inside || options.distance < 0 || !!seriesOptions.stacking ?
19655 renderer.getContrast(point.color || series.color) :
19656 '#000000';
19657 }
19658
19659 if (seriesOptions.cursor) {
19660 moreStyle.cursor = seriesOptions.cursor;
19661 }
19662
19663
19664
19665 // Remove unused attributes (#947)
19666 for (name in attr) {
19667 if (attr[name] === undefined) {
19668 delete attr[name];
19669 }
19670 }
19671
19672 dataLabel = point.dataLabel = renderer[rotation ? 'text' : 'label']( // labels don't support rotation
19673 str,
19674 0, -9999,
19675 options.shape,
19676 null,
19677 null,
19678 options.useHTML,
19679 null,
19680 'data-label'
19681 )
19682 .attr(attr);
19683
19684 dataLabel.addClass('highcharts-data-label-color-' + point.colorIndex + ' ' + (options.className || ''));
19685
19686
19687 // Styles must be applied before add in order to read text bounding box
19688 dataLabel.css(extend(style, moreStyle));
19689
19690
19691 dataLabel.add(dataLabelsGroup);
19692
19693
19694 dataLabel.shadow(options.shadow);
19695
19696
19697
19698 }
19699
19700 if (dataLabel) {
19701 // Now the data label is created and placed at 0,0, so we need to align it
19702 series.alignDataLabel(point, dataLabel, options, null, isNew);
19703 }
19704 }
19705 });
19706 }
19707 };
19708
19709 /**
19710 * Align each individual data label
19711 */
19712 Series.prototype.alignDataLabel = function(point, dataLabel, options, alignTo, isNew) {
19713 var chart = this.chart,
19714 inverted = chart.inverted,
19715 plotX = pick(point.plotX, -9999),
19716 plotY = pick(point.plotY, -9999),
19717 bBox = dataLabel.getBBox(),
19718 fontSize,
19719 baseline,
19720 rotation = options.rotation,
19721 normRotation,
19722 negRotation,
19723 align = options.align,
19724 rotCorr, // rotation correction
19725 // Math.round for rounding errors (#2683), alignTo to allow column labels (#2700)
19726 visible = this.visible && (point.series.forceDL || chart.isInsidePlot(plotX, Math.round(plotY), inverted) ||
19727 (alignTo && chart.isInsidePlot(plotX, inverted ? alignTo.x + 1 : alignTo.y + alignTo.height - 1, inverted))),
19728 alignAttr, // the final position;
19729 justify = pick(options.overflow, 'justify') === 'justify';
19730
19731 if (visible) {
19732
19733
19734 fontSize = options.style.fontSize;
19735
19736
19737 baseline = chart.renderer.fontMetrics(fontSize, dataLabel).b;
19738
19739 // The alignment box is a singular point
19740 alignTo = extend({
19741 x: inverted ? chart.plotWidth - plotY : plotX,
19742 y: Math.round(inverted ? chart.plotHeight - plotX : plotY),
19743 width: 0,
19744 height: 0
19745 }, alignTo);
19746
19747 // Add the text size for alignment calculation
19748 extend(options, {
19749 width: bBox.width,
19750 height: bBox.height
19751 });
19752
19753 // Allow a hook for changing alignment in the last moment, then do the alignment
19754 if (rotation) {
19755 justify = false; // Not supported for rotated text
19756 rotCorr = chart.renderer.rotCorr(baseline, rotation); // #3723
19757 alignAttr = {
19758 x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x,
19759 y: alignTo.y + options.y + {
19760 top: 0,
19761 middle: 0.5,
19762 bottom: 1
19763 }[options.verticalAlign] * alignTo.height
19764 };
19765 dataLabel[isNew ? 'attr' : 'animate'](alignAttr)
19766 .attr({ // #3003
19767 align: align
19768 });
19769
19770 // Compensate for the rotated label sticking out on the sides
19771 normRotation = (rotation + 720) % 360;
19772 negRotation = normRotation > 180 && normRotation < 360;
19773
19774 if (align === 'left') {
19775 alignAttr.y -= negRotation ? bBox.height : 0;
19776 } else if (align === 'center') {
19777 alignAttr.x -= bBox.width / 2;
19778 alignAttr.y -= bBox.height / 2;
19779 } else if (align === 'right') {
19780 alignAttr.x -= bBox.width;
19781 alignAttr.y -= negRotation ? 0 : bBox.height;
19782 }
19783
19784
19785 } else {
19786 dataLabel.align(options, null, alignTo);
19787 alignAttr = dataLabel.alignAttr;
19788 }
19789
19790 // Handle justify or crop
19791 if (justify) {
19792 this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew);
19793
19794 // Now check that the data label is within the plot area
19795 } else if (pick(options.crop, true)) {
19796 visible = chart.isInsidePlot(alignAttr.x, alignAttr.y) && chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height);
19797 }
19798
19799 // When we're using a shape, make it possible with a connector or an arrow pointing to thie point
19800 if (options.shape && !rotation) {
19801 dataLabel.attr({
19802 anchorX: point.plotX,
19803 anchorY: point.plotY
19804 });
19805 }
19806 }
19807
19808 // Show or hide based on the final aligned position
19809 if (!visible) {
19810 stop(dataLabel);
19811 dataLabel.attr({
19812 y: -9999
19813 });
19814 dataLabel.placed = false; // don't animate back in
19815 }
19816
19817 };
19818
19819 /**
19820 * If data labels fall partly outside the plot area, align them back in, in a way that
19821 * doesn't hide the point.
19822 */
19823 Series.prototype.justifyDataLabel = function(dataLabel, options, alignAttr, bBox, alignTo, isNew) {
19824 var chart = this.chart,
19825 align = options.align,
19826 verticalAlign = options.verticalAlign,
19827 off,
19828 justified,
19829 padding = dataLabel.box ? 0 : (dataLabel.padding || 0);
19830
19831 // Off left
19832 off = alignAttr.x + padding;
19833 if (off < 0) {
19834 if (align === 'right') {
19835 options.align = 'left';
19836 } else {
19837 options.x = -off;
19838 }
19839 justified = true;
19840 }
19841
19842 // Off right
19843 off = alignAttr.x + bBox.width - padding;
19844 if (off > chart.plotWidth) {
19845 if (align === 'left') {
19846 options.align = 'right';
19847 } else {
19848 options.x = chart.plotWidth - off;
19849 }
19850 justified = true;
19851 }
19852
19853 // Off top
19854 off = alignAttr.y + padding;
19855 if (off < 0) {
19856 if (verticalAlign === 'bottom') {
19857 options.verticalAlign = 'top';
19858 } else {
19859 options.y = -off;
19860 }
19861 justified = true;
19862 }
19863
19864 // Off bottom
19865 off = alignAttr.y + bBox.height - padding;
19866 if (off > chart.plotHeight) {
19867 if (verticalAlign === 'top') {
19868 options.verticalAlign = 'bottom';
19869 } else {
19870 options.y = chart.plotHeight - off;
19871 }
19872 justified = true;
19873 }
19874
19875 if (justified) {
19876 dataLabel.placed = !isNew;
19877 dataLabel.align(options, null, alignTo);
19878 }
19879 };
19880
19881 /**
19882 * Override the base drawDataLabels method by pie specific functionality
19883 */
19884 if (seriesTypes.pie) {
19885 seriesTypes.pie.prototype.drawDataLabels = function() {
19886 var series = this,
19887 data = series.data,
19888 point,
19889 chart = series.chart,
19890 options = series.options.dataLabels,
19891 connectorPadding = pick(options.connectorPadding, 10),
19892 connectorWidth = pick(options.connectorWidth, 1),
19893 plotWidth = chart.plotWidth,
19894 plotHeight = chart.plotHeight,
19895 connector,
19896 distanceOption = options.distance,
19897 seriesCenter = series.center,
19898 radius = seriesCenter[2] / 2,
19899 centerY = seriesCenter[1],
19900 outside = distanceOption > 0,
19901 dataLabel,
19902 dataLabelWidth,
19903 labelPos,
19904 labelHeight,
19905 halves = [ // divide the points into right and left halves for anti collision
19906 [], // right
19907 [] // left
19908 ],
19909 x,
19910 y,
19911 visibility,
19912 j,
19913 overflow = [0, 0, 0, 0]; // top, right, bottom, left
19914
19915 // get out if not enabled
19916 if (!series.visible || (!options.enabled && !series._hasPointLabels)) {
19917 return;
19918 }
19919
19920 // run parent method
19921 Series.prototype.drawDataLabels.apply(series);
19922
19923 each(data, function(point) {
19924 if (point.dataLabel && point.visible) { // #407, #2510
19925
19926 // Arrange points for detection collision
19927 halves[point.half].push(point);
19928
19929 // Reset positions (#4905)
19930 point.dataLabel._pos = null;
19931 }
19932 });
19933
19934 /* Loop over the points in each half, starting from the top and bottom
19935 * of the pie to detect overlapping labels.
19936 */
19937 each(halves, function(points, i) {
19938
19939 var top,
19940 bottom,
19941 length = points.length,
19942 positions,
19943 naturalY,
19944 size;
19945
19946 if (!length) {
19947 return;
19948 }
19949
19950 // Sort by angle
19951 series.sortByAngle(points, i - 0.5);
19952
19953 // Only do anti-collision when we are outside the pie and have connectors (#856)
19954 if (distanceOption > 0) {
19955 top = Math.max(0, centerY - radius - distanceOption);
19956 bottom = Math.min(centerY + radius + distanceOption, chart.plotHeight);
19957 positions = map(points, function(point) {
19958 if (point.dataLabel) {
19959 size = point.dataLabel.getBBox().height || 21;
19960 return {
19961 target: point.labelPos[1] - top + size / 2,
19962 size: size,
19963 rank: point.y
19964 };
19965 }
19966 });
19967 H.distribute(positions, bottom + size - top);
19968 }
19969
19970 // now the used slots are sorted, fill them up sequentially
19971 for (j = 0; j < length; j++) {
19972
19973 point = points[j];
19974 labelPos = point.labelPos;
19975 dataLabel = point.dataLabel;
19976 visibility = point.visible === false ? 'hidden' : 'inherit';
19977 naturalY = labelPos[1];
19978
19979 if (positions) {
19980 if (positions[j].pos === undefined) {
19981 visibility = 'hidden';
19982 } else {
19983 labelHeight = positions[j].size;
19984 y = top + positions[j].pos;
19985 }
19986
19987 } else {
19988 y = naturalY;
19989 }
19990
19991 // get the x - use the natural x position for labels near the top and bottom, to prevent the top
19992 // and botton slice connectors from touching each other on either side
19993 if (options.justify) {
19994 x = seriesCenter[0] + (i ? -1 : 1) * (radius + distanceOption);
19995 } else {
19996 x = series.getX(y < top + 2 || y > bottom - 2 ? naturalY : y, i);
19997 }
19998
19999
20000 // Record the placement and visibility
20001 dataLabel._attr = {
20002 visibility: visibility,
20003 align: labelPos[6]
20004 };
20005 dataLabel._pos = {
20006 x: x + options.x +
20007 ({
20008 left: connectorPadding,
20009 right: -connectorPadding
20010 }[labelPos[6]] || 0),
20011 y: y + options.y - 10 // 10 is for the baseline (label vs text)
20012 };
20013 labelPos.x = x;
20014 labelPos.y = y;
20015
20016
20017 // Detect overflowing data labels
20018 if (series.options.size === null) {
20019 dataLabelWidth = dataLabel.width;
20020 // Overflow left
20021 if (x - dataLabelWidth < connectorPadding) {
20022 overflow[3] = Math.max(Math.round(dataLabelWidth - x + connectorPadding), overflow[3]);
20023
20024 // Overflow right
20025 } else if (x + dataLabelWidth > plotWidth - connectorPadding) {
20026 overflow[1] = Math.max(Math.round(x + dataLabelWidth - plotWidth + connectorPadding), overflow[1]);
20027 }
20028
20029 // Overflow top
20030 if (y - labelHeight / 2 < 0) {
20031 overflow[0] = Math.max(Math.round(-y + labelHeight / 2), overflow[0]);
20032
20033 // Overflow left
20034 } else if (y + labelHeight / 2 > plotHeight) {
20035 overflow[2] = Math.max(Math.round(y + labelHeight / 2 - plotHeight), overflow[2]);
20036 }
20037 }
20038 } // for each point
20039 }); // for each half
20040
20041 // Do not apply the final placement and draw the connectors until we have verified
20042 // that labels are not spilling over.
20043 if (arrayMax(overflow) === 0 || this.verifyDataLabelOverflow(overflow)) {
20044
20045 // Place the labels in the final position
20046 this.placeDataLabels();
20047
20048 // Draw the connectors
20049 if (outside && connectorWidth) {
20050 each(this.points, function(point) {
20051 var isNew;
20052
20053 connector = point.connector;
20054 dataLabel = point.dataLabel;
20055
20056 if (dataLabel && dataLabel._pos && point.visible) {
20057 visibility = dataLabel._attr.visibility;
20058
20059 isNew = !connector;
20060
20061 if (isNew) {
20062 point.connector = connector = chart.renderer.path()
20063 .addClass('highcharts-data-label-connector highcharts-color-' + point.colorIndex)
20064 .add(series.dataLabelsGroup);
20065
20066
20067 connector.attr({
20068 'stroke-width': connectorWidth,
20069 'stroke': options.connectorColor || point.color || '#666666'
20070 });
20071
20072 }
20073 connector[isNew ? 'attr' : 'animate']({
20074 d: series.connectorPath(point.labelPos)
20075 });
20076 connector.attr('visibility', visibility);
20077
20078 } else if (connector) {
20079 point.connector = connector.destroy();
20080 }
20081 });
20082 }
20083 }
20084 };
20085
20086 /**
20087 * Extendable method for getting the path of the connector between the data label
20088 * and the pie slice.
20089 */
20090 seriesTypes.pie.prototype.connectorPath = function(labelPos) {
20091 var x = labelPos.x,
20092 y = labelPos.y;
20093 return pick(this.options.softConnector, true) ? [
20094 'M',
20095 x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
20096 'C',
20097 x, y, // first break, next to the label
20098 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5],
20099 labelPos[2], labelPos[3], // second break
20100 'L',
20101 labelPos[4], labelPos[5] // base
20102 ] : [
20103 'M',
20104 x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
20105 'L',
20106 labelPos[2], labelPos[3], // second break
20107 'L',
20108 labelPos[4], labelPos[5] // base
20109 ];
20110 };
20111
20112 /**
20113 * Perform the final placement of the data labels after we have verified that they
20114 * fall within the plot area.
20115 */
20116 seriesTypes.pie.prototype.placeDataLabels = function() {
20117 each(this.points, function(point) {
20118 var dataLabel = point.dataLabel,
20119 _pos;
20120
20121 if (dataLabel && point.visible) {
20122 _pos = dataLabel._pos;
20123 if (_pos) {
20124 dataLabel.attr(dataLabel._attr);
20125 dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos);
20126 dataLabel.moved = true;
20127 } else if (dataLabel) {
20128 dataLabel.attr({
20129 y: -9999
20130 });
20131 }
20132 }
20133 });
20134 };
20135
20136 seriesTypes.pie.prototype.alignDataLabel = noop;
20137
20138 /**
20139 * Verify whether the data labels are allowed to draw, or we should run more translation and data
20140 * label positioning to keep them inside the plot area. Returns true when data labels are ready
20141 * to draw.
20142 */
20143 seriesTypes.pie.prototype.verifyDataLabelOverflow = function(overflow) {
20144
20145 var center = this.center,
20146 options = this.options,
20147 centerOption = options.center,
20148 minSize = options.minSize || 80,
20149 newSize = minSize,
20150 ret;
20151
20152 // Handle horizontal size and center
20153 if (centerOption[0] !== null) { // Fixed center
20154 newSize = Math.max(center[2] - Math.max(overflow[1], overflow[3]), minSize);
20155
20156 } else { // Auto center
20157 newSize = Math.max(
20158 center[2] - overflow[1] - overflow[3], // horizontal overflow
20159 minSize
20160 );
20161 center[0] += (overflow[3] - overflow[1]) / 2; // horizontal center
20162 }
20163
20164 // Handle vertical size and center
20165 if (centerOption[1] !== null) { // Fixed center
20166 newSize = Math.max(Math.min(newSize, center[2] - Math.max(overflow[0], overflow[2])), minSize);
20167
20168 } else { // Auto center
20169 newSize = Math.max(
20170 Math.min(
20171 newSize,
20172 center[2] - overflow[0] - overflow[2] // vertical overflow
20173 ),
20174 minSize
20175 );
20176 center[1] += (overflow[0] - overflow[2]) / 2; // vertical center
20177 }
20178
20179 // If the size must be decreased, we need to run translate and drawDataLabels again
20180 if (newSize < center[2]) {
20181 center[2] = newSize;
20182 center[3] = Math.min(relativeLength(options.innerSize || 0, newSize), newSize); // #3632
20183 this.translate(center);
20184
20185 if (this.drawDataLabels) {
20186 this.drawDataLabels();
20187 }
20188 // Else, return true to indicate that the pie and its labels is within the plot area
20189 } else {
20190 ret = true;
20191 }
20192 return ret;
20193 };
20194 }
20195
20196 if (seriesTypes.column) {
20197
20198 /**
20199 * Override the basic data label alignment by adjusting for the position of the column
20200 */
20201 seriesTypes.column.prototype.alignDataLabel = function(point, dataLabel, options, alignTo, isNew) {
20202 var inverted = this.chart.inverted,
20203 series = point.series,
20204 dlBox = point.dlBox || point.shapeArgs, // data label box for alignment
20205 below = pick(point.below, point.plotY > pick(this.translatedThreshold, series.yAxis.len)), // point.below is used in range series
20206 inside = pick(options.inside, !!this.options.stacking), // draw it inside the box?
20207 overshoot;
20208
20209 // Align to the column itself, or the top of it
20210 if (dlBox) { // Area range uses this method but not alignTo
20211 alignTo = merge(dlBox);
20212
20213 if (alignTo.y < 0) {
20214 alignTo.height += alignTo.y;
20215 alignTo.y = 0;
20216 }
20217 overshoot = alignTo.y + alignTo.height - series.yAxis.len;
20218 if (overshoot > 0) {
20219 alignTo.height -= overshoot;
20220 }
20221
20222 if (inverted) {
20223 alignTo = {
20224 x: series.yAxis.len - alignTo.y - alignTo.height,
20225 y: series.xAxis.len - alignTo.x - alignTo.width,
20226 width: alignTo.height,
20227 height: alignTo.width
20228 };
20229 }
20230
20231 // Compute the alignment box
20232 if (!inside) {
20233 if (inverted) {
20234 alignTo.x += below ? 0 : alignTo.width;
20235 alignTo.width = 0;
20236 } else {
20237 alignTo.y += below ? alignTo.height : 0;
20238 alignTo.height = 0;
20239 }
20240 }
20241 }
20242
20243
20244 // When alignment is undefined (typically columns and bars), display the individual
20245 // point below or above the point depending on the threshold
20246 options.align = pick(
20247 options.align, !inverted || inside ? 'center' : below ? 'right' : 'left'
20248 );
20249 options.verticalAlign = pick(
20250 options.verticalAlign,
20251 inverted || inside ? 'middle' : below ? 'top' : 'bottom'
20252 );
20253
20254 // Call the parent method
20255 Series.prototype.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew);
20256 };
20257 }
20258
20259 }(Highcharts));
20260 (function(H) {
20261 /**
20262 * (c) 2009-2016 Torstein Honsi
20263 *
20264 * License: www.highcharts.com/license
20265 */
20266 'use strict';
20267 /**
20268 * Highcharts module to hide overlapping data labels. This module is included in Highcharts.
20269 */
20270 var Chart = H.Chart,
20271 each = H.each,
20272 pick = H.pick,
20273 addEvent = H.addEvent;
20274
20275 // Collect potensial overlapping data labels. Stack labels probably don't need to be
20276 // considered because they are usually accompanied by data labels that lie inside the columns.
20277 Chart.prototype.callbacks.push(function(chart) {
20278 function collectAndHide() {
20279 var labels = [];
20280
20281 each(chart.series, function(series) {
20282 var dlOptions = series.options.dataLabels,
20283 collections = series.dataLabelCollections || ['dataLabel']; // Range series have two collections
20284 if ((dlOptions.enabled || series._hasPointLabels) && !dlOptions.allowOverlap && series.visible) { // #3866
20285 each(collections, function(coll) {
20286 each(series.points, function(point) {
20287 if (point[coll]) {
20288 point[coll].labelrank = pick(point.labelrank, point.shapeArgs && point.shapeArgs.height); // #4118
20289 labels.push(point[coll]);
20290 }
20291 });
20292 });
20293 }
20294 });
20295 chart.hideOverlappingLabels(labels);
20296 }
20297
20298 // Do it now ...
20299 collectAndHide();
20300
20301 // ... and after each chart redraw
20302 addEvent(chart, 'redraw', collectAndHide);
20303
20304 });
20305
20306 /**
20307 * Hide overlapping labels. Labels are moved and faded in and out on zoom to provide a smooth
20308 * visual imression.
20309 */
20310 Chart.prototype.hideOverlappingLabels = function(labels) {
20311
20312 var len = labels.length,
20313 label,
20314 i,
20315 j,
20316 label1,
20317 label2,
20318 isIntersecting,
20319 pos1,
20320 pos2,
20321 parent1,
20322 parent2,
20323 padding,
20324 intersectRect = function(x1, y1, w1, h1, x2, y2, w2, h2) {
20325 return !(
20326 x2 > x1 + w1 ||
20327 x2 + w2 < x1 ||
20328 y2 > y1 + h1 ||
20329 y2 + h2 < y1
20330 );
20331 };
20332
20333 // Mark with initial opacity
20334 for (i = 0; i < len; i++) {
20335 label = labels[i];
20336 if (label) {
20337 label.oldOpacity = label.opacity;
20338 label.newOpacity = 1;
20339 }
20340 }
20341
20342 // Prevent a situation in a gradually rising slope, that each label
20343 // will hide the previous one because the previous one always has
20344 // lower rank.
20345 labels.sort(function(a, b) {
20346 return (b.labelrank || 0) - (a.labelrank || 0);
20347 });
20348
20349 // Detect overlapping labels
20350 for (i = 0; i < len; i++) {
20351 label1 = labels[i];
20352
20353 for (j = i + 1; j < len; ++j) {
20354 label2 = labels[j];
20355 if (label1 && label2 && label1.placed && label2.placed && label1.newOpacity !== 0 && label2.newOpacity !== 0) {
20356 pos1 = label1.alignAttr;
20357 pos2 = label2.alignAttr;
20358 parent1 = label1.parentGroup; // Different panes have different positions
20359 parent2 = label2.parentGroup;
20360 padding = 2 * (label1.box ? 0 : label1.padding); // Substract the padding if no background or border (#4333)
20361 isIntersecting = intersectRect(
20362 pos1.x + parent1.translateX,
20363 pos1.y + parent1.translateY,
20364 label1.width - padding,
20365 label1.height - padding,
20366 pos2.x + parent2.translateX,
20367 pos2.y + parent2.translateY,
20368 label2.width - padding,
20369 label2.height - padding
20370 );
20371
20372 if (isIntersecting) {
20373 (label1.labelrank < label2.labelrank ? label1 : label2).newOpacity = 0;
20374 }
20375 }
20376 }
20377 }
20378
20379 // Hide or show
20380 each(labels, function(label) {
20381 var complete,
20382 newOpacity;
20383
20384 if (label) {
20385 newOpacity = label.newOpacity;
20386
20387 if (label.oldOpacity !== newOpacity && label.placed) {
20388
20389 // Make sure the label is completely hidden to avoid catching clicks (#4362)
20390 if (newOpacity) {
20391 label.show(true);
20392 } else {
20393 complete = function() {
20394 label.hide();
20395 };
20396 }
20397
20398 // Animate or set the opacity
20399 label.alignAttr.opacity = newOpacity;
20400 label[label.isOld ? 'animate' : 'attr'](label.alignAttr, null, complete);
20401
20402 }
20403 label.isOld = true;
20404 }
20405 });
20406 };
20407
20408 }(Highcharts));
20409 (function(H) {
20410 /**
20411 * (c) 2010-2016 Torstein Honsi
20412 *
20413 * License: www.highcharts.com/license
20414 */
20415 'use strict';
20416 var addEvent = H.addEvent,
20417 Chart = H.Chart,
20418 createElement = H.createElement,
20419 css = H.css,
20420 defaultOptions = H.defaultOptions,
20421 defaultPlotOptions = H.defaultPlotOptions,
20422 each = H.each,
20423 extend = H.extend,
20424 fireEvent = H.fireEvent,
20425 hasTouch = H.hasTouch,
20426 inArray = H.inArray,
20427 isObject = H.isObject,
20428 Legend = H.Legend,
20429 merge = H.merge,
20430 pick = H.pick,
20431 Point = H.Point,
20432 Series = H.Series,
20433 seriesTypes = H.seriesTypes,
20434 svg = H.svg,
20435 TrackerMixin;
20436 /**
20437 * TrackerMixin for points and graphs
20438 */
20439 TrackerMixin = H.TrackerMixin = {
20440
20441 drawTrackerPoint: function() {
20442 var series = this,
20443 chart = series.chart,
20444 pointer = chart.pointer,
20445 onMouseOver = function(e) {
20446 var target = e.target,
20447 point;
20448
20449 while (target && !point) {
20450 point = target.point;
20451 target = target.parentNode;
20452 }
20453
20454 if (point !== undefined && point !== chart.hoverPoint) { // undefined on graph in scatterchart
20455 point.onMouseOver(e);
20456 }
20457 };
20458
20459 // Add reference to the point
20460 each(series.points, function(point) {
20461 if (point.graphic) {
20462 point.graphic.element.point = point;
20463 }
20464 if (point.dataLabel) {
20465 point.dataLabel.element.point = point;
20466 }
20467 });
20468
20469 // Add the event listeners, we need to do this only once
20470 if (!series._hasTracking) {
20471 each(series.trackerGroups, function(key) {
20472 if (series[key]) { // we don't always have dataLabelsGroup
20473 series[key]
20474 .addClass('highcharts-tracker')
20475 .on('mouseover', onMouseOver)
20476 .on('mouseout', function(e) {
20477 pointer.onTrackerMouseOut(e);
20478 });
20479 if (hasTouch) {
20480 series[key].on('touchstart', onMouseOver);
20481 }
20482
20483
20484 if (series.options.cursor) {
20485 series[key]
20486 .css(css)
20487 .css({
20488 cursor: series.options.cursor
20489 });
20490 }
20491
20492 }
20493 });
20494 series._hasTracking = true;
20495 }
20496 },
20497
20498 /**
20499 * Draw the tracker object that sits above all data labels and markers to
20500 * track mouse events on the graph or points. For the line type charts
20501 * the tracker uses the same graphPath, but with a greater stroke width
20502 * for better control.
20503 */
20504 drawTrackerGraph: function() {
20505 var series = this,
20506 options = series.options,
20507 trackByArea = options.trackByArea,
20508 trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath),
20509 trackerPathLength = trackerPath.length,
20510 chart = series.chart,
20511 pointer = chart.pointer,
20512 renderer = chart.renderer,
20513 snap = chart.options.tooltip.snap,
20514 tracker = series.tracker,
20515 i,
20516 onMouseOver = function() {
20517 if (chart.hoverSeries !== series) {
20518 series.onMouseOver();
20519 }
20520 },
20521 /*
20522 * Empirical lowest possible opacities for TRACKER_FILL for an element to stay invisible but clickable
20523 * IE6: 0.002
20524 * IE7: 0.002
20525 * IE8: 0.002
20526 * IE9: 0.00000000001 (unlimited)
20527 * IE10: 0.0001 (exporting only)
20528 * FF: 0.00000000001 (unlimited)
20529 * Chrome: 0.000001
20530 * Safari: 0.000001
20531 * Opera: 0.00000000001 (unlimited)
20532 */
20533 TRACKER_FILL = 'rgba(192,192,192,' + (svg ? 0.0001 : 0.002) + ')';
20534
20535 // Extend end points. A better way would be to use round linecaps,
20536 // but those are not clickable in VML.
20537 if (trackerPathLength && !trackByArea) {
20538 i = trackerPathLength + 1;
20539 while (i--) {
20540 if (trackerPath[i] === 'M') { // extend left side
20541 trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], 'L');
20542 }
20543 if ((i && trackerPath[i] === 'M') || i === trackerPathLength) { // extend right side
20544 trackerPath.splice(i, 0, 'L', trackerPath[i - 2] + snap, trackerPath[i - 1]);
20545 }
20546 }
20547 }
20548
20549 // handle single points
20550 /*for (i = 0; i < singlePoints.length; i++) {
20551 singlePoint = singlePoints[i];
20552 trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY,
20553 L, singlePoint.plotX + snap, singlePoint.plotY);
20554 }*/
20555
20556 // draw the tracker
20557 if (tracker) {
20558 tracker.attr({
20559 d: trackerPath
20560 });
20561 } else if (series.graph) { // create
20562
20563 series.tracker = renderer.path(trackerPath)
20564 .attr({
20565 'stroke-linejoin': 'round', // #1225
20566 visibility: series.visible ? 'visible' : 'hidden',
20567 stroke: TRACKER_FILL,
20568 fill: trackByArea ? TRACKER_FILL : 'none',
20569 'stroke-width': series.graph.strokeWidth() + (trackByArea ? 0 : 2 * snap),
20570 zIndex: 2
20571 })
20572 .add(series.group);
20573
20574 // The tracker is added to the series group, which is clipped, but is covered
20575 // by the marker group. So the marker group also needs to capture events.
20576 each([series.tracker, series.markerGroup], function(tracker) {
20577 tracker.addClass('highcharts-tracker')
20578 .on('mouseover', onMouseOver)
20579 .on('mouseout', function(e) {
20580 pointer.onTrackerMouseOut(e);
20581 });
20582
20583
20584 if (options.cursor) {
20585 tracker.css({
20586 cursor: options.cursor
20587 });
20588 }
20589
20590
20591 if (hasTouch) {
20592 tracker.on('touchstart', onMouseOver);
20593 }
20594 });
20595 }
20596 }
20597 };
20598 /* End TrackerMixin */
20599
20600
20601 /**
20602 * Add tracking event listener to the series group, so the point graphics
20603 * themselves act as trackers
20604 */
20605
20606 if (seriesTypes.column) {
20607 seriesTypes.column.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
20608 }
20609
20610 if (seriesTypes.pie) {
20611 seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
20612 }
20613
20614 if (seriesTypes.scatter) {
20615 seriesTypes.scatter.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
20616 }
20617
20618 /*
20619 * Extend Legend for item events
20620 */
20621 extend(Legend.prototype, {
20622
20623 setItemEvents: function(item, legendItem, useHTML) {
20624 var legend = this,
20625 chart = legend.chart,
20626 activeClass = 'highcharts-legend-' + (item.series ? 'point' : 'series') + '-active';
20627
20628 // Set the events on the item group, or in case of useHTML, the item itself (#1249)
20629 (useHTML ? legendItem : item.legendGroup).on('mouseover', function() {
20630 item.setState('hover');
20631
20632 // A CSS class to dim or hide other than the hovered series
20633 chart.seriesGroup.addClass(activeClass);
20634
20635
20636 legendItem.css(legend.options.itemHoverStyle);
20637
20638 })
20639 .on('mouseout', function() {
20640
20641 legendItem.css(item.visible ? legend.itemStyle : legend.itemHiddenStyle);
20642
20643
20644 // A CSS class to dim or hide other than the hovered series
20645 chart.seriesGroup.removeClass(activeClass);
20646
20647 item.setState();
20648 })
20649 .on('click', function(event) {
20650 var strLegendItemClick = 'legendItemClick',
20651 fnLegendItemClick = function() {
20652 if (item.setVisible) {
20653 item.setVisible();
20654 }
20655 };
20656
20657 // Pass over the click/touch event. #4.
20658 event = {
20659 browserEvent: event
20660 };
20661
20662 // click the name or symbol
20663 if (item.firePointEvent) { // point
20664 item.firePointEvent(strLegendItemClick, event, fnLegendItemClick);
20665 } else {
20666 fireEvent(item, strLegendItemClick, event, fnLegendItemClick);
20667 }
20668 });
20669 },
20670
20671 createCheckboxForItem: function(item) {
20672 var legend = this;
20673
20674 item.checkbox = createElement('input', {
20675 type: 'checkbox',
20676 checked: item.selected,
20677 defaultChecked: item.selected // required by IE7
20678 }, legend.options.itemCheckboxStyle, legend.chart.container);
20679
20680 addEvent(item.checkbox, 'click', function(event) {
20681 var target = event.target;
20682 fireEvent(
20683 item.series || item,
20684 'checkboxClick', { // #3712
20685 checked: target.checked,
20686 item: item
20687 },
20688 function() {
20689 item.select();
20690 }
20691 );
20692 });
20693 }
20694 });
20695
20696
20697
20698 // Add pointer cursor to legend itemstyle in defaultOptions
20699 defaultOptions.legend.itemStyle.cursor = 'pointer';
20700
20701
20702
20703 /*
20704 * Extend the Chart object with interaction
20705 */
20706
20707 extend(Chart.prototype, {
20708 /**
20709 * Display the zoom button
20710 */
20711 showResetZoom: function() {
20712 var chart = this,
20713 lang = defaultOptions.lang,
20714 btnOptions = chart.options.chart.resetZoomButton,
20715 theme = btnOptions.theme,
20716 states = theme.states,
20717 alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox';
20718
20719 function zoomOut() {
20720 chart.zoomOut();
20721 }
20722
20723 this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, zoomOut, theme, states && states.hover)
20724 .attr({
20725 align: btnOptions.position.align,
20726 title: lang.resetZoomTitle
20727 })
20728 .addClass('highcharts-reset-zoom')
20729 .add()
20730 .align(btnOptions.position, false, alignTo);
20731
20732 },
20733
20734 /**
20735 * Zoom out to 1:1
20736 */
20737 zoomOut: function() {
20738 var chart = this;
20739 fireEvent(chart, 'selection', {
20740 resetSelection: true
20741 }, function() {
20742 chart.zoom();
20743 });
20744 },
20745
20746 /**
20747 * Zoom into a given portion of the chart given by axis coordinates
20748 * @param {Object} event
20749 */
20750 zoom: function(event) {
20751 var chart = this,
20752 hasZoomed,
20753 pointer = chart.pointer,
20754 displayButton = false,
20755 resetZoomButton;
20756
20757 // If zoom is called with no arguments, reset the axes
20758 if (!event || event.resetSelection) {
20759 each(chart.axes, function(axis) {
20760 hasZoomed = axis.zoom();
20761 });
20762 } else { // else, zoom in on all axes
20763 each(event.xAxis.concat(event.yAxis), function(axisData) {
20764 var axis = axisData.axis,
20765 isXAxis = axis.isXAxis;
20766
20767 // don't zoom more than minRange
20768 if (pointer[isXAxis ? 'zoomX' : 'zoomY'] || pointer[isXAxis ? 'pinchX' : 'pinchY']) {
20769 hasZoomed = axis.zoom(axisData.min, axisData.max);
20770 if (axis.displayBtn) {
20771 displayButton = true;
20772 }
20773 }
20774 });
20775 }
20776
20777 // Show or hide the Reset zoom button
20778 resetZoomButton = chart.resetZoomButton;
20779 if (displayButton && !resetZoomButton) {
20780 chart.showResetZoom();
20781 } else if (!displayButton && isObject(resetZoomButton)) {
20782 chart.resetZoomButton = resetZoomButton.destroy();
20783 }
20784
20785
20786 // Redraw
20787 if (hasZoomed) {
20788 chart.redraw(
20789 pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation
20790 );
20791 }
20792 },
20793
20794 /**
20795 * Pan the chart by dragging the mouse across the pane. This function is called
20796 * on mouse move, and the distance to pan is computed from chartX compared to
20797 * the first chartX position in the dragging operation.
20798 */
20799 pan: function(e, panning) {
20800
20801 var chart = this,
20802 hoverPoints = chart.hoverPoints,
20803 doRedraw;
20804
20805 // remove active points for shared tooltip
20806 if (hoverPoints) {
20807 each(hoverPoints, function(point) {
20808 point.setState();
20809 });
20810 }
20811
20812 each(panning === 'xy' ? [1, 0] : [1], function(isX) { // xy is used in maps
20813 var axis = chart[isX ? 'xAxis' : 'yAxis'][0],
20814 horiz = axis.horiz,
20815 mousePos = e[horiz ? 'chartX' : 'chartY'],
20816 mouseDown = horiz ? 'mouseDownX' : 'mouseDownY',
20817 startPos = chart[mouseDown],
20818 halfPointRange = (axis.pointRange || 0) / 2,
20819 extremes = axis.getExtremes(),
20820 newMin = axis.toValue(startPos - mousePos, true) + halfPointRange,
20821 newMax = axis.toValue(startPos + axis.len - mousePos, true) - halfPointRange,
20822 goingLeft = startPos > mousePos; // #3613
20823
20824 if (axis.series.length &&
20825 (goingLeft || newMin > Math.min(extremes.dataMin, extremes.min)) &&
20826 (!goingLeft || newMax < Math.max(extremes.dataMax, extremes.max))) {
20827 axis.setExtremes(newMin, newMax, false, false, {
20828 trigger: 'pan'
20829 });
20830 doRedraw = true;
20831 }
20832
20833 chart[mouseDown] = mousePos; // set new reference for next run
20834 });
20835
20836 if (doRedraw) {
20837 chart.redraw(false);
20838 }
20839 css(chart.container, {
20840 cursor: 'move'
20841 });
20842 }
20843 });
20844
20845 /*
20846 * Extend the Point object with interaction
20847 */
20848 extend(Point.prototype, {
20849 /**
20850 * Toggle the selection status of a point
20851 * @param {Boolean} selected Whether to select or unselect the point.
20852 * @param {Boolean} accumulate Whether to add to the previous selection. By default,
20853 * this happens if the control key (Cmd on Mac) was pressed during clicking.
20854 */
20855 select: function(selected, accumulate) {
20856 var point = this,
20857 series = point.series,
20858 chart = series.chart;
20859
20860 selected = pick(selected, !point.selected);
20861
20862 // fire the event with the default handler
20863 point.firePointEvent(selected ? 'select' : 'unselect', {
20864 accumulate: accumulate
20865 }, function() {
20866 point.selected = point.options.selected = selected;
20867 series.options.data[inArray(point, series.data)] = point.options;
20868
20869 point.setState(selected && 'select');
20870
20871 // unselect all other points unless Ctrl or Cmd + click
20872 if (!accumulate) {
20873 each(chart.getSelectedPoints(), function(loopPoint) {
20874 if (loopPoint.selected && loopPoint !== point) {
20875 loopPoint.selected = loopPoint.options.selected = false;
20876 series.options.data[inArray(loopPoint, series.data)] = loopPoint.options;
20877 loopPoint.setState('');
20878 loopPoint.firePointEvent('unselect');
20879 }
20880 });
20881 }
20882 });
20883 },
20884
20885 /**
20886 * Runs on mouse over the point
20887 *
20888 * @param {Object} e The event arguments
20889 * @param {Boolean} byProximity Falsy for kd points that are closest to the mouse, or to
20890 * actually hovered points. True for other points in shared tooltip.
20891 */
20892 onMouseOver: function(e, byProximity) {
20893 var point = this,
20894 series = point.series,
20895 chart = series.chart,
20896 tooltip = chart.tooltip,
20897 hoverPoint = chart.hoverPoint;
20898
20899 if (chart.hoverSeries !== series) {
20900 series.onMouseOver();
20901 }
20902
20903 // set normal state to previous series
20904 if (hoverPoint && hoverPoint !== point) {
20905 hoverPoint.onMouseOut();
20906 }
20907
20908 if (point.series) { // It may have been destroyed, #4130
20909
20910 // trigger the event
20911 point.firePointEvent('mouseOver');
20912
20913 // update the tooltip
20914 if (tooltip && (!tooltip.shared || series.noSharedTooltip)) {
20915 tooltip.refresh(point, e);
20916 }
20917
20918 // hover this
20919 point.setState('hover');
20920 if (!byProximity) {
20921 chart.hoverPoint = point;
20922 }
20923 }
20924 },
20925
20926 /**
20927 * Runs on mouse out from the point
20928 */
20929 onMouseOut: function() {
20930 var chart = this.series.chart,
20931 hoverPoints = chart.hoverPoints;
20932
20933 this.firePointEvent('mouseOut');
20934
20935 if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887, #2240
20936 this.setState();
20937 chart.hoverPoint = null;
20938 }
20939 },
20940
20941 /**
20942 * Import events from the series' and point's options. Only do it on
20943 * demand, to save processing time on hovering.
20944 */
20945 importEvents: function() {
20946 if (!this.hasImportedEvents) {
20947 var point = this,
20948 options = merge(point.series.options.point, point.options),
20949 events = options.events,
20950 eventType;
20951
20952 point.events = events;
20953
20954 for (eventType in events) {
20955 addEvent(point, eventType, events[eventType]);
20956 }
20957 this.hasImportedEvents = true;
20958
20959 }
20960 },
20961
20962 /**
20963 * Set the point's state
20964 * @param {String} state
20965 */
20966 setState: function(state, move) {
20967 var point = this,
20968 plotX = Math.floor(point.plotX), // #4586
20969 plotY = point.plotY,
20970 series = point.series,
20971 stateOptions = series.options.states[state] || {},
20972 markerOptions = (defaultPlotOptions[series.type].marker && series.options.marker) || {},
20973 normalDisabled = markerOptions.enabled === false,
20974 markerStateOptions = (markerOptions.states && markerOptions.states[state]) || {},
20975 stateDisabled = markerStateOptions.enabled === false,
20976 stateMarkerGraphic = series.stateMarkerGraphic,
20977 pointMarker = point.marker || {},
20978 chart = series.chart,
20979 radius,
20980 halo = series.halo,
20981 haloOptions,
20982 attribs,
20983 newSymbol;
20984
20985 state = state || ''; // empty string
20986
20987 if (
20988 // already has this state
20989 (state === point.state && !move) ||
20990 // selected points don't respond to hover
20991 (point.selected && state !== 'select') ||
20992 // series' state options is disabled
20993 (stateOptions.enabled === false) ||
20994 // general point marker's state options is disabled
20995 (state && (stateDisabled || (normalDisabled && markerStateOptions.enabled === false))) ||
20996 // individual point marker's state options is disabled
20997 (state && pointMarker.states && pointMarker.states[state] && pointMarker.states[state].enabled === false) // #1610
20998
20999 ) {
21000 return;
21001 }
21002
21003 radius = markerStateOptions.radius || (markerOptions.radius + (markerStateOptions.radiusPlus || 0));
21004
21005 // Apply hover styles to the existing point
21006 if (point.graphic) {
21007
21008 if (point.state) {
21009 point.graphic.removeClass('highcharts-point-' + point.state);
21010 }
21011 if (state) {
21012 point.graphic.addClass('highcharts-point-' + state);
21013 }
21014
21015 attribs = radius ? { // new symbol attributes (#507, #612)
21016 x: plotX - radius,
21017 y: plotY - radius,
21018 width: 2 * radius,
21019 height: 2 * radius
21020 } : {};
21021
21022
21023 attribs = merge(series.pointAttribs(point, state), attribs);
21024
21025
21026 point.graphic.attr(attribs);
21027
21028 // Zooming in from a range with no markers to a range with markers
21029 if (stateMarkerGraphic) {
21030 stateMarkerGraphic.hide();
21031 }
21032 } else {
21033 // if a graphic is not applied to each point in the normal state, create a shared
21034 // graphic for the hover state
21035 if (state && markerStateOptions) {
21036 newSymbol = pointMarker.symbol || series.symbol;
21037
21038 // If the point has another symbol than the previous one, throw away the
21039 // state marker graphic and force a new one (#1459)
21040 if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) {
21041 stateMarkerGraphic = stateMarkerGraphic.destroy();
21042 }
21043
21044 // Add a new state marker graphic
21045 if (!stateMarkerGraphic) {
21046 if (newSymbol) {
21047 series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol(
21048 newSymbol,
21049 plotX - radius,
21050 plotY - radius,
21051 2 * radius,
21052 2 * radius
21053 )
21054 .add(series.markerGroup);
21055 stateMarkerGraphic.currentSymbol = newSymbol;
21056 }
21057
21058 // Move the existing graphic
21059 } else {
21060 stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054
21061 x: plotX - radius,
21062 y: plotY - radius
21063 });
21064 }
21065
21066 if (stateMarkerGraphic) {
21067 stateMarkerGraphic.attr(series.pointAttribs(point, state));
21068 }
21069
21070 }
21071
21072 if (stateMarkerGraphic) {
21073 stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? 'show' : 'hide'](); // #2450
21074 stateMarkerGraphic.element.point = point; // #4310
21075 }
21076 }
21077
21078 // Show me your halo
21079 haloOptions = stateOptions.halo;
21080 if (haloOptions && haloOptions.size) {
21081 if (!halo) {
21082 series.halo = halo = chart.renderer.path()
21083 .add(chart.seriesGroup);
21084 }
21085 halo[move ? 'animate' : 'attr']({
21086 d: point.haloPath(haloOptions.size)
21087 });
21088 halo.attr({
21089 'class': 'highcharts-halo highcharts-color-' + pick(point.colorIndex, series.colorIndex)
21090 });
21091
21092
21093 halo.attr(extend({
21094 'fill': point.color || series.color,
21095 'fill-opacity': haloOptions.opacity,
21096 'zIndex': -1 // #4929, IE8 added halo above everything
21097 },
21098 haloOptions.attributes))[move ? 'animate' : 'attr']({
21099 d: point.haloPath(haloOptions.size)
21100 });
21101
21102 } else if (halo) {
21103 halo.attr({
21104 d: []
21105 });
21106 }
21107
21108 point.state = state;
21109 },
21110
21111 /**
21112 * Get the circular path definition for the halo
21113 * @param {Number} size The radius of the circular halo
21114 * @returns {Array} The path definition
21115 */
21116 haloPath: function(size) {
21117 var series = this.series,
21118 chart = series.chart,
21119 plotBox = series.getPlotBox(),
21120 inverted = chart.inverted,
21121 plotX = Math.floor(this.plotX);
21122
21123 return chart.renderer.symbols.circle(
21124 plotBox.translateX + (inverted ? series.yAxis.len - this.plotY : plotX) - size,
21125 plotBox.translateY + (inverted ? series.xAxis.len - plotX : this.plotY) - size,
21126 size * 2,
21127 size * 2
21128 );
21129 }
21130 });
21131
21132 /*
21133 * Extend the Series object with interaction
21134 */
21135
21136 extend(Series.prototype, {
21137 /**
21138 * Series mouse over handler
21139 */
21140 onMouseOver: function() {
21141 var series = this,
21142 chart = series.chart,
21143 hoverSeries = chart.hoverSeries;
21144
21145 // set normal state to previous series
21146 if (hoverSeries && hoverSeries !== series) {
21147 hoverSeries.onMouseOut();
21148 }
21149
21150 // trigger the event, but to save processing time,
21151 // only if defined
21152 if (series.options.events.mouseOver) {
21153 fireEvent(series, 'mouseOver');
21154 }
21155
21156 // hover this
21157 series.setState('hover');
21158 chart.hoverSeries = series;
21159 },
21160
21161 /**
21162 * Series mouse out handler
21163 */
21164 onMouseOut: function() {
21165 // trigger the event only if listeners exist
21166 var series = this,
21167 options = series.options,
21168 chart = series.chart,
21169 tooltip = chart.tooltip,
21170 hoverPoint = chart.hoverPoint;
21171
21172 chart.hoverSeries = null; // #182, set to null before the mouseOut event fires
21173
21174 // trigger mouse out on the point, which must be in this series
21175 if (hoverPoint) {
21176 hoverPoint.onMouseOut();
21177 }
21178
21179 // fire the mouse out event
21180 if (series && options.events.mouseOut) {
21181 fireEvent(series, 'mouseOut');
21182 }
21183
21184
21185 // hide the tooltip
21186 if (tooltip && !options.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) {
21187 tooltip.hide();
21188 }
21189
21190 // set normal state
21191 series.setState();
21192 },
21193
21194 /**
21195 * Set the state of the graph
21196 */
21197 setState: function(state) {
21198 var series = this,
21199 options = series.options,
21200 graph = series.graph,
21201 stateOptions = options.states,
21202 lineWidth = options.lineWidth,
21203 attribs,
21204 i = 0;
21205
21206 state = state || '';
21207
21208 if (series.state !== state) {
21209
21210 // Toggle class names
21211 each([series.group, series.markerGroup], function(group) {
21212 if (group) {
21213 // Old state
21214 if (series.state) {
21215 group.removeClass('highcharts-series-' + series.state);
21216 }
21217 // New state
21218 if (state) {
21219 group.addClass('highcharts-series-' + state);
21220 }
21221 }
21222 });
21223
21224 series.state = state;
21225
21226
21227
21228 if (stateOptions[state] && stateOptions[state].enabled === false) {
21229 return;
21230 }
21231
21232 if (state) {
21233 lineWidth = stateOptions[state].lineWidth || lineWidth + (stateOptions[state].lineWidthPlus || 0); // #4035
21234 }
21235
21236 if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML
21237 attribs = {
21238 'stroke-width': lineWidth
21239 };
21240 // use attr because animate will cause any other animation on the graph to stop
21241 graph.attr(attribs);
21242 while (series['zone-graph-' + i]) {
21243 series['zone-graph-' + i].attr(attribs);
21244 i = i + 1;
21245 }
21246 }
21247
21248 }
21249 },
21250
21251 /**
21252 * Set the visibility of the graph
21253 *
21254 * @param vis {Boolean} True to show the series, false to hide. If undefined,
21255 * the visibility is toggled.
21256 */
21257 setVisible: function(vis, redraw) {
21258 var series = this,
21259 chart = series.chart,
21260 legendItem = series.legendItem,
21261 showOrHide,
21262 ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries,
21263 oldVisibility = series.visible;
21264
21265 // if called without an argument, toggle visibility
21266 series.visible = vis = series.options.visible = series.userOptions.visible = vis === undefined ? !oldVisibility : vis; // #5618
21267 showOrHide = vis ? 'show' : 'hide';
21268
21269 // show or hide elements
21270 each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker'], function(key) {
21271 if (series[key]) {
21272 series[key][showOrHide]();
21273 }
21274 });
21275
21276
21277 // hide tooltip (#1361)
21278 if (chart.hoverSeries === series || (chart.hoverPoint && chart.hoverPoint.series) === series) {
21279 series.onMouseOut();
21280 }
21281
21282
21283 if (legendItem) {
21284 chart.legend.colorizeItem(series, vis);
21285 }
21286
21287
21288 // rescale or adapt to resized chart
21289 series.isDirty = true;
21290 // in a stack, all other series are affected
21291 if (series.options.stacking) {
21292 each(chart.series, function(otherSeries) {
21293 if (otherSeries.options.stacking && otherSeries.visible) {
21294 otherSeries.isDirty = true;
21295 }
21296 });
21297 }
21298
21299 // show or hide linked series
21300 each(series.linkedSeries, function(otherSeries) {
21301 otherSeries.setVisible(vis, false);
21302 });
21303
21304 if (ignoreHiddenSeries) {
21305 chart.isDirtyBox = true;
21306 }
21307 if (redraw !== false) {
21308 chart.redraw();
21309 }
21310
21311 fireEvent(series, showOrHide);
21312 },
21313
21314 /**
21315 * Show the graph
21316 */
21317 show: function() {
21318 this.setVisible(true);
21319 },
21320
21321 /**
21322 * Hide the graph
21323 */
21324 hide: function() {
21325 this.setVisible(false);
21326 },
21327
21328
21329 /**
21330 * Set the selected state of the graph
21331 *
21332 * @param selected {Boolean} True to select the series, false to unselect. If
21333 * undefined, the selection state is toggled.
21334 */
21335 select: function(selected) {
21336 var series = this;
21337 // if called without an argument, toggle
21338 series.selected = selected = (selected === undefined) ? !series.selected : selected;
21339
21340 if (series.checkbox) {
21341 series.checkbox.checked = selected;
21342 }
21343
21344 fireEvent(series, selected ? 'select' : 'unselect');
21345 },
21346
21347 drawTracker: TrackerMixin.drawTrackerGraph
21348 });
21349
21350 }(Highcharts));
21351 (function(H) {
21352 /**
21353 * (c) 2010-2016 Torstein Honsi
21354 *
21355 * License: www.highcharts.com/license
21356 */
21357 'use strict';
21358 var Chart = H.Chart,
21359 each = H.each,
21360 inArray = H.inArray,
21361 isObject = H.isObject,
21362 pick = H.pick,
21363 splat = H.splat;
21364
21365 /**
21366 * Update the chart based on the current chart/document size and options for responsiveness
21367 */
21368 Chart.prototype.setResponsive = function(redraw) {
21369 var options = this.options.responsive;
21370
21371 if (options && options.rules) {
21372 each(options.rules, function(rule) {
21373 this.matchResponsiveRule(rule, redraw);
21374 }, this);
21375 }
21376 };
21377
21378 /**
21379 * Handle a single responsiveness rule
21380 */
21381 Chart.prototype.matchResponsiveRule = function(rule, redraw) {
21382 var respRules = this.respRules,
21383 condition = rule.condition,
21384 matches,
21385 fn = rule.callback || function() {
21386 return this.chartWidth <= pick(condition.maxWidth, Number.MAX_VALUE) &&
21387 this.chartHeight <= pick(condition.maxHeight, Number.MAX_VALUE) &&
21388 this.chartWidth >= pick(condition.minWidth, 0) &&
21389 this.chartHeight >= pick(condition.minHeight, 0);
21390 };
21391
21392
21393 if (rule._id === undefined) {
21394 rule._id = H.idCounter++;
21395 }
21396 matches = fn.call(this);
21397
21398 // Apply a rule
21399 if (!respRules[rule._id] && matches) {
21400
21401 // Store the current state of the options
21402 if (rule.chartOptions) {
21403 respRules[rule._id] = this.currentOptions(rule.chartOptions);
21404 this.update(rule.chartOptions, redraw);
21405 }
21406
21407 // Unapply a rule based on the previous options before the rule
21408 // was applied
21409 } else if (respRules[rule._id] && !matches) {
21410 this.update(respRules[rule._id], redraw);
21411 delete respRules[rule._id];
21412 }
21413 };
21414
21415 /**
21416 * Get the current values for a given set of options. Used before we update
21417 * the chart with a new responsiveness rule.
21418 * TODO: Restore axis options (by id?)
21419 */
21420 Chart.prototype.currentOptions = function(options) {
21421
21422 var ret = {};
21423
21424 /**
21425 * Recurse over a set of options and its current values,
21426 * and store the current values in the ret object.
21427 */
21428 function getCurrent(options, curr, ret) {
21429 var key, i;
21430 for (key in options) {
21431 if (inArray(key, ['series', 'xAxis', 'yAxis']) > -1) {
21432 options[key] = splat(options[key]);
21433
21434 ret[key] = [];
21435 for (i = 0; i < options[key].length; i++) {
21436 ret[key][i] = {};
21437 getCurrent(options[key][i], curr[key][i], ret[key][i]);
21438 }
21439 } else if (isObject(options[key])) {
21440 ret[key] = {};
21441 getCurrent(options[key], curr[key] || {}, ret[key]);
21442 } else {
21443 ret[key] = curr[key] || null;
21444 }
21445 }
21446 }
21447
21448 getCurrent(options, this.options, ret);
21449 return ret;
21450 };
21451
21452 }(Highcharts));
21453 return Highcharts
21454}));