app/assets/javascripts/highcharts.js in highcharts-rails-5.0.0 vs app/assets/javascripts/highcharts.js in highcharts-rails-5.0.3

- old
+ new

@@ -1,7 +1,7 @@ /** - * @license Highcharts JS v5.0.0 (2016-09-29) + * @license Highcharts JS v5.0.3 (2016-11-18) * * (c) 2009-2016 Torstein Honsi * * License: www.highcharts.com/license */ @@ -34,22 +34,23 @@ isFirefox = /Firefox/.test(userAgent), hasBidiBug = isFirefox && parseInt(userAgent.split('Firefox/')[1], 10) < 4; // issue #38 var Highcharts = win.Highcharts ? win.Highcharts.error(16, true) : { product: 'Highcharts', - version: '5.0.0', + version: '5.0.3', deg2rad: Math.PI * 2 / 360, doc: doc, hasBidiBug: hasBidiBug, + hasTouch: doc && doc.documentElement.ontouchstart !== undefined, isMS: isMS, isWebKit: /AppleWebKit/.test(userAgent), isFirefox: isFirefox, isTouchDevice: /(Mobile|Android|Windows Phone)/.test(userAgent), SVG_NS: SVG_NS, - idCounter: 0, chartCount: 0, seriesTypes: {}, + symbolSizes: {}, svg: svg, vml: vml, win: win, charts: [], marginNames: ['plotTop', 'marginRight', 'marginBottom', 'plotLeft'], @@ -63,59 +64,82 @@ /** * (c) 2010-2016 Torstein Honsi * * License: www.highcharts.com/license */ + /* eslint max-len: ["warn", 80, 4] */ 'use strict'; + + /** + * The Highcharts object is the placeholder for all other members, and various + * utility functions. + * @namespace Highcharts + */ + var timers = []; var charts = H.charts, doc = H.doc, win = H.win; /** - * Provide error messages for debugging, with links to online explanation + * Provide error messages for debugging, with links to online explanation. This + * function can be overridden to provide custom error handling. + * + * @function #error + * @memberOf Highcharts + * @param {Number} code - The error code. See [errors.xml]{@link + * https://github.com/highcharts/highcharts/blob/master/errors/errors.xml} + * for available codes. + * @param {Boolean} [stop=false] - Whether to throw an error or just log a + * warning in the console. */ H.error = function(code, stop) { - var msg = 'Highcharts error #' + code + ': www.highcharts.com/errors/' + code; + var msg = 'Highcharts error #' + code + ': www.highcharts.com/errors/' + + code; if (stop) { throw new Error(msg); } // else ... if (win.console) { console.log(msg); // eslint-disable-line no-console } }; /** - * An animator object. One instance applies to one property (attribute or style prop) - * on one element. - * - * @param {object} elem The element to animate. May be a DOM element or a Highcharts SVGElement wrapper. - * @param {object} options Animation options, including duration, easing, step and complete. - * @param {object} prop The property to animate. + * An animator object. One instance applies to one property (attribute or style + * prop) on one element. + * + * @constructor Fx + * @memberOf Highcharts + * @param {HTMLDOMElement|SVGElement} elem - The element to animate. + * @param {AnimationOptions} options - Animation options. + * @param {string} prop - The single attribute or CSS property to animate. */ H.Fx = function(elem, options, prop) { this.options = options; this.elem = elem; this.prop = prop; }; H.Fx.prototype = { /** - * Animating a path definition on SVGElement - * @returns {undefined} + * Set the current step of a path definition on SVGElement. + * + * @function #dSetter + * @memberOf Highcharts.Fx */ dSetter: function() { var start = this.paths[0], end = this.paths[1], ret = [], now = this.now, i = start.length, startVal; - if (now === 1) { // land on the final path without adjustment points appended in the ends + // Land on the final path without adjustment points appended in the ends + if (now === 1) { ret = this.toD; } else if (i === end.length && now < 1) { while (i--) { startVal = parseFloat(start[i]); @@ -123,19 +147,22 @@ isNaN(startVal) ? // a letter instruction like M or L start[i] : now * (parseFloat(end[i] - startVal)) + startVal; } - } else { // if animation is finished or length not matching, land on right value + // If animation is finished or length not matching, land on right value + } else { ret = end; } this.elem.attr('d', ret); }, /** - * Update the element with the current animation step - * @returns {undefined} + * Update the element with the current animation step. + * + * @function #update + * @memberOf Highcharts.Fx */ update: function() { var elem = this.elem, prop = this.prop, // if destroyed, it is null now = this.now, @@ -161,11 +188,18 @@ } }, /** - * Run an animation + * Run an animation. + * + * @function #run + * @memberOf Highcharts.Fx + * @param {Number} from - The current value, value to start from. + * @param {Number} to - The end value, value to land on. + * @param {String} [unit] - The property unit, for example `px`. + * @returns {void} */ run: function(from, to, unit) { var self = this, timer = function(gotoEnd) { return timer.stopped ? false : self.step(gotoEnd); @@ -196,13 +230,17 @@ }, 13); } }, /** - * Run a single step in the animation - * @param {Boolean} gotoEnd Whether to go to then endpoint of the animation after abort - * @returns {Boolean} True if animation continues + * Run a single step in the animation. + * + * @function #step + * @memberOf Highcharts.Fx + * @param {Boolean} [gotoEnd] - Whether to go to the endpoint of the + * animation after abort. + * @returns {Boolean} Returns `true` if animation continues. */ step: function(gotoEnd) { var t = +new Date(), ret, done, @@ -211,11 +249,11 @@ complete = options.complete, duration = options.duration, curAnim = options.curAnim, i; - if (elem.attr && !elem.element) { // #2616, element including flag is destroyed + if (elem.attr && !elem.element) { // #2616, element is destroyed ret = false; } else if (gotoEnd || t >= duration + this.startTime) { this.now = this.end; this.pos = 1; @@ -243,11 +281,19 @@ } return ret; }, /** - * Prepare start and end values so that the path can be animated one to one + * Prepare start and end values so that the path can be animated one to one. + * + * @function #initPath + * @memberOf Highcharts.Fx + * @param {SVGElement} elem - The SVGElement item. + * @param {String} fromD - Starting path definition. + * @param {Array} toD - Ending path definition. + * @returns {Array} An array containing start and end paths in array form + * so that they can be animated in parallel. */ initPath: function(elem, fromD, toD) { fromD = fromD || ''; var shift, startX = elem.startX, @@ -268,11 +314,15 @@ */ function sixify(arr) { i = arr.length; while (i--) { if (arr[i] === 'M' || arr[i] === 'L') { - arr.splice(i + 1, 0, arr[i + 1], arr[i + 2], arr[i + 1], arr[i + 2]); + arr.splice( + i + 1, 0, + arr[i + 1], arr[i + 2], + arr[i + 1], arr[i + 2] + ); } } } /** @@ -294,14 +344,17 @@ arr[0] = other[fullLength - arr.length]; // Prepend a copy of the first point insertSlice(arr, arr.slice(0, numParams), 0); - // For areas, the bottom path goes back again to the left, so we need - // to append a copy of the last point. + // For areas, the bottom path goes back again to the left, so we + // need to append a copy of the last point. if (isArea) { - insertSlice(arr, arr.slice(arr.length - numParams), arr.length); + insertSlice( + arr, + arr.slice(arr.length - numParams), arr.length + ); i--; } } arr[0] = 'M'; } @@ -311,15 +364,16 @@ */ function append(arr, other) { var i = (fullLength - arr.length) / numParams; while (i > 0 && i--) { - // Pull out the slice that is going to be appended or inserted. In a line graph, - // the positionFactor is 1, and the last point is sliced out. In an area graph, - // the positionFactor is 2, causing the middle two points to be sliced out, since - // an area path starts at left, follows the upper path then turns and follows the - // bottom back. + // Pull out the slice that is going to be appended or inserted. + // In a line graph, the positionFactor is 1, and the last point + // is sliced out. In an area graph, the positionFactor is 2, + // causing the middle two points to be sliced out, since an area + // path starts at left, follows the upper path then turns and + // follows the bottom back. slice = arr.slice().splice( (arr.length / positionFactor) - numParams, numParams * positionFactor ); @@ -330,11 +384,12 @@ if (bezier) { slice[numParams - 6] = slice[numParams - 2]; slice[numParams - 5] = slice[numParams - 1]; } - // Now insert the slice, either in the middle (for areas) or at the end (for lines) + // Now insert the slice, either in the middle (for areas) or at + // the end (for lines) insertSlice(arr, slice, arr.length / positionFactor); if (isArea) { i--; } @@ -344,18 +399,21 @@ if (bezier) { sixify(start); sixify(end); } - // For sideways animation, find out how much we need to shift to get the start path Xs - // to match the end path Xs. + // For sideways animation, find out how much we need to shift to get the + // start path Xs to match the end path Xs. if (startX && endX) { for (i = 0; i < startX.length; i++) { - if (startX[i] === endX[0]) { // Moving left, new points coming in on right + // Moving left, new points coming in on right + if (startX[i] === endX[0]) { shift = i; break; - } else if (startX[0] === endX[endX.length - startX.length + i]) { // Moving right + // Moving right + } else if (startX[0] === + endX[endX.length - startX.length + i]) { shift = i; reverse = true; break; } } @@ -383,13 +441,17 @@ } }; // End of Fx prototype /** - * Extend an object with the members of another - * @param {Object} a The object to be extended - * @param {Object} b The object to add to the first one + * Utility function to extend an object with the members of another. + * + * @function #extend + * @memberOf Highcharts + * @param {Object} a - The object to be extended. + * @param {Object} b - The object to add to the first one. + * @returns {Object} Object a, the original object. */ H.extend = function(a, b) { var n; if (!a) { a = {}; @@ -399,15 +461,24 @@ } return a; }; /** - * Deep merge two or more objects and return a third object. If the first argument is - * true, the contents of the second object is copied into the first object. - * Previously this function redirected to jQuery.extend(true), but this had two limitations. - * First, it deep merged arrays, which lead to workarounds in Highcharts. Second, - * it copied properties from extended prototypes. + * Utility function to deep merge two or more objects and return a third object. + * If the first argument is true, the contents of the second object is copied + * into the first object. The merge function can also be used with a single + * object argument to create a deep copy of an object. + * + * @function #merge + * @memberOf Highcharts + * @param {Boolean} [extend] - Whether to extend the left-side object (a) or + return a whole new object. + * @param {Object} a - The first object to extend. When only this is given, the + function returns a deep copy. + * @param {...Object} [n] - An object to merge into the previous one. + * @returns {Object} - The merged object. If the first argument is true, the + * return is the same as the second argument. */ H.merge = function() { var i, args = arguments, len, @@ -424,11 +495,12 @@ if (original.hasOwnProperty(key)) { value = original[key]; // Copy the contents of objects, but not arrays or DOM nodes if (H.isObject(value, true) && - key !== 'renderTo' && typeof value.nodeType !== 'number') { + key !== 'renderTo' && + typeof value.nodeType !== 'number') { copy[key] = doCopy(copy[key] || {}, value); // Primitives and arrays are copied over directly } else { copy[key] = original[key]; @@ -436,11 +508,12 @@ } } return copy; }; - // If first argument is true, copy into the existing object. Used in setOptions. + // If first argument is true, copy into the existing object. Used in + // setOptions. if (args[0] === true) { ret = args[1]; args = Array.prototype.slice.call(args, 2); } @@ -453,82 +526,111 @@ return ret; }; /** * Shortcut for parseInt + * @ignore * @param {Object} s * @param {Number} mag Magnitude */ H.pInt = function(s, mag) { return parseInt(s, mag || 10); }; /** - * Check for string - * @param {Object} s + * Utility function to check for string type. + * + * @function #isString + * @memberOf Highcharts + * @param {Object} s - The item to check. + * @returns {Boolean} - True if the argument is a string. */ H.isString = function(s) { return typeof s === 'string'; }; /** - * Check for object - * @param {Object} obj - * @param {Boolean} strict Also checks that the object is not an array + * Utility function to check if an item is an array. + * + * @function #isArray + * @memberOf Highcharts + * @param {Object} obj - The item to check. + * @returns {Boolean} - True if the argument is an array. */ H.isArray = function(obj) { var str = Object.prototype.toString.call(obj); return str === '[object Array]' || str === '[object Array Iterator]'; }; /** - * Check for array - * @param {Object} obj + * Utility function to check if an item is of type object. + * + * @function #isObject + * @memberOf Highcharts + * @param {Object} obj - The item to check. + * @param {Boolean} [strict=false] - Also checks that the object is not an + * array. + * @returns {Boolean} - True if the argument is an object. */ H.isObject = function(obj, strict) { return obj && typeof obj === 'object' && (!strict || !H.isArray(obj)); }; /** - * Check for number - * @param {Object} n + * Utility function to check if an item is of type number. + * + * @function #isNumber + * @memberOf Highcharts + * @param {Object} n - The item to check. + * @returns {Boolean} - True if the item is a number and is not NaN. */ H.isNumber = function(n) { return typeof n === 'number' && !isNaN(n); }; /** - * Remove last occurence of an item from an array - * @param {Array} arr - * @param {Mixed} item + * Remove the last occurence of an item from an array. + * + * @function #erase + * @memberOf Highcharts + * @param {Array} arr - The array. + * @param {*} item - The item to remove. */ H.erase = function(arr, item) { var i = arr.length; while (i--) { if (arr[i] === item) { arr.splice(i, 1); break; } } - //return arr; }; /** - * Returns true if the object is not null or undefined. - * @param {Object} obj + * Check if an object is null or undefined. + * + * @function #defined + * @memberOf Highcharts + * @param {Object} obj - The object to check. + * @returns {Boolean} - False if the object is null or undefined, otherwise + * true. */ H.defined = function(obj) { return obj !== undefined && obj !== null; }; /** - * Set or get an attribute or an object of attributes. Can't use jQuery attr because - * it attempts to set expando properties on the SVG element, which is not allowed. + * Set or get an attribute or an object of attributes. To use as a setter, pass + * a key and a value, or let the second argument be a collection of keys and + * values. To use as a getter, pass only a string as the second argument. * - * @param {Object} elem The DOM element to receive the attribute(s) - * @param {String|Object} prop The property or an abject of key-value pairs - * @param {String} value The value if a single property is set + * @function #attr + * @memberOf Highcharts + * @param {Object} elem - The DOM element to receive the attribute(s). + * @param {String|Object} [prop] - The property or an object of key-value pairs. + * @param {String} [value] - The value if a single property is set. + * @returns {*} When used as a getter, return the value. */ H.attr = function(elem, prop, value) { var key, ret; @@ -537,11 +639,11 @@ // set the value if (H.defined(value)) { elem.setAttribute(prop, value); // get the value - } else if (elem && elem.getAttribute) { // elem not defined when printing pie demo... + } else if (elem && elem.getAttribute) { ret = elem.getAttribute(prop); } // else if prop is defined, it is a hash of key/value pairs } else if (H.defined(prop) && H.isObject(prop)) { @@ -549,34 +651,50 @@ elem.setAttribute(key, prop[key]); } } return ret; }; + /** * Check if an element is an array, and if not, make it into an array. + * + * @function #splat + * @memberOf Highcharts + * @param obj {*} - The object to splat. + * @returns {Array} The produced or original array. */ H.splat = function(obj) { return H.isArray(obj) ? obj : [obj]; }; /** - * Set a timeout if the delay is given, otherwise perform the function synchronously - * @param {Function} fn The function to perform - * @param {Number} delay Delay in milliseconds - * @param {Ojbect} context The context - * @returns {Nubmer} An identifier for the timeout + * Set a timeout if the delay is given, otherwise perform the function + * synchronously. + * + * @function #syncTimeout + * @memberOf Highcharts + * @param {Function} fn - The function callback. + * @param {Number} delay - Delay in milliseconds. + * @param {Object} [context] - The context. + * @returns {Number} An identifier for the timeout that can later be cleared + * with clearTimeout. */ H.syncTimeout = function(fn, delay, context) { if (delay) { return setTimeout(fn, delay, context); } fn.call(0, context); }; /** - * Return the first value that is defined. + * Return the first value that is not null or undefined. + * + * @function #pick + * @memberOf Highcharts + * @param {...*} items - Variable number of arguments to inspect. + * @returns {*} The value of the first argument that is not null or undefined. */ H.pick = function() { var args = arguments, i, arg, @@ -588,31 +706,55 @@ } } }; /** - * Set CSS on a given element - * @param {Object} el - * @param {Object} styles Style object with camel case property names + * @typedef {Object} CSSObject - A style object with camel case property names. + * The properties can be whatever styles are supported on the given SVG or HTML + * element. + * @example + * { + * fontFamily: 'monospace', + * fontSize: '1.2em' + * } */ + /** + * Set CSS on a given element. + * + * @function #css + * @memberOf Highcharts + * @param {HTMLDOMElement} el - A HTML DOM element. + * @param {CSSObject} styles - Style object with camel case property names. + * @returns {void} + */ H.css = function(el, styles) { if (H.isMS && !H.svg) { // #2686 if (styles && styles.opacity !== undefined) { styles.filter = 'alpha(opacity=' + (styles.opacity * 100) + ')'; } } H.extend(el.style, styles); }; /** - * Utility function to create element with attributes and styles - * @param {Object} tag - * @param {Object} attribs - * @param {Object} styles - * @param {Object} parent - * @param {Object} nopad + * A HTML DOM element. + * @typedef {Object} HTMLDOMElement */ + + /** + * Utility function to create an HTML element with attributes and styles. + * + * @function #createElement + * @memberOf Highcharts + * @param {String} tag - The HTML tag. + * @param {Object} [attribs] - Attributes as an object of key-value pairs. + * @param {CSSObject} [styles] - Styles as an object of key-value pairs. + * @param {Object} [parent] - The parent HTML object. + * @param {Boolean} [nopad=false] - If true, remove all padding, border and + * margin. + * @returns {HTMLDOMElement} The created DOM element. + */ H.createElement = function(tag, attribs, styles, parent, nopad) { var el = doc.createElement(tag), css = H.css; if (attribs) { H.extend(el, attribs); @@ -632,65 +774,118 @@ } return el; }; /** - * Extend a prototyped class by new members - * @param {Object} parent - * @param {Object} members + * Extend a prototyped class by new members. + * + * @function #extendClass + * @memberOf Highcharts + * @param {Object} parent - The parent prototype to inherit. + * @param {Object} members - A collection of prototype members to add or + * override compared to the parent prototype. + * @returns {Object} A new prototype. */ - H.extendClass = function(Parent, members) { + H.extendClass = function(parent, members) { var object = function() {}; - object.prototype = new Parent(); + object.prototype = new parent(); // eslint-disable-line new-cap H.extend(object.prototype, members); return object; }; /** - * Pad a string to a given length by adding 0 to the beginning - * @param {Number} number - * @param {Number} length + * Left-pad a string to a given length by adding a character repetetively. + * + * @function #pad + * @memberOf Highcharts + * @param {Number} number - The input string or number. + * @param {Number} length - The desired string length. + * @param {String} [padder=0] - The character to pad with. + * @returns {String} The padded string. */ H.pad = function(number, length, padder) { - return new Array((length || 2) + 1 - String(number).length).join(padder || 0) + number; + return new Array((length || 2) + 1 - + String(number).length).join(padder || 0) + number; }; /** + * @typedef {Number|String} RelativeSize - If a number is given, it defines the + * pixel length. If a percentage string is given, like for example `'50%'`, + * the setting defines a length relative to a base size, for example the size + * of a container. + */ + /** * Return a length based on either the integer value, or a percentage of a base. + * + * @function #relativeLength + * @memberOf Highcharts + * @param {RelativeSize} value - A percentage string or a number. + * @param {Number} base - The full length that represents 100%. + * @returns {Number} The computed length. */ H.relativeLength = function(value, base) { - return (/%$/).test(value) ? base * parseFloat(value) / 100 : parseFloat(value); + return (/%$/).test(value) ? + base * parseFloat(value) / 100 : + parseFloat(value); }; /** - * Wrap a method with extended functionality, preserving the original function - * @param {Object} obj The context object that the method belongs to - * @param {String} method The name of the method to extend - * @param {Function} func A wrapper function callback. This function is called with the same arguments - * as the original function, except that the original function is unshifted and passed as the first - * argument. + * Wrap a method with extended functionality, preserving the original function. + * + * @function #wrap + * @memberOf Highcharts + * @param {Object} obj - The context object that the method belongs to. In real + * cases, this is often a prototype. + * @param {String} method - The name of the method to extend. + * @param {Function} func - A wrapper function callback. This function is called + * with the same arguments as the original function, except that the + * original function is unshifted and passed as the first argument. + * @returns {void} */ H.wrap = function(obj, method, func) { var proceed = obj[method]; obj[method] = function() { - var args = Array.prototype.slice.call(arguments); + var args = Array.prototype.slice.call(arguments), + outerArgs = arguments, + ctx = this, + ret; + ctx.proceed = function() { + proceed.apply(ctx, arguments.length ? arguments : outerArgs); + }; args.unshift(proceed); - return func.apply(this, args); + ret = func.apply(this, args); + ctx.proceed = null; + return ret; }; }; - + /** + * Get the time zone offset based on the current timezone information as set in + * the global options. + * + * @function #getTZOffset + * @memberOf Highcharts + * @param {Number} timestamp - The JavaScript timestamp to inspect. + * @return {Number} - The timezone offset in minutes compared to UTC. + */ H.getTZOffset = function(timestamp) { var d = H.Date; - return ((d.hcGetTimezoneOffset && d.hcGetTimezoneOffset(timestamp)) || d.hcTimezoneOffset || 0) * 60000; + return ((d.hcGetTimezoneOffset && d.hcGetTimezoneOffset(timestamp)) || + d.hcTimezoneOffset || 0) * 60000; }; /** - * Based on http://www.php.net/manual/en/function.strftime.php - * @param {String} format - * @param {Number} timestamp - * @param {Boolean} capitalize + * Format a date, based on the syntax for PHP's [strftime]{@link + * http://www.php.net/manual/en/function.strftime.php} function. + * + * @function #dateFormat + * @memberOf Highcharts + * @param {String} format - The desired format where various time + * representations are prefixed with %. + * @param {Number} timestamp - The JavaScript timestamp. + * @param {Boolean} [capitalize=false] - Upper case first letter in the return. + * @returns {String} The formatted date. */ H.dateFormat = function(format, timestamp, capitalize) { if (!H.defined(timestamp) || isNaN(timestamp)) { return H.defaultOptions.lang.invalidDate || ''; } @@ -711,55 +906,91 @@ pad = H.pad, // List all format keys. Custom formats can be added from the outside. replacements = H.extend({ - // Day - 'a': shortWeekdays ? shortWeekdays[day] : langWeekdays[day].substr(0, 3), // Short weekday, like 'Mon' - 'A': langWeekdays[day], // Long weekday, like 'Monday' - 'd': pad(dayOfMonth), // Two digit day of the month, 01 to 31 - 'e': pad(dayOfMonth, 2, ' '), // Day of the month, 1 through 31 + //-- Day + // Short weekday, like 'Mon' + 'a': shortWeekdays ? + shortWeekdays[day] : langWeekdays[day].substr(0, 3), + // Long weekday, like 'Monday' + 'A': langWeekdays[day], + // Two digit day of the month, 01 to 31 + 'd': pad(dayOfMonth), + // Day of the month, 1 through 31 + 'e': pad(dayOfMonth, 2, ' '), 'w': day, // Week (none implemented) //'W': weekNumber(), - // Month - 'b': lang.shortMonths[month], // Short month, like 'Jan' - 'B': lang.months[month], // Long month, like 'January' - 'm': pad(month + 1), // Two digit month number, 01 through 12 + //-- Month + // Short month, like 'Jan' + 'b': lang.shortMonths[month], + // Long month, like 'January' + 'B': lang.months[month], + // Two digit month number, 01 through 12 + 'm': pad(month + 1), - // Year - 'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009 - 'Y': fullYear, // Four digits year, like 2009 + //-- Year + // Two digits year, like 09 for 2009 + 'y': fullYear.toString().substr(2, 2), + // Four digits year, like 2009 + 'Y': fullYear, - // Time - 'H': pad(hours), // Two digits hours in 24h format, 00 through 23 - 'k': hours, // Hours in 24h format, 0 through 23 - 'I': pad((hours % 12) || 12), // Two digits hours in 12h format, 00 through 11 - 'l': (hours % 12) || 12, // Hours in 12h format, 1 through 12 - 'M': pad(date[D.hcGetMinutes]()), // Two digits minutes, 00 through 59 - 'p': hours < 12 ? 'AM' : 'PM', // Upper case AM or PM - 'P': hours < 12 ? 'am' : 'pm', // Lower case AM or PM - 'S': pad(date.getSeconds()), // Two digits seconds, 00 through 59 - 'L': pad(Math.round(timestamp % 1000), 3) // Milliseconds (naming from Ruby) + //-- Time + // Two digits hours in 24h format, 00 through 23 + 'H': pad(hours), + // Hours in 24h format, 0 through 23 + 'k': hours, + // Two digits hours in 12h format, 00 through 11 + 'I': pad((hours % 12) || 12), + // Hours in 12h format, 1 through 12 + 'l': (hours % 12) || 12, + // Two digits minutes, 00 through 59 + 'M': pad(date[D.hcGetMinutes]()), + // Upper case AM or PM + 'p': hours < 12 ? 'AM' : 'PM', + // Lower case AM or PM + 'P': hours < 12 ? 'am' : 'pm', + // Two digits seconds, 00 through 59 + 'S': pad(date.getSeconds()), + // Milliseconds (naming from Ruby) + 'L': pad(Math.round(timestamp % 1000), 3) }, H.dateFormats); - // do the replaces + // Do the replaces for (key in replacements) { - while (format.indexOf('%' + key) !== -1) { // regex would do it in one line, but this is faster - format = format.replace('%' + key, typeof replacements[key] === 'function' ? replacements[key](timestamp) : replacements[key]); + // Regex would do it in one line, but this is faster + while (format.indexOf('%' + key) !== -1) { + format = format.replace( + '%' + key, + typeof replacements[key] === 'function' ? + replacements[key](timestamp) : + replacements[key] + ); } } // Optionally capitalize the string and return - return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format; + return capitalize ? + format.substr(0, 1).toUpperCase() + format.substr(1) : + format; }; /** * Format a single variable. Similar to sprintf, without the % prefix. + * + * @example + * formatSingle('.2f', 5); // => '5.00'. + * + * @function #formatSingle + * @memberOf Highcharts + * @param {String} format The format string. + * @param {*} val The value. + * @returns {String} The formatted representation of the value. */ H.formatSingle = function(format, val) { var floatRegex = /f$/, decRegex = /\.([0-9])/, lang = H.defaultOptions.lang, @@ -781,11 +1012,26 @@ } return val; }; /** - * Format a string according to a subset of the rules of Python's String.format method. + * Format a string according to a subset of the rules of Python's String.format + * method. + * + * @function #format + * @memberOf Highcharts + * @param {String} str The string to format. + * @param {Object} ctx The context, a collection of key-value pairs where each + * key is replaced by its value. + * @returns {String} The formatted string. + * + * @example + * var s = Highcharts.format( + * 'The {color} fox was {len:.2f} feet long', + * { color: 'red', len: Math.PI } + * ); + * // => The red fox was 3.14 feet long */ H.format = function(str, ctx) { var splitter = '{', isInside = false, segment, @@ -805,11 +1051,11 @@ segment = str.slice(0, index); if (isInside) { // we're on the closing bracket looking back valueAndFormat = segment.split(':'); - path = valueAndFormat.shift().split('.'); // get first and leave format + path = valueAndFormat.shift().split('.'); // get first and leave len = path.length; val = ctx; // Assign deeper paths for (i = 0; i < len; i++) { @@ -835,51 +1081,77 @@ ret.push(str); return ret.join(''); }; /** - * Get the magnitude of a number + * Get the magnitude of a number. + * + * @function #getMagnitude + * @memberOf Highcharts + * @param {Number} number The number. + * @returns {Number} The magnitude, where 1-9 are magnitude 1, 10-99 magnitude 2 + * etc. */ H.getMagnitude = function(num) { return Math.pow(10, Math.floor(Math.log(num) / Math.LN10)); }; /** - * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5 - * @param {Number} interval - * @param {Array} multiples - * @param {Number} magnitude - * @param {Object} options + * Take an interval and normalize it to multiples of round numbers. + * + * @todo Move this function to the Axis prototype. It is here only for + * historical reasons. + * @function #normalizeTickInterval + * @memberOf Highcharts + * @param {Number} interval - The raw, un-rounded interval. + * @param {Array} [multiples] - Allowed multiples. + * @param {Number} [magnitude] - The magnitude of the number. + * @param {Boolean} [allowDecimals] - Whether to allow decimals. + * @param {Boolean} [hasTickAmount] - If it has tickAmount, avoid landing + * on tick intervals lower than original. + * @returns {Number} The normalized interval. */ - H.normalizeTickInterval = function(interval, multiples, magnitude, allowDecimals, preventExceed) { + H.normalizeTickInterval = function(interval, multiples, magnitude, + allowDecimals, hasTickAmount) { var normalized, i, retInterval = interval; // round to a tenfold of 1, 2, 2.5 or 5 magnitude = H.pick(magnitude, 1); normalized = interval / magnitude; // multiples for a linear scale if (!multiples) { - multiples = [1, 2, 2.5, 5, 10]; + multiples = hasTickAmount ? + // Finer grained ticks when the tick amount is hard set, including + // when alignTicks is true on multiple axes (#4580). + [1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10] : + // Else, let ticks fall on rounder numbers + [1, 2, 2.5, 5, 10]; + + // the allowDecimals option if (allowDecimals === false) { if (magnitude === 1) { - multiples = [1, 2, 5, 10]; + multiples = H.grep(multiples, function(num) { + return num % 1 === 0; + }); } else if (magnitude <= 0.1) { multiples = [1 / magnitude]; } } } // normalize the interval to the nearest multiple for (i = 0; i < multiples.length; i++) { retInterval = multiples[i]; - if ((preventExceed && retInterval * magnitude >= interval) || // only allow tick amounts smaller than natural - (!preventExceed && (normalized <= (multiples[i] + (multiples[i + 1] || multiples[i])) / 2))) { + // only allow tick amounts smaller than natural + if ((hasTickAmount && retInterval * magnitude >= interval) || + (!hasTickAmount && (normalized <= (multiples[i] + + (multiples[i + 1] || multiples[i])) / 2))) { break; } } // multiply back to the correct magnitude @@ -888,12 +1160,19 @@ return retInterval; }; /** - * Utility method that sorts an object array and keeping the order of equal items. - * ECMA script standard does not specify the behaviour when items are equal. + * Sort an object array and keep the order of equal items. The ECMAScript + * standard does not specify the behaviour when items are equal. + * + * @function #stableSort + * @memberOf Highcharts + * @param {Array} arr - The array to sort. + * @param {Function} sortFunction - The function to sort it with, like with + * regular Array.prototype.sort. + * @returns {void} */ H.stableSort = function(arr, sortFunction) { var length = arr.length, sortValue, i; @@ -913,13 +1192,18 @@ delete arr[i].safeI; // stable sort index } }; /** - * Non-recursive method to find the lowest member of an array. Math.min raises a maximum - * call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This - * method is slightly slower, but safe. + * Non-recursive method to find the lowest member of an array. `Math.min` raises + * a maximum call stack size exceeded error in Chrome when trying to apply more + * than 150.000 points. This method is slightly slower, but safe. + * + * @function #arrayMin + * @memberOf Highcharts + * @param {Array} data An array of numbers. + * @returns {Number} The lowest number. */ H.arrayMin = function(data) { var i = data.length, min = data[0]; @@ -930,13 +1214,18 @@ } return min; }; /** - * Non-recursive method to find the lowest member of an array. Math.min raises a maximum - * call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This - * method is slightly slower, but safe. + * Non-recursive method to find the lowest member of an array. `Math.max` raises + * a maximum call stack size exceeded error in Chrome when trying to apply more + * than 150.000 points. This method is slightly slower, but safe. + * + * @function #arrayMax + * @memberOf Highcharts + * @param {Array} data - An array of numbers. + * @returns {Number} The highest number. */ H.arrayMax = function(data) { var i = data.length, max = data[0]; @@ -947,15 +1236,20 @@ } return max; }; /** - * Utility method that destroys any SVGElement or VMLElement that are properties on the given object. - * It loops all properties and invokes destroy if there is a destroy method. The property is - * then delete'ed. - * @param {Object} The object to destroy properties on - * @param {Object} Exception, do not destroy this property, only delete it. + * Utility method that destroys any SVGElement instances that are properties on + * the given object. It loops all properties and invokes destroy if there is a + * destroy method. The property is then delete. + * + * @function #destroyObjectProperties + * @memberOf Highcharts + * @param {Object} obj - The object to destroy properties on. + * @param {Object} [except] - Exception, do not destroy this property, only + * delete it. + * @returns {void} */ H.destroyObjectProperties = function(obj, except) { var n; for (n in obj) { // If the object is non-null and destroy is defined @@ -969,12 +1263,16 @@ } }; /** - * Discard an element by moving it to the bin and delete - * @param {Object} The HTML node to discard + * Discard a HTML element by moving it to the bin and delete. + * + * @function #discardElement + * @memberOf Highcharts + * @param {HTMLDOMElement} element - The HTML node to discard. + * @returns {void} */ H.discardElement = function(element) { var garbageBin = H.garbageBin; // create a garbage bin element, not part of the DOM if (!garbageBin) { @@ -987,37 +1285,60 @@ } garbageBin.innerHTML = ''; }; /** - * Fix JS round off float errors - * @param {Number} num + * Fix JS round off float errors. + * + * @function #correctFloat + * @memberOf Highcharts + * @param {Number} num - A float number to fix. + * @param {Number} [prec=14] - The precision. + * @returns {Number} The corrected float number. */ H.correctFloat = function(num, prec) { return parseFloat( num.toPrecision(prec || 14) ); }; /** - * Set the global animation to either a given value, or fall back to the - * given chart's animation option - * @param {Object} animation - * @param {Object} chart + * Set the global animation to either a given value, or fall back to the given + * chart's animation option. + * + * @function #setAnimation + * @memberOf Highcharts + * @param {Boolean|Animation} animation - The animation object. + * @param {Object} chart - The chart instance. + * @returns {void} + * @todo This function always relates to a chart, and sets a property on the + * renderer, so it should be moved to the SVGRenderer. */ H.setAnimation = function(animation, chart) { - chart.renderer.globalAnimation = H.pick(animation, chart.options.chart.animation, true); + chart.renderer.globalAnimation = H.pick( + animation, + chart.options.chart.animation, + true + ); }; /** * Get the animation in object form, where a disabled animation is always - * returned with duration: 0 + * returned as `{ duration: 0 }`. + * + * @function #animObject + * @memberOf Highcharts + * @param {Boolean|AnimationOptions} animation - An animation setting. Can be an + * object with duration, complete and easing properties, or a boolean to + * enable or disable. + * @returns {AnimationOptions} An object with at least a duration property. */ H.animObject = function(animation) { - return H.isObject(animation) ? H.merge(animation) : { - duration: animation ? 500 : 0 - }; + return H.isObject(animation) ? + H.merge(animation) : { + duration: animation ? 500 : 0 + }; }; /** * The time unit lookup */ @@ -1031,15 +1352,21 @@ month: 28 * 24 * 3600000, year: 364 * 24 * 3600000 }; /** - * Format a number and return a string based on input settings - * @param {Number} number The input number to format - * @param {Number} decimals The amount of decimals - * @param {String} decimalPoint The decimal point, defaults to the one given in the lang options - * @param {String} thousandsSep The thousands separator, defaults to the one given in the lang options + * Format a number and return a string based on input settings. + * + * @function #numberFormat + * @memberOf Highcharts + * @param {Number} number - The input number to format. + * @param {Number} decimals - The amount of decimals. + * @param {String} [decimalPoint] - The decimal point, defaults to the one given + * in the lang options. + * @param {String} [thousandsSep] - The thousands separator, defaults to the one + * given in the lang options. + * @returns {String} The formatted number. */ H.numberFormat = function(number, decimals, decimalPoint, thousandsSep) { number = +number || 0; decimals = +decimals; @@ -1051,11 +1378,12 @@ thousands, absNumber = Math.abs(number), ret; if (decimals === -1) { - decimals = Math.min(origDec, 20); // Preserve decimals. Not huge numbers (#3793). + // Preserve decimals. Not huge numbers (#3793). + decimals = Math.min(origDec, 20); } else if (!H.isNumber(decimals)) { decimals = 2; } // A string containing the positive integer component of the number @@ -1069,70 +1397,108 @@ thousandsSep = H.pick(thousandsSep, lang.thousandsSep); // Start building the return ret = number < 0 ? '-' : ''; - // Add the leftover after grouping into thousands. For example, in the number 42 000 000, - // this line adds 42. + // Add the leftover after grouping into thousands. For example, in the + // number 42 000 000, this line adds 42. ret += thousands ? strinteger.substr(0, thousands) + thousandsSep : ''; // Add the remaining thousands groups, joined by the thousands separator - ret += strinteger.substr(thousands).replace(/(\d{3})(?=\d)/g, '$1' + thousandsSep); + ret += strinteger + .substr(thousands) + .replace(/(\d{3})(?=\d)/g, '$1' + thousandsSep); // Add the decimal point and the decimal component if (decimals) { - // Get the decimal component, and add power to avoid rounding errors with float numbers (#4573) - decimalComponent = Math.abs(absNumber - strinteger + Math.pow(10, -Math.max(decimals, origDec) - 1)); + // Get the decimal component, and add power to avoid rounding errors + // with float numbers (#4573) + decimalComponent = Math.abs(absNumber - strinteger + + Math.pow(10, -Math.max(decimals, origDec) - 1)); ret += decimalPoint + decimalComponent.toFixed(decimals).slice(2); } return ret; }; /** * Easing definition - * @param {Number} pos Current position, ranging from 0 to 1 + * @ignore + * @param {Number} pos Current position, ranging from 0 to 1. */ Math.easeInOutSine = function(pos) { return -0.5 * (Math.cos(Math.PI * pos) - 1); }; /** - * Internal method to return CSS value for given element and property + * Get the computed CSS value for given element and property, only for numerical + * properties. For width and height, the dimension of the inner box (excluding + * padding) is returned. Used for fitting the chart within the container. + * + * @function #getStyle + * @memberOf Highcharts + * @param {HTMLDOMElement} el - A HTML element. + * @param {String} prop - The property name. + * @returns {Number} - The numeric value. */ H.getStyle = function(el, prop) { var style; // For width and height, return the actual inner pixel size (#4913) if (prop === 'width') { - return Math.min(el.offsetWidth, el.scrollWidth) - H.getStyle(el, 'padding-left') - H.getStyle(el, 'padding-right'); + return Math.min(el.offsetWidth, el.scrollWidth) - + H.getStyle(el, 'padding-left') - + H.getStyle(el, 'padding-right'); } else if (prop === 'height') { - return Math.min(el.offsetHeight, el.scrollHeight) - H.getStyle(el, 'padding-top') - H.getStyle(el, 'padding-bottom'); + return Math.min(el.offsetHeight, el.scrollHeight) - + H.getStyle(el, 'padding-top') - + H.getStyle(el, 'padding-bottom'); } // Otherwise, get the computed style style = win.getComputedStyle(el, undefined); return style && H.pInt(style.getPropertyValue(prop)); }; /** - * Return the index of an item in an array, or -1 if not found + * Search for an item in an array. + * + * @function #inArray + * @memberOf Highcharts + * @param {*} item - The item to search for. + * @param {arr} arr - The array or node collection to search in. + * @returns {Number} - The index within the array, or -1 if not found. */ H.inArray = function(item, arr) { return arr.indexOf ? arr.indexOf(item) : [].indexOf.call(arr, item); }; /** - * Filter an array + * Filter an array by a callback. + * + * @function #grep + * @memberOf Highcharts + * @param {Array} arr - The array to filter. + * @param {Function} callback - The callback function. The function receives the + * item as the first argument. Return `true` if the item is to be + * preserved. + * @returns {Array} - A new, filtered array. */ - H.grep = function(elements, callback) { - return [].filter.call(elements, callback); + H.grep = function(arr, callback) { + return [].filter.call(arr, callback); }; /** - * Map an array + * Map an array by a callback. + * + * @function #map + * @memberOf Highcharts + * @param {Array} arr - The array to map. + * @param {Function} fn - The callback function. Return the new value for the + * new array. + * @returns {Array} - A new array item with modified items. */ H.map = function(arr, fn) { var results = [], i = 0, len = arr.length; @@ -1143,29 +1509,43 @@ return results; }; /** - * Get the element's offset position, corrected by overflow:auto. + * Get the element's offset position, corrected for `overflow: auto`. + * + * @function #offset + * @memberOf Highcharts + * @param {HTMLDOMElement} el - The HTML element. + * @returns {Object} An object containing `left` and `top` properties for the + * position in the page. */ H.offset = function(el) { var docElem = doc.documentElement, box = el.getBoundingClientRect(); return { - top: box.top + (win.pageYOffset || docElem.scrollTop) - (docElem.clientTop || 0), - left: box.left + (win.pageXOffset || docElem.scrollLeft) - (docElem.clientLeft || 0) + top: box.top + (win.pageYOffset || docElem.scrollTop) - + (docElem.clientTop || 0), + left: box.left + (win.pageXOffset || docElem.scrollLeft) - + (docElem.clientLeft || 0) }; }; /** * Stop running animation. - * A possible extension to this would be to stop a single property, when + * + * @todo A possible extension to this would be to stop a single property, when * we want to continue animating others. Then assign the prop to the timer - * in the Fx.run method, and check for the prop here. This would be an improvement - * in all cases where we stop the animation from .attr. Instead of stopping - * everything, we can just stop the actual attributes we're setting. + * in the Fx.run method, and check for the prop here. This would be an + * improvement in all cases where we stop the animation from .attr. Instead of + * stopping everything, we can just stop the actual attributes we're setting. + * + * @function #stop + * @memberOf Highcharts + * @param {SVGElement} el - The SVGElement to stop animation on. + * @returns {void} */ H.stop = function(el) { var i = timers.length; @@ -1176,20 +1556,35 @@ } } }; /** - * Utility for iterating over an array. - * @param {Array} arr - * @param {Function} fn + * Iterate over an array. + * + * @function #each + * @memberOf Highcharts + * @param {Array} arr - The array to iterate over. + * @param {Function} fn - The iterator callback. It passes two arguments: + * * item - The array item. + * * index - The item's index in the array. + * @param {Object} [ctx] The context. */ H.each = function(arr, fn, ctx) { // modern browsers return Array.prototype.forEach.call(arr, fn, ctx); }; /** - * Add an event listener + * Add an event listener. + * + * @function #addEvent + * @memberOf Highcharts + * @param {Object} el - The element or object to add a listener to. It can be a + * {@link HTMLDOMElement}, an {@link SVGElement} or any other object. + * @param {String} type - The event type. + * @param {Function} fn - The function callback to execute when the event is + * fired. + * @returns {Function} A callback function to remove the added event. */ H.addEvent = function(el, type, fn) { var events = el.hcEvents = el.hcEvents || {}; @@ -1218,14 +1613,28 @@ if (!events[type]) { events[type] = []; } events[type].push(fn); + + // Return a function that can be called to remove this event. + return function() { + H.removeEvent(el, type, fn); + }; }; /** - * Remove event added with addEvent + * Remove an event that was added with {@link Highcharts#addEvent}. + * + * @function #removeEvent + * @memberOf Highcharts + * @param {Object} el - The element to remove events on. + * @param {String} [type] - The type of events to remove. If undefined, all + * events are removed from the element. + * @param {Function} [fn] - The specific callback to remove. If undefined, all + * events that match the element and optionally the type are removed. + * @returns {void} */ H.removeEvent = function(el, type, fn) { var events, hcEvents = el.hcEvents, @@ -1287,11 +1696,22 @@ } } }; /** - * Fire an event on a custom object + * Fire an event that was registered with {@link Highcharts#addEvent}. + * + * @function #fireEvent + * @memberOf Highcharts + * @param {Object} el - The object to fire the event on. It can be a + * {@link HTMLDOMElement}, an {@link SVGElement} or any other object. + * @param {String} type - The type of event. + * @param {Object} [eventArguments] - Custom event arguments that are passed on + * as an argument to the event handler. + * @param {Function} [defaultFunction] - The default function to execute if the + * other listeners haven't returned false. + * @returns {void} */ H.fireEvent = function(el, type, eventArguments, defaultFunction) { var e, hcEvents = el.hcEvents, events, @@ -1320,29 +1740,32 @@ len = events.length; if (!eventArguments.target) { // We're running a custom event H.extend(eventArguments, { - // Attach a simple preventDefault function to skip default handler if called. - // The built-in defaultPrevented property is not overwritable (#5112) + // Attach a simple preventDefault function to skip default + // handler if called. The built-in defaultPrevented property is + // not overwritable (#5112) preventDefault: function() { eventArguments.defaultPrevented = true; }, - // Setting target to native events fails with clicking the zoom-out button in Chrome. + // Setting target to native events fails with clicking the + // zoom-out button in Chrome. target: el, - // If the type is not set, we're running a custom event (#2297). If it is set, - // we're running a browser event, and setting it will cause en error in - // IE8 (#2465). + // If the type is not set, we're running a custom event (#2297). + // If it is set, we're running a browser event, and setting it + // will cause en error in IE8 (#2465). type: type }); } for (i = 0; i < len; i++) { fn = events[i]; - // If the event handler return false, prevent the default handler from executing + // If the event handler return false, prevent the default handler + // from executing if (fn && fn.call(el, eventArguments) === false) { eventArguments.preventDefault(); } } } @@ -1352,11 +1775,35 @@ defaultFunction(eventArguments); } }; /** + * An animation configuration. Animation configurations can also be defined as + * booleans, where `false` turns off animation and `true` defaults to a duration + * of 500ms. + * @typedef {Object} AnimationOptions + * @property {Number} duration - The animation duration in milliseconds. + * @property {String} [easing] - The name of an easing function as defined on + * the `Math` object. + * @property {Function} [complete] - A callback function to exectute when the + * animation finishes. + * @property {Function} [step] - A callback function to execute on each step of + * each attribute or CSS property that's being animated. The first argument + * contains information about the animation and progress. + */ + + + /** * The global animate method, which uses Fx to create individual animators. + * + * @function #animate + * @memberOf Highcharts + * @param {HTMLDOMElement|SVGElement} el - The element to animate. + * @param {Object} params - An object containing key-value pairs of the + * properties to animate. Supports numeric as pixel-based CSS properties + * for HTML objects and attributes for SVGElements. + * @param {AnimationOptions} [opt] - Animation options. */ H.animate = function(el, params, opt) { var start, unit = '', end, @@ -1373,11 +1820,13 @@ }; } if (!H.isNumber(opt.duration)) { opt.duration = 400; } - opt.easing = typeof opt.easing === 'function' ? opt.easing : (Math[opt.easing] || Math.easeInOutSine); + opt.easing = typeof opt.easing === 'function' ? + opt.easing : + (Math[opt.easing] || Math.easeInOutSine); opt.curAnim = H.merge(params); for (prop in params) { fx = new H.Fx(el, opt, prop); end = null; @@ -1409,41 +1858,73 @@ fx.run(start, end, unit); } }; /** - * The series type factory. + * Factory to create new series prototypes. * - * @param {string} type The series type name. - * @param {string} parent The parent series type name. - * @param {object} options The additional default options that is merged with the parent's options. - * @param {object} props The properties (functions and primitives) to set on the new prototype. - * @param {object} pointProps Members for a series-specific Point prototype if needed. + * @function #seriesType + * @memberOf Highcharts + * + * @param {String} type - The series type name. + * @param {String} parent - The parent series type name. Use `line` to inherit + * from the basic {@link Series} object. + * @param {Object} options - The additional default options that is merged with + * the parent's options. + * @param {Object} props - The properties (functions and primitives) to set on + * the new prototype. + * @param {Object} [pointProps] - Members for a series-specific extension of the + * {@link Point} prototype if needed. + * @returns {*} - The newly created prototype as extended from {@link Series} + * or its derivatives. */ - H.seriesType = function(type, parent, options, props, pointProps) { // docs: add to API + extending Highcharts + // docs: add to API + extending Highcharts + H.seriesType = function(type, parent, options, props, pointProps) { var defaultOptions = H.getOptions(), seriesTypes = H.seriesTypes; // Merge the options defaultOptions.plotOptions[type] = H.merge( defaultOptions.plotOptions[parent], options ); // Create the class - seriesTypes[type] = H.extendClass(seriesTypes[parent] || function() {}, props); + seriesTypes[type] = H.extendClass(seriesTypes[parent] || + function() {}, props); seriesTypes[type].prototype.type = type; // Create the point class if needed if (pointProps) { - seriesTypes[type].prototype.pointClass = H.extendClass(H.Point, pointProps); + seriesTypes[type].prototype.pointClass = + H.extendClass(H.Point, pointProps); } return seriesTypes[type]; }; /** + * Get a unique key for using in internal element id's and pointers. The key + * is composed of a random hash specific to this Highcharts instance, and a + * counter. + * @function #uniqueKey + * @memberOf Highcharts + * @return {string} The key. + * @example + * var id = H.uniqueKey(); // => 'highcharts-x45f6hp-0' + */ + H.uniqueKey = (function() { + + var uniqueKeyHash = Math.random().toString(36).substring(2, 9), + idCounter = 0; + + return function() { + return 'highcharts-' + uniqueKeyHash + '-' + idCounter++; + }; + }()); + + /** * Register Highcharts as a plugin in jQuery */ if (win.jQuery) { win.jQuery.fn.highcharts = function() { var args = [].slice.call(arguments); @@ -1451,25 +1932,27 @@ if (this[0]) { // this[0] is the renderTo div // Create the chart if (args[0]) { new H[ // eslint-disable-line no-new - H.isString(args[0]) ? args.shift() : 'Chart' // Constructor defaults to Chart + // Constructor defaults to Chart + H.isString(args[0]) ? args.shift() : 'Chart' ](this[0], args[0], args[1]); return this; } - // When called without parameters or with the return argument, return an existing chart + // When called without parameters or with the return argument, + // return an existing chart return charts[H.attr(this[0], 'data-highcharts-chart')]; } }; } /** - * Compatibility section to add support for legacy IE. This can be removed if old IE - * support is not needed. + * Compatibility section to add support for legacy IE. This can be removed if + * old IE support is not needed. */ if (doc && !doc.defaultView) { H.getStyle = function(el, prop) { var val, alias = { @@ -1566,11 +2049,20 @@ var each = H.each, isNumber = H.isNumber, map = H.map, merge = H.merge, pInt = H.pInt; + /** + * @typedef {string} ColorString + * A valid color to be parsed and handled by Highcharts. Highcharts internally + * supports hex colors like `#ffffff`, rgb colors like `rgb(255,255,255)` and + * rgba colors like `rgba(255,255,255,1)`. Other colors may be supported by the + * browsers and displayed correctly, but Highcharts is not able to process them + * and apply concepts like opacity and brightening. + */ + /** * Handle color operations. The object methods are chainable. * @param {String} input The input color in either rbga or hex format */ H.Color = function(input) { // Backwards compatibility, allow instanciation without new @@ -1755,46 +2247,82 @@ removeEvent = H.removeEvent, splat = H.splat, stop = H.stop, svg = H.svg, SVG_NS = H.SVG_NS, + symbolSizes = H.symbolSizes, win = H.win; /** - * A wrapper object for SVG elements + * @typedef {Object} SVGDOMElement - An SVG DOM element. */ + /** + * The SVGElement prototype is a JavaScript wrapper for SVG elements used in the + * rendering layer of Highcharts. Combined with the {@link SVGRenderer} object, + * these prototypes allow freeform annotation in the charts or even in HTML + * pages without instanciating a chart. The SVGElement can also wrap HTML + * labels, when `text` or `label` elements are created with the `useHTML` + * parameter. + * + * The SVGElement instances are created through factory functions on the + * {@link SVGRenderer} object, like [rect]{@link SVGRenderer#rect}, + * [path]{@link SVGRenderer#path}, [text]{@link SVGRenderer#text}, [label]{@link + * SVGRenderer#label}, [g]{@link SVGRenderer#g} and more. + * + * @class + */ SVGElement = H.SVGElement = function() { return this; }; SVGElement.prototype = { // Default base for animation opacity: 1, SVG_NS: SVG_NS, - // For labels, these CSS properties are applied to the <text> node directly - textProps: ['direction', 'fontSize', 'fontWeight', 'fontFamily', 'fontStyle', 'color', - 'lineHeight', 'width', 'textDecoration', 'textOverflow', 'textShadow' + + /** + * For labels, these CSS properties are applied to the `text` node directly. + * @type {Array.<string>} + */ + textProps: ['direction', 'fontSize', 'fontWeight', 'fontFamily', + 'fontStyle', 'color', 'lineHeight', 'width', 'textDecoration', + 'textOverflow', 'textOutline' ], /** - * Initialize the SVG renderer - * @param {Object} renderer - * @param {String} nodeName + * Initialize the SVG renderer. This function only exists to make the + * initiation process overridable. It should not be called directly. + * + * @param {SVGRenderer} renderer The SVGRenderer instance to initialize to. + * @param {String} nodeName The SVG node name. + * @returns {void} */ init: function(renderer, nodeName) { - var wrapper = this; - wrapper.element = nodeName === 'span' ? + + /** + * The DOM node. Each SVGRenderer instance wraps a main DOM node, but + * may also represent more nodes. + * @type {SVGDOMNode|HTMLDOMNode} + */ + this.element = nodeName === 'span' ? createElement(nodeName) : - doc.createElementNS(wrapper.SVG_NS, nodeName); - wrapper.renderer = renderer; + doc.createElementNS(this.SVG_NS, nodeName); + + /** + * The renderer that the SVGElement belongs to. + * @type {SVGRenderer} + */ + this.renderer = renderer; }, /** - * Animate a given attribute - * @param {Object} params - * @param {Number} options Options include duration, easing, step and complete - * @param {Function} complete Function to perform at the end of animation + * Animate to given attributes or CSS properties. + * + * @param {SVGAttributes} params SVG attributes or CSS to animate. + * @param {AnimationOptions} [options] Animation options. + * @param {Function} [complete] Function to perform at the end of animation. + * @returns {SVGElement} Returns the SVGElement for chaining. */ animate: function(params, options, complete) { var animOptions = pick(options, this.renderer.globalAnimation, true); stop(this); // stop regardless of animation actually running, or reverting to .attr (#607) if (animOptions) { @@ -1807,12 +2335,57 @@ } return this; }, /** - * Build an SVG gradient out of a common JavaScript configuration object + * @typedef {Object} GradientOptions + * @property {Object} linearGradient Holds an object that defines the start + * position and the end position relative to the shape. + * @property {Number} linearGradient.x1 Start horizontal position of the + * gradient. Ranges 0-1. + * @property {Number} linearGradient.x2 End horizontal position of the + * gradient. Ranges 0-1. + * @property {Number} linearGradient.y1 Start vertical position of the + * gradient. Ranges 0-1. + * @property {Number} linearGradient.y2 End vertical position of the + * gradient. Ranges 0-1. + * @property {Object} radialGradient Holds an object that defines the center + * position and the radius. + * @property {Number} radialGradient.cx Center horizontal position relative + * to the shape. Ranges 0-1. + * @property {Number} radialGradient.cy Center vertical position relative + * to the shape. Ranges 0-1. + * @property {Number} radialGradient.r Radius relative to the shape. Ranges + * 0-1. + * @property {Array.<Array>} stops The first item in each tuple is the + * position in the gradient, where 0 is the start of the gradient and 1 + * is the end of the gradient. Multiple stops can be applied. The second + * item is the color for each stop. This color can also be given in the + * rgba format. + * + * @example + * // Linear gradient used as a color option + * color: { + * linearGradient: { x1: 0, x2: 0, y1: 0, y2: 1 }, + * stops: [ + * [0, '#003399'], // start + * [0.5, '#ffffff'], // middle + * [1, '#3366AA'] // end + * ] + * } + * } */ + /** + * Build and apply an SVG gradient out of a common JavaScript configuration + * object. This function is called from the attribute setters. + * + * @private + * @param {GradientOptions} color The gradient options structure. + * @param {string} prop The property to apply, can either be `fill` or + * `stroke`. + * @param {SVGDOMElement} elem SVG DOM element to apply the gradient on. + */ colorGradient: function(color, prop, elem) { var renderer = this.renderer, colorObject, gradName, gradAttr, @@ -1878,11 +2451,11 @@ id = gradients[key].attr('id'); } else { // Set the id and create the element - gradAttr.id = id = 'highcharts-' + H.idCounter++; + gradAttr.id = id = H.uniqueKey(); gradients[key] = gradientObject = renderer.createElement(gradName) .attr(gradAttr) .add(renderer.defs); gradientObject.radAttr = radAttr; @@ -1921,102 +2494,165 @@ }; } }, /** - * Apply a polyfill to the text-stroke CSS property, by copying the text element - * and apply strokes to the copy. + * Apply a text outline through a custom CSS property, by copying the text + * element and apply stroke to the copy. Used internally. Contrast checks + * at http://jsfiddle.net/highcharts/43soe9m1/2/ . * - * Contrast checks at http://jsfiddle.net/highcharts/43soe9m1/2/ + * @private + * @param {String} textOutline A custom CSS `text-outline` setting, defined + * by `width color`. + * @example + * // Specific color + * text.css({ + * textOutline: '1px black' + * }); + * // Automatic contrast + * text.css({ + * color: '#000000', // black text + * textOutline: '1px contrast' // => white outline + * }); */ - applyTextShadow: function(textShadow) { + applyTextOutline: function(textOutline) { var elem = this.element, tspans, - hasContrast = textShadow.indexOf('contrast') !== -1, + hasContrast = textOutline.indexOf('contrast') !== -1, styles = {}, - forExport = this.renderer.forExport, - // IE10 and IE11 report textShadow in elem.style even though it doesn't work. Check - // this again with new IE release. In exports, the rendering is passed to PhantomJS. - supports = this.renderer.forExport || (elem.style.textShadow !== undefined && !isMS); + color, + strokeWidth; - // When the text shadow is set to contrast, use dark stroke for light text and vice versa + // When the text shadow is set to contrast, use dark stroke for light + // text and vice versa. if (hasContrast) { - styles.textShadow = textShadow = textShadow.replace(/contrast/g, this.renderer.getContrast(elem.style.fill)); + styles.textOutline = textOutline = textOutline.replace( + /contrast/g, + this.renderer.getContrast(elem.style.fill) + ); } - // Safari with retina displays as well as PhantomJS bug (#3974). Firefox does not tolerate this, - // it removes the text shadows. - if (isWebKit || forExport) { - styles.textRendering = 'geometricPrecision'; - } + this.fakeTS = true; // Fake text shadow - /* Selective side-by-side testing in supported browser (http://jsfiddle.net/highcharts/73L1ptrh/) - if (elem.textContent.indexOf('2.') === 0) { - elem.style['text-shadow'] = 'none'; - supports = false; - } - // */ + // In order to get the right y position of the clone, + // copy over the y setter + this.ySetter = this.xSetter; - // No reason to polyfill, we've got native support - if (supports) { - this.css(styles); // Apply altered textShadow or textRendering workaround - } else { + tspans = [].slice.call(elem.getElementsByTagName('tspan')); - this.fakeTS = true; // Fake text shadow + // Extract the stroke width and color + textOutline = textOutline.split(' '); + color = textOutline[textOutline.length - 1]; + strokeWidth = textOutline[0]; - // In order to get the right y position of the clones, - // copy over the y setter - this.ySetter = this.xSetter; + if (strokeWidth && strokeWidth !== 'none') { - tspans = [].slice.call(elem.getElementsByTagName('tspan')); - each(textShadow.split(/\s?,\s?/g), function(textShadow) { - var firstChild = elem.firstChild, - color, - strokeWidth; + // Since the stroke is applied on center of the actual outline, we + // need to double it to get the correct stroke-width outside the + // glyphs. + strokeWidth = strokeWidth.replace( + /(^[\d\.]+)(.*?)$/g, + function(match, digit, unit) { + return (2 * digit) + unit; + } + ); - textShadow = textShadow.split(' '); - color = textShadow[textShadow.length - 1]; + // Remove shadows from previous runs + each(tspans, function(tspan) { + if (tspan.getAttribute('class') === 'highcharts-text-outline') { + // Remove then erase + erase(tspans, elem.removeChild(tspan)); + } + }); - // Approximately tune the settings to the text-shadow behaviour - strokeWidth = textShadow[textShadow.length - 2]; + this.realBox = elem.getBBox(); - if (strokeWidth) { - each(tspans, function(tspan, y) { - var clone; + // For each of the tspans, create a stroked copy behind it. + each(tspans, function(tspan, y) { + var clone; - // Let the first line start at the correct X position - if (y === 0) { - tspan.setAttribute('x', elem.getAttribute('x')); - y = elem.getAttribute('y'); - tspan.setAttribute('y', y || 0); - if (y === null) { - elem.setAttribute('y', 0); - } - } - - // Create the clone and apply shadow properties - clone = tspan.cloneNode(1); - attr(clone, { - 'class': 'highcharts-text-shadow', - 'fill': color, - 'stroke': color, - 'stroke-opacity': 1 / Math.max(pInt(strokeWidth), 3), - 'stroke-width': strokeWidth, - 'stroke-linejoin': 'round' - }); - elem.insertBefore(clone, firstChild); - }); + // Let the first line start at the correct X position + if (y === 0) { + tspan.setAttribute('x', elem.getAttribute('x')); + y = elem.getAttribute('y'); + tspan.setAttribute('y', y || 0); + if (y === null) { + elem.setAttribute('y', 0); + } } + + // Create the clone and apply outline properties + clone = tspan.cloneNode(1); + attr(clone, { + 'class': 'highcharts-text-outline', + 'fill': color, + 'stroke': color, + 'stroke-width': strokeWidth, + 'stroke-linejoin': 'round' + }); + elem.insertBefore(clone, elem.firstChild); }); } }, /** - * Set or get a given attribute - * @param {Object|String} hash - * @param {Mixed|Undefined} val + * + * @typedef {Object} SVGAttributes An object of key-value pairs for SVG + * attributes. Attributes in Highcharts elements for the most parts + * correspond to SVG, but some are specific to Highcharts, like `zIndex`, + * `rotation`, `translateX`, `translateY`, `scaleX` and `scaleY`. SVG + * attributes containing a hyphen are _not_ camel-cased, they should be + * quoted to preserve the hyphen. + * @example + * { + * 'stroke': '#ff0000', // basic + * 'stroke-width': 2, // hyphenated + * 'rotation': 45 // custom + * 'd': ['M', 10, 10, 'L', 30, 30, 'z'] // path definition, note format + * } */ + /** + * Apply native and custom attributes to the SVG elements. + * + * In order to set the rotation center for rotation, set x and y to 0 and + * use `translateX` and `translateY` attributes to position the element + * instead. + * + * Attributes frequently used in Highcharts are `fill`, `stroke`, + * `stroke-width`. + * + * @param {SVGAttributes|String} hash - The native and custom SVG + * attributes. + * @param {string} [val] - If the type of the first argument is `string`, + * the second can be a value, which will serve as a single attribute + * setter. If the first argument is a string and the second is undefined, + * the function serves as a getter and the current value of the property + * is returned. + * @param {Function} complete - A callback function to execute after setting + * the attributes. This makes the function compliant and interchangeable + * with the {@link SVGElement#animate} function. + * + * @returns {SVGElement|string|number} If used as a setter, it returns the + * current {@link SVGElement} so the calls can be chained. If used as a + * getter, the current value of the attribute is returned. + * + * @example + * // Set multiple attributes + * element.attr({ + * stroke: 'red', + * fill: 'blue', + * x: 10, + * y: 10 + * }); + * + * // Set a single attribute + * element.attr('stroke', 'red'); + * + * // Get an attribute + * element.attr('stroke'); // => 'red' + * + */ attr: function(hash, val, complete) { var key, value, element = this.element, hasSetSymbolSize, @@ -2086,15 +2722,18 @@ return ret; }, /** - * Update the shadow elements with new attributes - * @param {String} key The attribute name - * @param {String|Number} value The value of the attribute - * @param {Function} setter The setter function, inherited from the parent wrapper - * @returns {undefined} + * Update the shadow elements with new attributes. + * + * @private + * @param {String} key - The attribute name. + * @param {String|Number} value - The value of the attribute. + * @param {Function} setter - The setter function, inherited from the + * parent wrapper + * @returns {void} */ updateShadows: function(key, value, setter) { var shadows = this.shadows, i = shadows.length; @@ -2110,36 +2749,57 @@ } }, /** - * Add a class name to an element + * Add a class name to an element. + * + * @param {string} className - The new class name to add. + * @param {boolean} [replace=false] - When true, the existing class name(s) + * will be overwritten with the new one. When false, the new one is + * added. + * @returns {SVGElement} Return the SVG element for chainability. */ addClass: function(className, replace) { var currentClassName = this.attr('class') || ''; if (currentClassName.indexOf(className) === -1) { if (!replace) { - className = (currentClassName + (currentClassName ? ' ' : '') + className).replace(' ', ' '); + className = + (currentClassName + (currentClassName ? ' ' : '') + + className).replace(' ', ' '); } this.attr('class', className); } return this; }, + + /** + * Check if an element has the given class name. + * @param {string} className - The class name to check for. + * @return {Boolean} + */ hasClass: function(className) { return attr(this.element, 'class').indexOf(className) !== -1; }, + + /** + * Remove a class name from the element. + * @param {string} className The class name to remove. + * @return {SVGElement} Returns the SVG element for chainability. + */ removeClass: function(className) { attr(this.element, 'class', (attr(this.element, 'class') || '').replace(className, '')); return this; }, /** * If one of the symbol size affecting parameters are changed, * check all the others only once for each call to an element's * .attr() method - * @param {Object} hash + * @param {Object} hash - The attributes to set. + * @private */ symbolAttr: function(hash) { var wrapper = this; each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR', 'anchorX', 'anchorY'], function(key) { @@ -2156,25 +2816,40 @@ ) }); }, /** - * Apply a clipping path to this object - * @param {String} id + * Apply a clipping rectangle to this element. + * + * @param {ClipRect} [clipRect] - The clipping rectangle. If skipped, the + * current clip is removed. + * @returns {SVGElement} Returns the SVG element to allow chaining. */ clip: function(clipRect) { - return this.attr('clip-path', clipRect ? 'url(' + this.renderer.url + '#' + clipRect.id + ')' : 'none'); + return this.attr( + 'clip-path', + clipRect ? + 'url(' + this.renderer.url + '#' + clipRect.id + ')' : + 'none' + ); }, /** - * Calculate the coordinates needed for drawing a rectangle crisply and return the - * calculated attributes - * @param {Number} strokeWidth - * @param {Number} x - * @param {Number} y - * @param {Number} width - * @param {Number} height + * Calculate the coordinates needed for drawing a rectangle crisply and + * return the calculated attributes. + * + * @param {Object} rect - A rectangle. + * @param {number} rect.x - The x position. + * @param {number} rect.y - The y position. + * @param {number} rect.width - The width. + * @param {number} rect.height - The height. + * @param {number} [strokeWidth] - The stroke width to consider when + * computing crisp positioning. It can also be set directly on the rect + * parameter. + * + * @returns {{x: Number, y: Number, width: Number, height: Number}} The + * modified rectangle arguments. */ crisp: function(rect, strokeWidth) { var wrapper = this, key, @@ -2201,12 +2876,16 @@ return attribs; }, /** - * Set styles for the element - * @param {Object} styles + * Set styles for the element. In addition to CSS styles supported by + * native SVG and HTML elements, there are also some custom made for + * Highcharts, like `width`, `ellipsis` and `textOverflow` for SVG text + * elements. + * @param {CSSObject} styles The new CSS styles. + * @returns {SVGElement} Return the SVG element for chaining. */ css: function(styles) { var elemWrapper = this, oldStyles = elemWrapper.styles, newStyles = {}, @@ -2263,43 +2942,63 @@ } attr(elem, 'style', serializedCss); // #1881 } - // Rebuild text after added - if (elemWrapper.added && textWidth) { - elemWrapper.renderer.buildText(elemWrapper); + if (elemWrapper.added) { + // Rebuild text after added + if (textWidth) { + elemWrapper.renderer.buildText(elemWrapper); + } + + // Apply text outline after added + if (styles && styles.textOutline) { + elemWrapper.applyTextOutline(styles.textOutline); + } } } return elemWrapper; }, + /** + * Get the current stroke width. In classic mode, the setter registers it + * directly on the element. + * @returns {number} The stroke width in pixels. + * @ignore + */ strokeWidth: function() { return this['stroke-width'] || 0; }, /** - * Add an event listener - * @param {String} eventType - * @param {Function} handler + * Add an event listener. This is a simple setter that replaces all other + * events of the same type, opposed to the {@link Highcharts#addEvent} + * function. + * @param {string} eventType - The event type. If the type is `click`, + * Highcharts will internally translate it to a `touchstart` event on + * touch devices, to prevent the browser from waiting for a click event + * from firing. + * @param {Function} handler - The handler callback. + * @returns {SVGElement} The SVGElement for chaining. */ on: function(eventType, handler) { var svgElement = this, element = svgElement.element; // touch if (hasTouch && eventType === 'click') { element.ontouchstart = function(e) { - svgElement.touchEventFired = Date.now(); + svgElement.touchEventFired = Date.now(); // #2269 e.preventDefault(); handler.call(element, e); }; element.onclick = function(e) { - if (win.navigator.userAgent.indexOf('Android') === -1 || Date.now() - (svgElement.touchEventFired || 0) > 1100) { // #2269 + if (win.navigator.userAgent.indexOf('Android') === -1 || + Date.now() - (svgElement.touchEventFired || 0) > 1100) { handler.call(element, e); } }; } else { // simplest possible event model for internal use @@ -2308,12 +3007,16 @@ return this; }, /** * Set the coordinates needed to draw a consistent radial gradient across - * pie slices regardless of positioning inside the chart. The format is - * [centerX, centerY, diameter] in pixels. + * a shape regardless of positioning inside the chart. Used on pie slices + * to make all the slices have the same radial reference point. + * + * @param {Array} coordinates The center reference. The format is + * `[centerX, centerY, diameter]` in pixels. + * @returns {SVGElement} Returns the SVGElement for chaining. */ setRadialReference: function(coordinates) { var existingGradient = this.renderer.gradients[this.element.gradient]; this.element.radialReference = coordinates; @@ -2331,34 +3034,44 @@ return this; }, /** - * Move an object and its children by x and y values - * @param {Number} x - * @param {Number} y + * Move an object and its children by x and y values. + * + * @param {number} x - The x value. + * @param {number} y - The y value. */ translate: function(x, y) { return this.attr({ translateX: x, translateY: y }); }, /** - * Invert a group, rotate and flip + * Invert a group, rotate and flip. This is used internally on inverted + * charts, where the points and graphs are drawn as if not inverted, then + * the series group elements are inverted. + * + * @param {boolean} inverted - Whether to invert or not. An inverted shape + * can be un-inverted by setting it to false. + * @returns {SVGElement} Return the SVGElement for chaining. */ invert: function(inverted) { var wrapper = this; wrapper.inverted = inverted; wrapper.updateTransform(); return wrapper; }, /** - * Private method to update the transform attribute based on internal - * properties + * Update the transform attribute based on internal properties. Deals with + * the custom `translateX`, `translateY`, `rotation`, `scaleX` and `scaleY` + * attributes and updates the SVG `transform` attribute. + * @private + * @returns {void} */ updateTransform: function() { var wrapper = this, translateX = wrapper.translateX || 0, translateY = wrapper.translateY || 0, @@ -2396,31 +3109,44 @@ if (transform.length) { element.setAttribute('transform', transform.join(' ')); } }, + /** - * Bring the element to the front + * Bring the element to the front. + * + * @returns {SVGElement} Returns the SVGElement for chaining. */ toFront: function() { var element = this.element; element.parentNode.appendChild(element); return this; }, /** - * Break down alignment options like align, verticalAlign, x and y - * to x and y relative to the chart. - * - * @param {Object} alignOptions - * @param {Boolean} alignByTranslate - * @param {String[Object} box The box to align to, needs a width and height. When the - * box is a string, it refers to an object in the Renderer. For example, when - * box is 'spacingBox', it refers to Renderer.spacingBox which holds width, height - * x and y properties. - * + * Align the element relative to the chart or another box. + * ß + * @param {Object} [alignOptions] The alignment options. The function can be + * called without this parameter in order to re-align an element after the + * box has been updated. + * @param {string} [alignOptions.align=left] Horizontal alignment. Can be + * one of `left`, `center` and `right`. + * @param {string} [alignOptions.verticalAlign=top] Vertical alignment. Can + * be one of `top`, `middle` and `bottom`. + * @param {number} [alignOptions.x=0] Horizontal pixel offset from + * alignment. + * @param {number} [alignOptions.y=0] Vertical pixel offset from alignment. + * @param {Boolean} [alignByTranslate=false] Use the `transform` attribute + * with translateX and translateY custom attributes to align this elements + * rather than `x` and `y` attributes. + * @param {String|Object} box The box to align to, needs a width and height. + * When the box is a string, it refers to an object in the Renderer. For + * example, when box is `spacingBox`, it refers to `Renderer.spacingBox` + * which holds `width`, `height`, `x` and `y` properties. + * @returns {SVGElement} Returns the SVGElement for chaining. */ align: function(alignOptions, alignByTranslate, box) { var align, vAlign, x, @@ -2488,11 +3214,24 @@ return this; }, /** - * Get the bounding box (width, height, x and y) for the element + * Get the bounding box (width, height, x and y) for the element. Generally + * used to get rendered text size. Since this is called a lot in charts, + * the results are cached based on text properties, in order to save DOM + * traffic. The returned bounding box includes the rotation, so for example + * a single text line of rotation 90 will report a greater height, and a + * width corresponding to the line-height. + * + * @param {boolean} [reload] Skip the cache and get the updated DOM bouding + * box. + * @param {number} [rot] Override the element's rotation. This is internally + * used on axis labels with a value of 0 to find out what the bounding box + * would be have been if it were not rotated. + * @returns {Object} The bounding box with `x`, `y`, `width` and `height` + * properties. */ getBBox: function(reload, rot) { var wrapper = this, bBox, // = wrapper.bBox, renderer = wrapper.renderer, @@ -2502,12 +3241,10 @@ rad, element = wrapper.element, styles = wrapper.styles, fontSize, textStr = wrapper.textStr, - textShadow, - elemStyle = element.style, toggleTextShadowShim, cache = renderer.cache, cacheKeys = renderer.cacheKeys, cacheKey; @@ -2518,19 +3255,29 @@ fontSize = styles && styles.fontSize; if (textStr !== undefined) { - cacheKey = + cacheKey = textStr.toString(); - // Since numbers are monospaced, and numerical labels appear a lot in a chart, - // we assume that a label of n characters has the same bounding box as others - // of the same length. - textStr.toString().replace(/[0-9]/g, '0') + + // Since numbers are monospaced, and numerical labels appear a lot + // in a chart, we assume that a label of n characters has the same + // bounding box as others of the same length. Unless there is inner + // HTML in the label. In that case, leave the numbers as is (#5899). + if (cacheKey.indexOf('<') === -1) { + cacheKey = cacheKey.replace(/[0-9]/g, '0'); + } - // Properties that affect bounding box - ['', rotation || 0, fontSize, element.style.width].join(','); + // Properties that affect bounding box + cacheKey += [ + '', + rotation || 0, + fontSize, + element.style.width, + element.style['text-overflow'] // #5968 + ] + .join(','); } if (cacheKey && !reload) { bBox = cache[cacheKey]; @@ -2544,20 +3291,17 @@ try { // Fails in Firefox if the container has display: none. // When the text shadow shim is used, we need to hide the fake shadows // to get the correct bounding box (#3872) toggleTextShadowShim = this.fakeTS && function(display) { - each(element.querySelectorAll('.highcharts-text-shadow'), function(tspan) { + each(element.querySelectorAll('.highcharts-text-outline'), function(tspan) { tspan.style.display = display; }); }; // Workaround for #3842, Firefox reporting wrong bounding box for shadows - if (isFirefox && elemStyle.textShadow) { - textShadow = elemStyle.textShadow; - elemStyle.textShadow = ''; - } else if (toggleTextShadowShim) { + if (toggleTextShadowShim) { toggleTextShadowShim('none'); } bBox = element.getBBox ? // SVG: use extend because IE9 is not allowed to change width and height in case @@ -2568,13 +3312,11 @@ width: element.offsetWidth, height: element.offsetHeight }; // #3842 - if (textShadow) { - elemStyle.textShadow = textShadow; - } else if (toggleTextShadowShim) { + if (toggleTextShadowShim) { toggleTextShadowShim(''); } } catch (e) {} // If the bBox is not set, the try-catch block above failed. The other condition @@ -2629,27 +3371,42 @@ } return bBox; }, /** - * Show the element + * Show the element after it has been hidden. + * + * @param {boolean} [inherit=false] Set the visibility attribute to + * `inherit` rather than `visible`. The difference is that an element with + * `visibility="visible"` will be visible even if the parent is hidden. + * + * @returns {SVGElement} Returns the SVGElement for chaining. */ show: function(inherit) { return this.attr({ visibility: inherit ? 'inherit' : 'visible' }); }, /** - * Hide the element + * Hide the element, equivalent to setting the `visibility` attribute to + * `hidden`. + * + * @returns {SVGElement} Returns the SVGElement for chaining. */ hide: function() { return this.attr({ visibility: 'hidden' }); }, + /** + * Fade out an element by animating its opacity down to 0, and hide it on + * complete. Used internally for the tooltip. + * + * @param {number} [duration=150] The fade duration in milliseconds. + */ fadeOut: function(duration) { var elemWrapper = this; elemWrapper.animate({ opacity: 0 }, { @@ -2661,13 +3418,18 @@ } }); }, /** - * Add the element - * @param {Object|Undefined} parent Can be an element, an element wrapper or undefined - * to append the element to the renderer.box. + * Add the element to the DOM. All elements must be added this way. + * + * @param {SVGElement|SVGDOMElement} [parent] The parent item to add it to. + * If undefined, the element is added to the {@link SVGRenderer.box}. + * + * @returns {SVGElement} Returns the SVGElement for chaining. + * + * @sample highcharts/members/renderer-g - Elements added to a group */ add: function(parent) { var renderer = this.renderer, element = this.element, @@ -2706,22 +3468,27 @@ return this; }, /** - * Removes a child either by removeChild or move to garbageBin. - * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not. + * Removes an element from the DOM. + * + * @private + * @param {SVGDOMElement|HTMLDOMElement} element The DOM node to remove. */ safeRemoveChild: function(element) { var parentNode = element.parentNode; if (parentNode) { parentNode.removeChild(element); } }, /** - * Destroy the element and element wrapper + * Destroy the element and element wrapper and clear up the DOM and event + * hooks. + * + * @returns {void} */ destroy: function() { var wrapper = this, element = wrapper.element || {}, parentToClean = wrapper.renderer.isSVG && element.nodeName === 'SPAN' && wrapper.parentGroup, @@ -2772,13 +3539,39 @@ return null; }, /** - * Add a shadow to the element. Must be done after the element is added to the DOM - * @param {Boolean|Object} shadowOptions + * @typedef {Object} ShadowOptions + * @property {string} [color=#000000] The shadow color. + * @property {number} [offsetX=1] The horizontal offset from the element. + * @property {number} [offsetY=1] The vertical offset from the element. + * @property {number} [opacity=0.15] The shadow opacity. + * @property {number} [width=3] The shadow width or distance from the + * element. */ + /** + * Add a shadow to the element. Must be called after the element is added to + * the DOM. In styled mode, this method is not used, instead use `defs` and + * filters. + * + * @param {boolean|ShadowOptions} shadowOptions The shadow options. If + * `true`, the default options are applied. If `false`, the current + * shadow will be removed. + * @param {SVGElement} [group] The SVG group element where the shadows will + * be applied. The default is to add it to the same parent as the current + * element. Internally, this is ised for pie slices, where all the + * shadows are added to an element behind all the slices. + * @param {boolean} [cutOff] Used internally for column shadows. + * + * @returns {SVGElement} Returns the SVGElement for chaining. + * + * @example + * renderer.rect(10, 100, 100, 100) + * .attr({ fill: 'red' }) + * .shadow(true); + */ shadow: function(shadowOptions, group, cutOff) { var shadows = [], i, shadow, element = this.element, @@ -2827,10 +3620,14 @@ } return this; }, + /** + * Destroy shadows on the element. + * @private + */ destroyShadows: function() { each(this.shadows || [], function(shadow) { this.safeRemoveChild(shadow); }, this); this.shadows = undefined; @@ -2849,11 +3646,14 @@ return this._defaultGetter(key); }, /** * Get the current value of an attribute or pseudo attribute, used mainly - * for animation. + * for animation. Called internally from the {@link SVGRenderer#attr} + * function. + * + * @private */ _defaultGetter: function(key) { var ret = pick(this[key], this.element ? this.element.getAttribute(key) : null, 0); if (/^[\-0-9\.]+$/.test(ret)) { // is numerical @@ -2913,10 +3713,14 @@ center: 'middle', right: 'end' }; this.element.setAttribute('text-anchor', convert[value]); }, + opacitySetter: function(value, key, element) { + this[key] = value; + element.setAttribute(key, value); + }, titleSetter: function(value) { var titleNode = this.element.getElementsByTagName('title')[0]; if (!titleNode) { titleNode = doc.createElementNS(this.SVG_NS, 'title'); this.element.appendChild(titleNode); @@ -2997,11 +3801,15 @@ otherZIndex = otherElement.zIndex; if (otherElement !== element && ( // Insert before the first element with a higher zIndex pInt(otherZIndex) > value || // If no zIndex given, insert before the first element with a zIndex - (!defined(value) && defined(otherZIndex)) + (!defined(value) && defined(otherZIndex)) || + // Negative zIndex versus no zIndex: + // On all levels except the highest. If the parent is <svg>, + // then we don't want to put items before <desc> or <defs> + (value < 0 && !defined(otherZIndex) && parentNode !== renderer.box) )) { parentNode.insertBefore(element, otherElement); inserted = true; } @@ -3023,15 +3831,10 @@ SVGElement.prototype.rotationSetter = SVGElement.prototype.verticalAlignSetter = SVGElement.prototype.scaleXSetter = SVGElement.prototype.scaleYSetter = function(value, key) { this[key] = value; this.doTransform = true; }; - // These setters both set the key on the instance itself plus as an attribute - SVGElement.prototype.opacitySetter = SVGElement.prototype.displaySetter = function(value, key, element) { - this[key] = value; - element.setAttribute(key, value); - }; // WebKit and Batik have problems with a stroke-width of zero, so in this case we remove the // stroke attribute altogether. #1270, #1369, #3065, #3072. SVGElement.prototype['stroke-widthSetter'] = SVGElement.prototype.strokeSetter = function(value, key, element) { @@ -3047,24 +3850,49 @@ } }; /** - * The default SVG renderer + * Allows direct access to the Highcharts rendering layer in order to draw + * primitive shapes like circles, rectangles, paths or text directly on a chart, + * or independent from any chart. The SVGRenderer represents a wrapper object + * for SVGin modern browsers and through the VMLRenderer, for VML in IE < 8. + * + * An existing chart's renderer can be accessed through {@link Chart#renderer}. + * The renderer can also be used completely decoupled from a chart. + * + * @param {HTMLDOMElement} container - Where to put the SVG in the web page. + * @param {number} width - The width of the SVG. + * @param {number} height - The height of the SVG. + * @param {boolean} [forExport=false] - Whether the rendered content is intended + * for export. + * @param {boolean} [allowHTML=true] - Whether the renderer is allowed to + * include HTML text, which will be projected on top of the SVG. + * + * @example + * // Use directly without a chart object. + * var renderer = new Highcharts.Renderer(parentNode, 600, 400); + * + * @sample highcharts/members/renderer-on-chart - Annotating a chart programmatically. + * @sample highcharts/members/renderer-basic - Independedt SVG drawing. + * + * @class */ SVGRenderer = H.SVGRenderer = function() { this.init.apply(this, arguments); }; SVGRenderer.prototype = { + /** + * A pointer to the renderer's associated Element class. The VMLRenderer + * will have a pointer to VMLElement here. + * @type {SVGElement} + */ Element: SVGElement, SVG_NS: SVG_NS, /** - * Initialize the SVGRenderer - * @param {Object} container - * @param {Number} width - * @param {Number} height - * @param {Boolean} forExport + * Initialize the SVGRenderer. Overridable initiator function that takes + * the same parameters as the constructor. */ init: function(container, width, height, style, forExport, allowHTML) { var renderer = this, boxWrapper, element, @@ -3085,25 +3913,38 @@ attr(element, 'xmlns', this.SVG_NS); } // object properties renderer.isSVG = true; - renderer.box = element; - renderer.boxWrapper = boxWrapper; + + /** + * The root `svg` node of the renderer. + * @type {SVGDOMElement} + */ + this.box = element; + /** + * The wrapper for the root `svg` node of the renderer. + * @type {SVGElement} + */ + this.boxWrapper = boxWrapper; renderer.alignedObjects = []; - // Page url used for internal references. #24, #672, #1070 - renderer.url = (isFirefox || isWebKit) && doc.getElementsByTagName('base').length ? + /** + * Page url used for internal references. + * @type {string} + */ + // #24, #672, #1070 + this.url = (isFirefox || isWebKit) && doc.getElementsByTagName('base').length ? win.location.href .replace(/#.*?$/, '') // remove the hash .replace(/([\('\)])/g, '\\$1') // escape parantheses and quotes .replace(/ /g, '%20') : // replace spaces (needed for Safari only) ''; // Add description desc = this.createElement('desc').add(); - desc.element.appendChild(doc.createTextNode('Created with Highcharts 5.0.0')); + desc.element.appendChild(doc.createTextNode('Created with Highcharts 5.0.3')); renderer.defs = this.createElement('defs').add(); renderer.allowHTML = allowHTML; renderer.forExport = forExport; @@ -3122,11 +3963,11 @@ // inside this container will be drawn at subpixel precision. In order to draw // sharp lines, this must be compensated for. This doesn't seem to work inside // iframes though (like in jsFiddle). var subPixelFix, rect; if (isFirefox && container.getBoundingClientRect) { - renderer.subPixelFix = subPixelFix = function() { + subPixelFix = function() { css(container, { left: 0, top: 0 }); rect = container.getBoundingClientRect(); @@ -3138,35 +3979,49 @@ // run the fix now subPixelFix(); // run it on resize - addEvent(win, 'resize', subPixelFix); + renderer.unSubPixelFix = addEvent(win, 'resize', subPixelFix); } }, + /** + * Get the global style setting for the renderer. + * @private + * @param {CSSObject} style - Style settings. + * @return {CSSObject} The style settings mixed with defaults. + */ getStyle: function(style) { this.style = extend({ fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif', // default font fontSize: '12px' }, style); return this.style; }, + /** + * Apply the global style on the renderer, mixed with the default styles. + * @param {CSSObject} style - CSS to apply. + */ setStyle: function(style) { this.boxWrapper.css(this.getStyle(style)); }, /** - * Detect whether the renderer is hidden. This happens when one of the parent elements - * has display: none. #608. + * Detect whether the renderer is hidden. This happens when one of the + * parent elements has display: none. Used internally to detect when we need + * to render preliminarily in another div to get the text bounding boxes + * right. + * + * @returns {boolean} True if it is hidden. */ - isHidden: function() { + isHidden: function() { // #608 return !this.boxWrapper.getBBox().width; }, /** * Destroys the renderer and its allocated members. @@ -3185,52 +4040,64 @@ // Otherwise, destroy them here. if (rendererDefs) { renderer.defs = rendererDefs.destroy(); } - // Remove sub pixel fix handler - // We need to check that there is a handler, otherwise all functions that are registered for event 'resize' are removed - // See issue #982 - if (renderer.subPixelFix) { - removeEvent(win, 'resize', renderer.subPixelFix); + // Remove sub pixel fix handler (#982) + if (renderer.unSubPixelFix) { + renderer.unSubPixelFix(); } renderer.alignedObjects = null; return null; }, /** - * Create a wrapper for an SVG element - * @param {Object} nodeName + * Create a wrapper for an SVG element. Serves as a factory for + * {@link SVGElement}, but this function is itself mostly called from + * primitive factories like {@link SVGRenderer#path}, {@link + * SVGRenderer#rect} or {@link SVGRenderer#text}. + * + * @param {string} nodeName - The node name, for example `rect`, `g` etc. + * @returns {SVGElement} The generated SVGElement. */ createElement: function(nodeName) { var wrapper = new this.Element(); wrapper.init(this, nodeName); return wrapper; }, /** - * Dummy function for plugins + * Dummy function for plugins, called every time the renderer is updated. + * Prior to Highcharts 5, this was used for the canvg renderer. + * @function */ draw: noop, /** - * Get converted radial gradient attributes + * Get converted radial gradient attributes according to the radial + * reference. Used internally from the {@link SVGElement#colorGradient} + * function. + * + * @private */ getRadialAttr: function(radialReference, gradAttr) { return { cx: (radialReference[0] - radialReference[2] / 2) + gradAttr.cx * radialReference[2], cy: (radialReference[1] - radialReference[2] / 2) + gradAttr.cy * radialReference[2], r: gradAttr.r * radialReference[2] }; }, /** - * Parse a simple HTML string into SVG tspans - * - * @param {Object} textNode The parent text SVG node + * Parse a simple HTML string into SVG tspans. Called internally when text + * is set on an SVGElement. The function supports a subset of HTML tags, + * CSS text features like `width`, `text-overflow`, `white-space`, and + * also attributes like `href` and `style`. + * @private + * @param {SVGElement} wrapper The parent SVGElement. */ buildText: function(wrapper) { var textNode = wrapper.element, renderer = this, forExport = renderer.forExport, @@ -3244,11 +4111,11 @@ wasTooLong, parentX = attr(textNode, 'x'), textStyles = wrapper.styles, width = wrapper.textWidth, textLineHeight = textStyles && textStyles.lineHeight, - textShadow = textStyles && textStyles.textShadow, + textOutline = textStyles && textStyles.textOutline, ellipsis = textStyles && textStyles.textOverflow === 'ellipsis', i = childNodes.length, tempParent = width && !wrapper.added && this.box, getLineHeight = function(tspan) { var fontSizeStyle; @@ -3260,11 +4127,12 @@ return textLineHeight ? pInt(textLineHeight) : renderer.fontMetrics( fontSizeStyle, - tspan + // Get the computed size from parent if not explicit + tspan.getAttribute('style') ? tspan : textNode ).h; }, unescapeAngleBrackets = function(inputStr) { return inputStr.replace(/&lt;/g, '<').replace(/&gt;/g, '>'); }; @@ -3274,11 +4142,11 @@ textNode.removeChild(childNodes[i]); } // Skip tspans, add text directly to text node. The forceTSpan is a hook // used in text outline hack. - if (!hasMarkup && !textShadow && !ellipsis && !width && textStr.indexOf(' ') === -1) { + if (!hasMarkup && !textOutline && !ellipsis && !width && textStr.indexOf(' ') === -1) { textNode.appendChild(doc.createTextNode(unescapeAngleBrackets(textStr))); // Complex strings, add more logic } else { @@ -3468,13 +4336,13 @@ } if (tempParent) { tempParent.removeChild(textNode); // attach it to the DOM to read offset width } - // Apply the text shadow - if (textShadow && wrapper.applyTextShadow) { - wrapper.applyTextShadow(textShadow); + // Apply the text outline + if (textOutline && wrapper.applyTextOutline) { + wrapper.applyTextOutline(textOutline); } } }, @@ -3511,26 +4379,36 @@ console.log('width', width, 'stringWidth', node.getSubStringLength(0, finalPos)) }, */ /** - * Returns white for dark colors and black for bright colors + * Returns white for dark colors and black for bright colors. + * + * @param {ColorString} rgba - The color to get the contrast for. + * @returns {string} The contrast color, either `#000000` or `#FFFFFF`. */ getContrast: function(rgba) { rgba = color(rgba).rgba; return rgba[0] + rgba[1] + rgba[2] > 2 * 255 ? '#000000' : '#FFFFFF'; }, /** - * Create a button with preset states - * @param {String} text - * @param {Number} x - * @param {Number} y - * @param {Function} callback - * @param {Object} normalState - * @param {Object} hoverState - * @param {Object} pressedState + * Create a button with preset states. + * @param {string} text - The text or HTML to draw. + * @param {number} x - The x position of the button's left side. + * @param {number} y - The y position of the button's top side. + * @param {Function} callback - The function to execute on button click or + * touch. + * @param {SVGAttributes} [normalState] - SVG attributes for the normal + * state. + * @param {SVGAttributes} [hoverState] - SVG attributes for the hover state. + * @param {SVGAttributes} [pressedState] - SVG attributes for the pressed + * state. + * @param {SVGAttributes} [disabledState] - SVG attributes for the disabled + * state. + * @param {Symbol} [shape=rect] - The shape type. + * @returns {SVGRenderer} The button element. */ button: function(text, x, y, callback, normalState, hoverState, pressedState, disabledState, shape) { var label = this.label(text, x, y, shape, null, null, null, null, 'button'), curState = 0; @@ -3633,16 +4511,19 @@ } }); }, /** - * Make a straight line crisper by not spilling out to neighbour pixels - * @param {Array} points - * @param {Number} width + * Make a straight line crisper by not spilling out to neighbour pixels. + * + * @param {Array} points - The original points on the format `['M', 0, 0, + * 'L', 100, 0]`. + * @param {number} width - The width of the line. + * @returns {Array} The original points array, but modified to render + * crisply. */ crispLine: function(points, width) { - // points format: ['M', 0, 0, 'L', 100, 0] // normalize to a crisp line if (points[1] === points[4]) { // Substract due to #1129. Now bottom and left axis gridlines behave the same. points[1] = points[4] = Math.round(points[1]) - (width % 2 / 2); } @@ -3652,13 +4533,26 @@ return points; }, /** - * Draw a path - * @param {Array} path An SVG path in array form + * Draw a path, wraps the SVG `path` element. + * + * @param {Array} [path] An SVG path definition in array form. + * + * @example + * var path = renderer.path(['M', 10, 10, 'L', 30, 30, 'z']) + * .attr({ stroke: '#ff00ff' }) + * .add(); + * @returns {SVGElement} The generated wrapper element. */ + /** + * Draw a path, wraps the SVG `path` element. + * + * @param {SVGAttributes} [attribs] The initial attributes. + * @returns {SVGElement} The generated wrapper element. + */ path: function(path) { var attribs = { fill: 'none' @@ -3670,15 +4564,23 @@ } return this.createElement('path').attr(attribs); }, /** - * Draw and return an SVG circle - * @param {Number} x The x position - * @param {Number} y The y position - * @param {Number} r The radius + * Draw a circle, wraps the SVG `circle` element. + * + * @param {number} [x] The center x position. + * @param {number} [y] The center y position. + * @param {number} [r] The radius. + * @returns {SVGElement} The generated wrapper element. */ + /** + * Draw a circle, wraps the SVG `circle` element. + * + * @param {SVGAttributes} [attribs] The initial attributes. + * @returns {SVGElement} The generated wrapper element. + */ circle: function(x, y, r) { var attribs = isObject(x) ? x : { x: x, y: y, r: r @@ -3692,18 +4594,26 @@ return wrapper.attr(attribs); }, /** - * Draw and return an arc - * @param {Number} x X position - * @param {Number} y Y position - * @param {Number} r Radius - * @param {Number} innerR Inner radius like used in donut charts - * @param {Number} start Starting angle - * @param {Number} end Ending angle + * Draw and return an arc. + * @param {number} [x=0] Center X position. + * @param {number} [y=0] Center Y position. + * @param {number} [r=0] The outer radius of the arc. + * @param {number} [innerR=0] Inner radius like used in donut charts. + * @param {number} [start=0] The starting angle of the arc in radians, where + * 0 is to the right and `-Math.PI/2` is up. + * @param {number} [end=0] The ending angle of the arc in radians, where 0 + * is to the right and `-Math.PI/2` is up. + * @returns {SVGElement} The generated wrapper element. */ + /** + * Draw and return an arc. Overloaded function that takes arguments object. + * @param {SVGAttributes} attribs Initial SVG attributes. + * @returns {SVGElement} The generated wrapper element. + */ arc: function(x, y, r, innerR, start, end) { var arc; if (isObject(x)) { y = x.y; @@ -3724,18 +4634,26 @@ arc.r = r; // #959 return arc; }, /** - * Draw and return a rectangle - * @param {Number} x Left position - * @param {Number} y Top position - * @param {Number} width - * @param {Number} height - * @param {Number} r Border corner radius - * @param {Number} strokeWidth A stroke width can be supplied to allow crisp drawing + * Draw and return a rectangle. + * @param {number} [x] Left position. + * @param {number} [y] Top position. + * @param {number} [width] Width of the rectangle. + * @param {number} [height] Height of the rectangle. + * @param {number} [r] Border corner radius. + * @param {number} [strokeWidth] A stroke width can be supplied to allow + * crisp drawing. + * @returns {SVGElement} The generated wrapper element. */ + /** + * Draw and return a rectangle. + * @param {SVGAttributes} [attributes] General SVG attributes for the + * rectangle. + * @returns {SVGElement} The generated wrapper element. + */ rect: function(x, y, width, height, r, strokeWidth) { r = isObject(x) ? x.r : r; var wrapper = this.createElement('rect'), @@ -3767,15 +4685,15 @@ return wrapper.attr(attribs); }, /** - * Resize the box and re-align all aligned elements - * @param {Object} width - * @param {Object} height - * @param {Boolean} animate - * + * Resize the {@link SVGRenderer#box} and re-align all aligned child + * elements. + * @param {number} width The new pixel width. + * @param {number} height The new pixel height. + * @param {boolean} animate Whether to animate. */ setSize: function(width, height, animate) { var renderer = this, alignedObjects = renderer.alignedObjects, i = alignedObjects.length; @@ -3799,28 +4717,33 @@ alignedObjects[i].align(); } }, /** - * Create a group - * @param {String} name The group will be given a class name of 'highcharts-{name}'. - * This can be used for styling and scripting. + * Create and return an svg group element. + * + * @param {string} [name] The group will be given a class name of + * `highcharts-{name}`. This can be used for styling and scripting. + * @returns {SVGElement} The generated wrapper element. */ g: function(name) { var elem = this.createElement('g'); return name ? elem.attr({ 'class': 'highcharts-' + name }) : elem; }, /** - * Display an image - * @param {String} src - * @param {Number} x - * @param {Number} y - * @param {Number} width - * @param {Number} height + * Display an image. + * @param {string} src The image source. + * @param {number} [x] The X position. + * @param {number} [y] The Y position. + * @param {number} [width] The image width. If omitted, it defaults to the + * image file width. + * @param {number} [height] The image height. If omitted it defaults to the + * image file height. + * @returns {SVGElement} The generated wrapper element. */ image: function(src, x, y, width, height) { var attribs = { preserveAspectRatio: 'none' }, @@ -3849,17 +4772,31 @@ } return elemWrapper; }, /** - * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object. + * Draw a symbol out of pre-defined shape paths from {@SVGRenderer#symbols}. + * It is used in Highcharts for point makers, which cake a `symbol` option, + * and label and button backgrounds like in the tooltip and stock flags. * - * @param {Object} symbol - * @param {Object} x - * @param {Object} y - * @param {Object} radius - * @param {Object} options + * @param {Symbol} symbol - The symbol name. + * @param {number} x - The X coordinate for the top left position. + * @param {number} y - The Y coordinate for the top left position. + * @param {number} width - The pixel width. + * @param {number} height - The pixel height. + * @param {Object} [options] - Additional options, depending on the actual + * symbol drawn. + * @param {number} [options.anchorX] - The anchor X position for the + * `callout` symbol. This is where the chevron points to. + * @param {number} [options.anchorY] - The anchor Y position for the + * `callout` symbol. This is where the chevron points to. + * @param {number} [options.end] - The end angle of an `arc` symbol. + * @param {boolean} [options.open] - Whether to draw `arc` symbol open or + * closed. + * @param {number} [options.r] - The radius of an `arc` symbol, or the + * border radius for the `callout` symbol. + * @param {number} [options.start] - The start angle of an `arc` symbol. */ symbol: function(symbol, x, y, width, height, options) { var ren = this, obj, @@ -3875,12 +4812,11 @@ height, options ), imageRegex = /^url\((.*?)\)$/, imageSrc, - centerImage, - symbolSizes = {}; + centerImage; if (symbolFn) { obj = this.path(path); @@ -3907,14 +4843,21 @@ imageSrc = symbol.match(imageRegex)[1]; // Create the image synchronously, add attribs async obj = this.image(imageSrc); - // The image width is not always the same as the symbol width. The image may be centered within the symbol, - // as is the case when image shapes are used as label backgrounds, for example in flags. - obj.imgwidth = pick(symbolSizes[imageSrc] && symbolSizes[imageSrc].width, options && options.width); - obj.imgheight = pick(symbolSizes[imageSrc] && symbolSizes[imageSrc].height, options && options.height); + // The image width is not always the same as the symbol width. The + // image may be centered within the symbol, as is the case when + // image shapes are used as label backgrounds, for example in flags. + obj.imgwidth = pick( + symbolSizes[imageSrc] && symbolSizes[imageSrc].width, + options && options.width + ); + obj.imgheight = pick( + symbolSizes[imageSrc] && symbolSizes[imageSrc].height, + options && options.height + ); /** * Set the size and position */ centerImage = function() { obj.attr({ @@ -3922,24 +4865,26 @@ height: obj.height }); }; /** - * Width and height setters that take both the image's physical size and the label size into - * consideration, and translates the image to center within the label. + * Width and height setters that take both the image's physical size + * and the label size into consideration, and translates the image + * to center within the label. */ each(['width', 'height'], function(key) { obj[key + 'Setter'] = function(value, key) { var attribs = {}, - imgSize = this['img' + key]; + imgSize = this['img' + key], + trans = key === 'width' ? 'translateX' : 'translateY'; this[key] = value; if (defined(imgSize)) { if (this.element) { this.element.setAttribute(key, imgSize); } if (!this.alignByTranslate) { - attribs[key === 'width' ? 'translateX' : 'translateY'] = (this[key] - imgSize) / 2; + attribs[trans] = ((this[key] || 0) - imgSize) / 2; this.attr(attribs); } } }; }); @@ -4010,10 +4955,18 @@ return obj; }, /** + * @typedef {string} Symbol + * + * Can be one of `arc`, `callout`, `circle`, `diamond`, `square`, + * `triangle`, `triangle-down`. Symbols are used internally for point + * markers, button and label borders and backgrounds, or custom shapes. + * Extendable by adding to {@link SVGRenderer#symbols}. + */ + /** * An extendable collection of functions for defining symbol paths. */ symbols: { 'circle': function(x, y, w, h) { var cpw = 0.166 * w; @@ -4120,27 +5073,57 @@ 'L', x + w, y + h - r, // right side 'C', x + w, y + h, x + w, y + h, x + w - r, y + h, // bottom-right corner 'L', x + r, y + h, // bottom side 'C', x, y + h, x, y + h, x, y + h - r, // bottom-left corner 'L', x, y + r, // left side - 'C', x, y, x, y, x + r, y // top-right corner + 'C', x, y, x, y, x + r, y // top-left corner ]; - if (anchorX && anchorX > w && anchorY > y + safeDistance && anchorY < y + h - safeDistance) { // replace right side - path.splice(13, 3, - 'L', x + w, anchorY - halfDistance, - x + w + arrowLength, anchorY, - x + w, anchorY + halfDistance, - x + w, y + h - r - ); - } else if (anchorX && anchorX < 0 && anchorY > y + safeDistance && anchorY < y + h - safeDistance) { // replace left side - path.splice(33, 3, - 'L', x, anchorY + halfDistance, - x - arrowLength, anchorY, - x, anchorY - halfDistance, - x, y + r - ); + // Anchor on right side + if (anchorX && anchorX > w) { + + // Chevron + if (anchorY > y + safeDistance && anchorY < y + h - safeDistance) { + path.splice(13, 3, + 'L', x + w, anchorY - halfDistance, + x + w + arrowLength, anchorY, + x + w, anchorY + halfDistance, + x + w, y + h - r + ); + + // Simple connector + } else { + path.splice(13, 3, + 'L', x + w, h / 2, + anchorX, anchorY, + x + w, h / 2, + x + w, y + h - r + ); + } + + // Anchor on left side + } else if (anchorX && anchorX < 0) { + + // Chevron + if (anchorY > y + safeDistance && anchorY < y + h - safeDistance) { + path.splice(33, 3, + 'L', x, anchorY + halfDistance, + x - arrowLength, anchorY, + x, anchorY - halfDistance, + x, y + r + ); + + // Simple connector + } else { + path.splice(33, 3, + 'L', x, h / 2, + anchorX, anchorY, + x, h / 2, + x, y + r + ); + } + } else if (anchorY && anchorY > h && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace bottom path.splice(23, 3, 'L', anchorX + halfDistance, y + h, anchorX, y + h + arrowLength, anchorX - halfDistance, y + h, @@ -4152,25 +5135,42 @@ anchorX, y - arrowLength, anchorX + halfDistance, y, w - r, y ); } + return path; } }, /** + * @typedef {SVGElement} ClipRect - A clipping rectangle that can be applied + * to one or more {@link SVGElement} instances. It is instanciated with the + * {@link SVGRenderer#clipRect} function and applied with the {@link + * SVGElement#clip} function. + * + * @example + * var circle = renderer.circle(100, 100, 100) + * .attr({ fill: 'red' }) + * .add(); + * var clipRect = renderer.clipRect(100, 100, 100, 100); + * + * // Leave only the lower right quarter visible + * circle.clip(clipRect); + */ + /** * Define a clipping rectangle * @param {String} id - * @param {Number} x - * @param {Number} y - * @param {Number} width - * @param {Number} height + * @param {number} x + * @param {number} y + * @param {number} width + * @param {number} height + * @returns {ClipRect} A clipping rectangle. */ clipRect: function(x, y, width, height) { var wrapper, - id = 'highcharts-' + H.idCounter++, + id = H.uniqueKey(), clipPath = this.createElement('clipPath').attr({ id: id }).add(this.defs); @@ -4187,12 +5187,12 @@ /** * Add text to the SVG object * @param {String} str - * @param {Number} x Left position - * @param {Number} y Top position + * @param {number} x Left position + * @param {number} y Top position * @param {Boolean} useHTML Use HTML to render the text */ text: function(str, x, y, useHTML) { // declare variables @@ -4242,24 +5242,47 @@ return wrapper; }, /** - * Utility to return the baseline offset and total line height from the font size + * Utility to return the baseline offset and total line height from the font + * size. + * + * @param {?string} fontSize The current font size to inspect. If not given, + * the font size will be found from the DOM element. + * @param {SVGElement|SVGDOMElement} [elem] The element to inspect for a + * current font size. + * @returns {Object} An object containing `h`: the line height, `b`: the + * baseline relative to the top of the box, and `f`: the font size. */ - fontMetrics: function(fontSize, elem) { // eslint-disable-line no-unused-vars + fontMetrics: function(fontSize, elem) { var lineHeight, baseline; - fontSize = fontSize || (this.style && this.style.fontSize); + fontSize = fontSize || + // When the elem is a DOM element (#5932) + (elem && elem.style && elem.style.fontSize) || + // Fall back on the renderer style default + (this.style && this.style.fontSize); - fontSize = /px/.test(fontSize) ? pInt(fontSize) : /em/.test(fontSize) ? parseFloat(fontSize) * 12 : 12; - // Empirical values found by comparing font size and bounding box height. - // Applies to the default font family. http://jsfiddle.net/highcharts/7xvn7/ + // Handle different units + if (/px/.test(fontSize)) { + fontSize = pInt(fontSize); + } else if (/em/.test(fontSize)) { + // The em unit depends on parent items + fontSize = parseFloat(fontSize) * + (elem ? this.fontMetrics(null, elem.parentNode).f : 16); + } else { + fontSize = 12; + } + + // Empirical values found by comparing font size and bounding box + // height. Applies to the default font family. + // http://jsfiddle.net/highcharts/7xvn7/ lineHeight = fontSize < 24 ? fontSize + 3 : Math.round(fontSize * 1.2); baseline = Math.round(lineHeight * 0.8); return { h: lineHeight, @@ -4282,18 +5305,20 @@ }; }, /** * Add a label, a text item that can hold a colored or gradient background - * as well as a border and shadow. + * as well as a border and shadow. Supported custom attributes include + * `padding`. + * * @param {string} str - * @param {Number} x - * @param {Number} y + * @param {number} x + * @param {number} y * @param {String} shape - * @param {Number} anchorX In case the shape has a pointer, like a flag, this is the + * @param {number} anchorX In case the shape has a pointer, like a flag, this is the * coordinates it should be pinned to - * @param {Number} anchorY + * @param {number} anchorY * @param {Boolean} baseline Whether to position the label relative to the text baseline, * like renderer.text, or to the upper border of the rectangle. * @param {String} className Class name for the group */ label: function(str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) { @@ -4543,11 +5568,13 @@ // Redirect certain methods to either the box or the text var baseCss = wrapper.css; return extend(wrapper, { /** - * Pick up some properties and apply them to the text instead of the wrapper + * Pick up some properties and apply them to the text instead of the + * wrapper. + * @ignore */ css: function(styles) { if (styles) { var textStyles = {}; styles = merge(styles); // create a copy to avoid altering the original object (#537) @@ -4560,11 +5587,12 @@ text.css(textStyles); } return baseCss.call(wrapper, styles); }, /** - * Return the bounding box of the box, not the group + * Return the bounding box of the box, not the group. + * @ignore */ getBBox: function() { return { width: bBox.width + 2 * padding, height: bBox.height + 2 * padding, @@ -4572,11 +5600,12 @@ y: bBox.y - padding }; }, /** - * Apply the shadow to the box + * Apply the shadow to the box. + * @ignore */ shadow: function(b) { if (b) { updateBoxSize(); if (box) { @@ -4586,10 +5615,11 @@ return wrapper; }, /** * Destroy and release memory. + * @ignore */ destroy: function() { // Added by button implementation removeEvent(wrapper.element, 'mouseenter'); @@ -4636,12 +5666,12 @@ SVGElement = H.SVGElement, SVGRenderer = H.SVGRenderer, win = H.win, wrap = H.wrap; - // extend SvgElement for useHTML option - extend(SVGElement.prototype, { + // Extend SvgElement for useHTML option + extend(SVGElement.prototype, /** @lends SVGElement.prototype */ { /** * Apply CSS to HTML elements. This is used in text within SVG rendering and * by the VML renderer */ htmlCss: function(styles) { @@ -4813,11 +5843,11 @@ this.yCorr = -baseline; } }); // Extend SvgRenderer for useHTML option. - extend(SVGRenderer.prototype, { + extend(SVGRenderer.prototype, /** @lends SVGRenderer.prototype */ { /** * Create HTML text node. This is used by the VML renderer as well as the SVG * renderer through the useHTML option. * * @param {String} str @@ -4830,11 +5860,11 @@ renderer = wrapper.renderer, isSVG = renderer.isSVG, addSetters = function(element, style) { // These properties are set as attributes on the SVG group, and as // identical CSS properties on the div. (#3542) - each(['display', 'opacity', 'visibility'], function(prop) { + each(['opacity', 'visibility'], function(prop) { wrap(element, prop + 'Setter', function(proceed, value, key, elem) { proceed.call(this, value, key, elem); style[key] = value; }); }); @@ -5357,11 +6387,15 @@ if (cutOff) { shadow.cutOff = strokeWidth + 1; } // apply the opacity - markup = ['<stroke color="', shadowOptions.color || '#000000', '" opacity="', shadowElementOpacity * i, '"/>']; + markup = [ + '<stroke color="', + shadowOptions.color || '#000000', + '" opacity="', shadowElementOpacity * i, '"/>' + ]; createElement(renderer.prepVML(markup), null, null, shadow); // insert it if (group) { @@ -5397,11 +6431,11 @@ var strokeElem = element.getElementsByTagName('stroke')[0] || createElement(this.renderer.prepVML(['<stroke/>']), null, null, element); strokeElem[key] = value || 'solid'; this[key] = value; /* because changing stroke-width will change the dash length - and cause an epileptic effect */ + and cause an epileptic effect */ }, dSetter: function(value, key, element) { var i, shadows = this.shadows; value = value || []; @@ -5485,24 +6519,21 @@ } key = 'top'; } element.style[key] = value; }, - displaySetter: function(value, key, element) { - element.style[key] = value; - }, xSetter: function(value, key, element) { this[key] = value; // used in getter if (key === 'x') { key = 'left'; } else if (key === 'y') { key = 'top'; } /* else { - value = Math.max(0, value); // don't set width or height below zero (#311) - }*/ + value = Math.max(0, value); // don't set width or height below zero (#311) + }*/ // clipping rectangle special if (this.updateClipping) { this[key] = value; // the key is now 'left' or 'top' for 'x' and 'y' this.updateClipping(); @@ -5648,11 +6679,14 @@ }, // used in attr and animation to update the clipping of all members updateClipping: function() { each(clipRect.members, function(member) { - if (member.element) { // Deleted series, like in stock/members/series-remove demo. Should be removed from members, but this will do. + // Member.element is falsy on deleted series, like in + // stock/members/series-remove demo. Should be removed + // from members, but this will do. + if (member.element) { member.css(clipRect.getCSS(member)); } }); } }); @@ -5701,12 +6735,14 @@ lastStop, colors = [], addFillNode = function() { // Add the fill subnode. When colors attribute is used, the meanings of opacity and o:opacity2 // are reversed. - markup = ['<fill colors="' + colors.join(',') + '" opacity="', opacity2, '" o:opacity2="', opacity1, - '" type="', fillType, '" ', fillAttr, 'focus="100%" method="any" />' + markup = ['<fill colors="' + colors.join(',') + + '" opacity="', opacity2, '" o:opacity2="', + opacity1, '" type="', fillType, '" ', fillAttr, + 'focus="100%" method="any" />' ]; createElement(renderer.prepVML(markup), null, null, elem); }; // Extend from 0 to 1 @@ -5958,11 +6994,13 @@ /** * For rectangles, VML uses a shape for rect to overcome bugs and rotation problems */ createElement: function(nodeName) { - return nodeName === 'rect' ? this.symbol(nodeName) : SVGRenderer.prototype.createElement.call(this, nodeName); + return nodeName === 'rect' ? + this.symbol(nodeName) : + SVGRenderer.prototype.createElement.call(this, nodeName); }, /** * In the VML renderer, each child of an inverted div (group) is inverted * @param {Object} element @@ -5978,11 +7016,12 @@ left: pInt(parentStyle.width) - (imgStyle ? pInt(imgStyle.top) : 1), top: pInt(parentStyle.height) - (imgStyle ? pInt(imgStyle.left) : 1), rotation: -90 }); - // Recursively invert child elements, needed for nested composite shapes like box plots and error bars. #1680, #1806. + // Recursively invert child elements, needed for nested composite + // shapes like box plots and error bars. #1680, #1806. each(element.childNodes, function(child) { ren.invertChild(child, element); }); }, @@ -6156,11 +7195,11 @@ }, global: { useUTC: true, //timezoneOffset: 0, - VMLRadialGradientURL: 'http://code.highcharts.com@product.cdnpath@/5.0.0/gfx/vml-radial-gradient.png' + VMLRadialGradientURL: 'http://code.highcharts.com/5.0.3/gfx/vml-radial-gradient.png' }, chart: { //animation: true, //alignTicks: false, @@ -6210,11 +7249,10 @@ plotBorderColor: '#cccccc' //plotBorderWidth: 0, //plotShadow: false }, - title: { text: 'Chart title', align: 'center', // floating: false, margin: 15, @@ -6403,12 +7441,15 @@ }; /** - * Set the time methods globally based on the useUTC option. Time method can be either - * local time or UTC (default). + * Set the time methods globally based on the useUTC option. Time method can be + * either local time or UTC (default). It is called internally on initiating + * Highcharts and after running `Highcharts.setOptions`. + * + * @private */ function setTimeMethods() { var globalOptions = H.defaultOptions.global, Date, useUTC = globalOptions.useUTC, @@ -6733,11 +7774,12 @@ path.push( toPath[4], toPath[5], toPath[1], - toPath[2] + toPath[2], + 'z' // #5909 ); } else { // outside the axis area path = null; } @@ -6854,11 +7896,14 @@ dateTimeLabelFormat; // Set the datetime label format. If a higher rank is set for this position, use that. If not, // use the general format. if (axis.isDatetimeAxis && tickPositionInfo) { - dateTimeLabelFormat = options.dateTimeLabelFormats[tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName]; + dateTimeLabelFormat = + options.dateTimeLabelFormats[ + tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName + ]; } // set properties for access in render method tick.isFirst = isFirst; tick.isLast = isLast; @@ -6953,11 +7998,12 @@ goRight = -1; } modifiedSlotWidth = Math.min(slotWidth, modifiedSlotWidth); // #4177 if (modifiedSlotWidth < slotWidth && axis.labelAlign === 'center') { - xy.x += goRight * (slotWidth - modifiedSlotWidth - xCorrection * (slotWidth - Math.min(labelWidth, modifiedSlotWidth))); + xy.x += goRight * (slotWidth - modifiedSlotWidth - xCorrection * + (slotWidth - Math.min(labelWidth, modifiedSlotWidth))); } // If the label width exceeds the available space, set a text width to be // picked up below. Also, if a width has been set before, we need to set a new // one because the reported labelWidth will be limited by the box (#3938). if (labelWidth > modifiedSlotWidth || (axis.autoRotation && (label.styles || {}).width)) { @@ -6988,11 +8034,15 @@ chart = axis.chart, cHeight = (old && chart.oldChartHeight) || chart.chartHeight; return { x: horiz ? - 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), + 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 + ), y: horiz ? cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) : cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB }; @@ -7086,11 +8136,12 @@ show = true, tickmarkOffset = axis.tickmarkOffset, xy = tick.getPosition(horiz, pos, tickmarkOffset, old), x = xy.x, y = xy.y, - reverseCrisp = ((horiz && x === axis.pos + axis.len) || (!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687 + reverseCrisp = ((horiz && x === axis.pos + axis.len) || + (!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687 var gridPrefix = type ? type + 'Grid' : 'grid', gridLineWidth = options[gridPrefix + 'LineWidth'], gridLineColor = options[gridPrefix + 'LineColor'], @@ -7172,11 +8223,12 @@ if ((tick.isFirst && !tick.isLast && !pick(options.showFirstLabel, 1)) || (tick.isLast && !tick.isFirst && !pick(options.showLastLabel, 1))) { show = false; // Handle label overflow and show or hide accordingly - } else if (horiz && !axis.isRadial && !labelOptions.step && !labelOptions.rotation && !old && opacity !== 0) { + } else if (horiz && !axis.isRadial && !labelOptions.step && + !labelOptions.rotation && !old && opacity !== 0) { tick.handleOverflow(xy); } // apply step if (step && index % step) { @@ -7210,10 +8262,11 @@ * (c) 2010-2016 Torstein Honsi * * License: www.highcharts.com/license */ 'use strict'; + var addEvent = H.addEvent, animObject = H.animObject, arrayMax = H.arrayMax, arrayMin = H.arrayMin, AxisPlotLineOrBandExtension = H.AxisPlotLineOrBandExtension, @@ -7240,12 +8293,14 @@ PlotLineOrBand = H.PlotLineOrBand, removeEvent = H.removeEvent, splat = H.splat, syncTimeout = H.syncTimeout, Tick = H.Tick; + /** - * Create a new axis object + * Create a new axis object. + * @constructor Axis * @param {Object} chart * @param {Object} options */ H.Axis = function() { this.init.apply(this, arguments); @@ -7386,11 +8441,11 @@ style: { fontSize: '11px', fontWeight: 'bold', color: '#000000', - textShadow: '1px 1px contrast, -1px -1px contrast, -1px 1px contrast, 1px -1px contrast' + textOutline: '1px contrast' } }, gridLineWidth: 1, @@ -7635,11 +8690,13 @@ defaultLabelFormatter: function() { var axis = this.axis, value = this.value, categories = axis.categories, dateTimeLabelFormat = this.dateTimeLabelFormat, - numericSymbols = defaultOptions.lang.numericSymbols, + lang = defaultOptions.lang, + numericSymbols = lang.numericSymbols, + numSymMagnitude = lang.numericSymbolMagnitude || 1000, i = numericSymbols && numericSymbols.length, multi, ret, formatOption = axis.options.labels.format, @@ -7658,11 +8715,11 @@ } else if (i && numericSymbolDetector >= 1000) { // Decide whether we should add a numeric symbol like k (thousands) or M (millions). // If we are to enable this in tooltip or other places as well, we can move this // logic to the numberFormatter and enable it by a parameter. while (i-- && ret === undefined) { - multi = Math.pow(1000, i + 1); + multi = Math.pow(numSymMagnitude, i + 1); if (numericSymbolDetector >= multi && (value * 10) % multi === 0 && numericSymbols[i] !== null && value !== 0) { // #5480 ret = H.numberFormat(value / multi, -1) + numericSymbols[i]; } } } @@ -7805,15 +8862,13 @@ // From value to pixels } else { if (doPostTranslate) { // log and ordinal axes val = axis.val2lin(val); } - if (pointPlacement === 'between') { - pointPlacement = 0.5; - } - returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * minPixelPadding) + - (isNumber(pointPlacement) ? localA * pointPlacement * axis.pointRange : 0); + returnValue = sign * (val - localMin) * localA + cvsOffset + + (sign * minPixelPadding) + + (isNumber(pointPlacement) ? localA * pointPlacement : 0); } return returnValue; }, @@ -7965,11 +9020,17 @@ max, options.startOfWeek ) ); } else { - for (pos = min + (tickPositions[0] - min) % minorTickInterval; pos <= max; pos += minorTickInterval) { + for ( + pos = min + (tickPositions[0] - min) % minorTickInterval; pos <= max; pos += minorTickInterval + ) { + // Very, very, tight grid lines (#5771) + if (pos === minorTickPositions[0]) { + break; + } minorTickPositions.push(pos); } } } @@ -8065,12 +9126,18 @@ if (this.categories) { ret = 1; } else { each(this.series, function(series) { - var seriesClosest = series.closestPointRange; - if (!series.noSharedTooltip && defined(seriesClosest)) { + var seriesClosest = series.closestPointRange, + visible = series.visible || + !series.chart.options.chart.ignoreHiddenSeries; + + if (!series.noSharedTooltip && + defined(seriesClosest) && + visible + ) { ret = defined(ret) ? Math.min(ret, seriesClosest) : seriesClosest; } }); @@ -8089,11 +9156,11 @@ x; point.series.requireSorting = false; if (!defined(nameX)) { - nameX = this.options.nameToX === false ? + nameX = this.options.uniqueNames === false ? point.series.autoIncrement() : inArray(point.name, names); } if (nameX === -1) { // The name is not found in currenct categories if (!explicitCategories) { @@ -8118,12 +9185,15 @@ if (this.names.length > 0) { this.names.length = 0; this.minRange = undefined; each(this.series || [], function(series) { + // Reset incrementer (#5928) + series.xIncrement = null; + // When adding a series, points are not yet generated - if (!series.processedXData) { + if (!series.points || series.isDirtyData) { series.processData(); series.generatePoints(); } each(series.points, function(point, i) { @@ -8332,16 +9402,20 @@ axis.max += length * maxPadding; } } } - // Stay within floor and ceiling + // Handle options for floor, ceiling, softMin and softMax if (isNumber(options.floor)) { axis.min = Math.max(axis.min, options.floor); + } else if (isNumber(options.softMin)) { + axis.min = Math.min(axis.min, options.softMin); } if (isNumber(options.ceiling)) { axis.max = Math.min(axis.max, options.ceiling); + } else if (isNumber(options.softMax)) { + axis.max = Math.max(axis.max, options.softMax); } // When the threshold is soft, adjust the extreme value only if // the data extreme and the padded extreme land on either side of the threshold. For example, // a series of [0, 1, 2, 3] would make the yAxis add a tick for -1 because of the @@ -8744,32 +9818,36 @@ dataMax = this.dataMax, options = this.options, min = Math.min(dataMin, pick(options.min, dataMin)), max = Math.max(dataMax, pick(options.max, dataMax)); - // Prevent pinch zooming out of range. Check for defined is for #1946. #1734. - if (!this.allowZoomOutside) { - if (defined(dataMin) && newMin <= min) { - newMin = min; + if (newMin !== this.min || newMax !== this.max) { // #5790 + + // Prevent pinch zooming out of range. Check for defined is for #1946. #1734. + if (!this.allowZoomOutside) { + if (defined(dataMin) && newMin <= min) { + newMin = min; + } + if (defined(dataMax) && newMax >= max) { + newMax = max; + } } - if (defined(dataMax) && newMax >= max) { - newMax = max; - } - } - // In full view, displaying the reset zoom button is not required - this.displayBtn = newMin !== undefined || newMax !== undefined; + // In full view, displaying the reset zoom button is not required + this.displayBtn = newMin !== undefined || newMax !== undefined; - // Do it - this.setExtremes( - newMin, - newMax, - false, - undefined, { - trigger: 'zoom' - } - ); + // Do it + this.setExtremes( + newMin, + newMax, + false, + undefined, { + trigger: 'zoom' + } + ); + } + return true; }, /** * Update the axis metrics @@ -9323,11 +10401,10 @@ ], lineWidth); }, /** * Render the axis line - * @returns {[type]} [description] */ renderLine: function() { if (!this.axisLine) { this.axisLine = this.chart.renderer.path() .addClass('highcharts-axis-line') @@ -9587,21 +10664,24 @@ series.isDirty = true; }); }, + // Properties to survive after destroy, needed for Axis.update (#4317, + // #5773, #5881). + keepProps: ['extKey', 'hcEvents', 'names', 'series', 'userMax', 'userMin'], + /** * Destroys an Axis instance. */ destroy: function(keepEvents) { var axis = this, stacks = axis.stacks, stackKey, plotLinesAndBands = axis.plotLinesAndBands, i, - n, - keepProps; + n; // Remove the events if (!keepEvents) { removeEvent(axis); } @@ -9629,16 +10709,13 @@ if (axis[prop]) { axis[prop] = axis[prop].destroy(); } }); - // Delete all properties and fall back to the prototype. - // Preserve some properties, needed for Axis.update (#4317). - keepProps = ['names', 'series', 'userMax', 'userMin']; for (n in axis) { - if (axis.hasOwnProperty(n) && inArray(n, keepProps) === -1) { + if (axis.hasOwnProperty(n) && inArray(n, axis.keepProps) === -1) { delete axis[n]; } } }, @@ -9650,10 +10727,11 @@ */ drawCrosshair: function(e, point) { var path, options = this.crosshair, + snap = pick(options.snap, true), pos, categorized, graphic = this.cross; // Use last available event when updating non-snapped crosshairs without @@ -9664,29 +10742,34 @@ if ( // Disabled in options !this.crosshair || // Snap - ((defined(point) || !pick(options.snap, true)) === false) + ((defined(point) || !snap) === false) ) { this.hideCrosshair(); } else { // Get the path - if (!pick(options.snap, true)) { - pos = (this.horiz ? e.chartX - this.pos : this.len - e.chartY + this.pos); + if (!snap) { + pos = e && (this.horiz ? e.chartX - this.pos : this.len - e.chartY + this.pos); } else if (defined(point)) { pos = this.isXAxis ? point.plotX : this.len - point.plotY; // #3834 } - if (this.isRadial) { - path = this.getPlotLinePath(this.isXAxis ? point.x : pick(point.stackY, point.y)) || null; // #3189 - } else { - path = this.getPlotLinePath(null, null, null, null, pos) || null; // #3189 + if (defined(pos)) { + path = this.getPlotLinePath( + // First argument, value, only used on radial + point && (this.isXAxis ? point.x : pick(point.stackY, point.y)), + null, + null, + null, + pos // Translated position + ) || null; // #3189 } - if (path === null) { + if (!defined(path)) { this.hideCrosshair(); return; } categorized = this.categories && !this.isRadial; @@ -9719,18 +10802,17 @@ graphic.show().attr({ d: path }); - if (categorized) { + if (categorized && !options.width) { graphic.attr({ 'stroke-width': this.transA }); } this.cross.e = e; } - }, /** * Hide the crosshair. */ @@ -9751,17 +10833,17 @@ * License: www.highcharts.com/license */ 'use strict'; var Axis = H.Axis, Date = H.Date, + dateFormat = H.dateFormat, defaultOptions = H.defaultOptions, defined = H.defined, each = H.each, extend = H.extend, getMagnitude = H.getMagnitude, getTZOffset = H.getTZOffset, - grep = H.grep, normalizeTickInterval = H.normalizeTickInterval, pick = H.pick, timeUnits = H.timeUnits; /** * Set the tick positions to a time unit that makes sense, for example @@ -9781,11 +10863,12 @@ useUTC = defaultOptions.global.useUTC, minYear, // used in months and years as a basis for Date.UTC() minDate = new Date(min - getTZOffset(min)), makeTime = Date.hcMakeTime, interval = normalizedInterval.unitRange, - count = normalizedInterval.count; + count = normalizedInterval.count, + variableDayLength; if (defined(min)) { // #1300 minDate[Date.hcSetMilliseconds](interval >= timeUnits.second ? 0 : // #3935 count * Math.floor(minDate.getMilliseconds() / count)); // #3652, #3654 @@ -9826,26 +10909,44 @@ minDate[Date.hcSetDate](minDate[Date.hcGetDate]() - minDate[Date.hcGetDay]() + pick(startOfWeek, 1)); } - // get tick positions - i = 1; + // Get basics for variable time spans + minYear = minDate[Date.hcGetFullYear](); + var minMonth = minDate[Date.hcGetMonth](), + minDateDate = minDate[Date.hcGetDate](), + minHours = minDate[Date.hcGetHours](); + + + // Handle local timezone offset if (Date.hcTimezoneOffset || Date.hcGetTimezoneOffset) { + + // Detect whether we need to take the DST crossover into + // consideration. If we're crossing over DST, the day length may be + // 23h or 25h and we need to compute the exact clock time for each + // tick instead of just adding hours. This comes at a cost, so first + // we found out if it is needed. #4951. + variableDayLength = + (!useUTC || !!Date.hcGetTimezoneOffset) && + ( + // Long range, assume we're crossing over. + max - min > 4 * timeUnits.month || + // Short range, check if min and max are in different time + // zones. + getTZOffset(min) !== getTZOffset(max) + ); + + // Adjust minDate to the offset date minDate = minDate.getTime(); minDate = new Date(minDate + getTZOffset(minDate)); } - minYear = minDate[Date.hcGetFullYear](); - var time = minDate.getTime(), - minMonth = minDate[Date.hcGetMonth](), - minDateDate = minDate[Date.hcGetDate](), - variableDayLength = !useUTC || !!Date.hcGetTimezoneOffset, // #4951 - localTimezoneOffset = (timeUnits.day + - (useUTC ? getTZOffset(minDate) : minDate.getTimezoneOffset() * 60 * 1000) - ) % timeUnits.day; // #950, #3359 - // iterate and add tick positions at appropriate values + + // Iterate and add tick positions at appropriate values + var time = minDate.getTime(); + i = 1; while (time < max) { tickPositions.push(time); // if the interval is years, use Date.UTC to increase years if (interval === timeUnits.year) { @@ -9859,10 +10960,13 @@ // one hour at the DST crossover } else if (variableDayLength && (interval === timeUnits.day || interval === timeUnits.week)) { time = makeTime(minYear, minMonth, minDateDate + i * count * (interval === timeUnits.day ? 1 : 7)); + } else if (variableDayLength && interval === timeUnits.hour) { + time = makeTime(minYear, minMonth, minDateDate, minHours + i * count); + // else, the interval is fixed and we use simple addition } else { time += interval * count; } @@ -9871,16 +10975,19 @@ // push the last time tickPositions.push(time); - // mark new days if the time is dividible by day (#1649, #1760) - each(grep(tickPositions, function(time) { - return interval <= timeUnits.hour && time % timeUnits.day === localTimezoneOffset; - }), function(time) { - higherRanks[time] = 'day'; - }); + // Handle higher ranks. Mark new days if the time is on midnight + // (#950, #1649, #1760, #3349) + if (interval <= timeUnits.hour) { + each(tickPositions, function(time) { + if (dateFormat('%H%M%S%L', time) === '000000000') { + higherRanks[time] = 'day'; + } + }); + } } // record information on the chosen unit - for dynamic label formatter tickPositions.info = extend(normalizedInterval, { @@ -10102,12 +11209,11 @@ * (c) 2010-2016 Torstein Honsi * * License: www.highcharts.com/license */ 'use strict'; - var addEvent = H.addEvent, - dateFormat = H.dateFormat, + var dateFormat = H.dateFormat, each = H.each, extend = H.extend, format = H.format, isNumber = H.isNumber, map = H.map, @@ -10153,47 +11259,85 @@ // Public property for getting the shared state. this.split = options.split && !chart.inverted; this.shared = options.shared || this.split; + }, - // Create the label - if (this.split) { - this.label = this.chart.renderer.g('tooltip'); - } else { - this.label = chart.renderer.label( - '', - 0, - 0, - options.shape || 'callout', - null, - null, - options.useHTML, - null, - 'tooltip' - ) - .attr({ - padding: options.padding, - r: options.borderRadius, - display: 'none' // #2301, #2657, #3532, #5570 - }); + /** + * Destroy the single tooltips in a split tooltip. + * If the tooltip is active then it is not destroyed, unless forced to. + * @param {boolean} force Force destroy all tooltips. + * @return {undefined} + */ + cleanSplit: function(force) { + each(this.chart.series, function(series) { + var tt = series && series.tt; + if (tt) { + if (!tt.isActive || force) { + series.tt = tt.destroy(); + } else { + tt.isActive = false; + } + } + }); + }, + + + /** + * Create the Tooltip label element if it doesn't exist, then return the + * label. + */ + getLabel: function() { + + var renderer = this.chart.renderer, + options = this.options; + + if (!this.label) { + // Create the label + if (this.split) { + this.label = renderer.g('tooltip'); + } else { + this.label = renderer.label( + '', + 0, + 0, + options.shape || 'callout', + null, + null, + options.useHTML, + null, + 'tooltip' + ) + .attr({ + padding: options.padding, + r: options.borderRadius + }); + + + this.label + .attr({ + 'fill': options.backgroundColor, + 'stroke-width': options.borderWidth + }) + // #2301, #2657 + .css(options.style) + .shadow(options.shadow); + + } + + + this.label .attr({ - 'fill': options.backgroundColor, - 'stroke-width': options.borderWidth + zIndex: 8 }) - // #2301, #2657 - .css(options.style) - .shadow(options.shadow); - + .add(); } - this.label.attr({ - zIndex: 8 - }) - .add(); + return this.label; }, update: function(options) { this.destroy(); this.init(this.chart, merge(true, this.options, options)); @@ -10205,10 +11349,14 @@ destroy: function() { // Destroy and clear local variables if (this.label) { this.label = this.label.destroy(); } + if (this.split && this.tt) { + this.cleanSplit(this.chart, true); + this.tt = this.tt.destroy(); + } clearTimeout(this.hideTimer); clearTimeout(this.tooltipTimeout); }, /** @@ -10233,22 +11381,23 @@ anchorX: skipAnchor ? undefined : animate ? (2 * now.anchorX + anchorX) / 3 : anchorX, anchorY: skipAnchor ? undefined : animate ? (now.anchorY + anchorY) / 2 : anchorY }); // Move to the intermediate value - tooltip.label.attr(now); + tooltip.getLabel().attr(now); // Run on next tick of the mouse tracker if (animate) { // Never allow two timeouts clearTimeout(this.tooltipTimeout); // Set the fixed interval ticking for the smooth tooltip this.tooltipTimeout = setTimeout(function() { - // The interval function may still be running during destroy, so check that the chart is really there before calling. + // The interval function may still be running during destroy, + // so check that the chart is really there before calling. if (tooltip) { tooltip.move(x, y, anchorX, anchorY); } }, 32); @@ -10262,11 +11411,11 @@ var tooltip = this; clearTimeout(this.hideTimer); // disallow duplicate timers (#1728, #1766) delay = pick(delay, this.options.hideDelay, 500); if (!this.isHidden) { this.hideTimer = syncTimeout(function() { - tooltip.label[delay ? 'fadeOut' : 'hide'](); + tooltip.getLabel()[delay ? 'fadeOut' : 'hide'](); tooltip.isHidden = true; }, delay); } }, @@ -10333,12 +11482,18 @@ var chart = this.chart, distance = this.distance, ret = {}, h = point.h || 0, // #4117 swapped, - first = ['y', chart.chartHeight, boxHeight, point.plotY + chart.plotTop, chart.plotTop, chart.plotTop + chart.plotHeight], - second = ['x', chart.chartWidth, boxWidth, point.plotX + chart.plotLeft, chart.plotLeft, chart.plotLeft + chart.plotWidth], + first = ['y', chart.chartHeight, boxHeight, + point.plotY + chart.plotTop, chart.plotTop, + chart.plotTop + chart.plotHeight + ], + second = ['x', chart.chartWidth, boxWidth, + point.plotX + chart.plotLeft, chart.plotLeft, + chart.plotLeft + chart.plotWidth + ], // The far side is right or bottom preferFarSide = !this.followPointer && pick(point.ttBelow, !chart.inverted === !!point.negative), // #4984 /** * Handle the preferred dimension. When the preferred dimension is tooltip * on top or bottom of the point, it will look for space there. @@ -10354,11 +11509,16 @@ } else if (!preferFarSide && roomLeft) { ret[dim] = alignedLeft; } else if (roomLeft) { ret[dim] = Math.min(max - innerSize, alignedLeft - h < 0 ? alignedLeft : alignedLeft - h); } else if (roomRight) { - ret[dim] = Math.max(min, alignedRight + h + innerSize > outerSize ? alignedRight : alignedRight + h); + ret[dim] = Math.max( + min, + alignedRight + h + innerSize > outerSize ? + alignedRight : + alignedRight + h + ); } else { return false; } }, /** @@ -10426,18 +11586,18 @@ */ defaultFormatter: function(tooltip) { var items = this.points || splat(this), s; - // build the header - s = [tooltip.tooltipFooterHeaderFormatter(items[0])]; //#3397: abstraction to enable formatting of footer and header + // Build the header + s = [tooltip.tooltipFooterHeaderFormatter(items[0])]; // build the values s = s.concat(tooltip.bodyFormatter(items)); // footer - s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true)); //#3397: abstraction to enable formatting of footer and header + s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true)); return s; }, /** @@ -10445,11 +11605,11 @@ * @param {Object} point */ refresh: function(point, mouseEvent) { var tooltip = this, chart = tooltip.chart, - label = tooltip.label, + label, options = tooltip.options, x, y, anchor, textConfig = {}, @@ -10507,25 +11667,26 @@ // update the inner HTML if (text === false) { this.hide(); } else { + label = tooltip.getLabel(); + // show it if (tooltip.isHidden) { stop(label); label.attr({ - opacity: 1, - display: 'block' + opacity: 1 }).show(); } // update text if (tooltip.split) { this.renderSplit(text, chart.hoverPoints); } else { label.attr({ - text: text.join ? text.join('') : text + text: text && text.join ? text.join('') : text }); // Set the stroke color of the box to reflect the point label.removeClass(/highcharts-color-[\d]+/g) .addClass('highcharts-color-' + pick(point.colorIndex, currentSeries.colorIndex)); @@ -10559,20 +11720,13 @@ boxes = [], chart = this.chart, ren = chart.renderer, rightAligned = true, options = this.options, - headerHeight; + headerHeight, + tooltipLabel = this.getLabel(); - /** - * Destroy a single-series tooltip - */ - function destroy(tt) { - tt.connector = tt.connector.destroy(); - tt.destroy(); - } - // Create the individual labels each(labels.slice(0, labels.length - 1), function(str, i) { var point = points[i - 1] || // Item 0 is the header. Instead of this, we could also use the crosshair label { @@ -10583,57 +11737,52 @@ tt = owner.tt, series = point.series || {}, colorClass = 'highcharts-color-' + pick(point.colorIndex, series.colorIndex, 'none'), target, x, - bBox; + bBox, + boxWidth; // Store the tooltip referance on the series if (!tt) { - owner.tt = tt = ren.label(null, null, null, point.isHeader && 'callout') + owner.tt = tt = ren.label(null, null, null, 'callout') .addClass('highcharts-tooltip-box ' + colorClass) .attr({ 'padding': options.padding, 'r': options.borderRadius, 'fill': options.backgroundColor, 'stroke': point.color || series.color || '#333333', 'stroke-width': options.borderWidth }) - .add(tooltip.label); - - // Add a connector back to the point - if (point.series) { - tt.connector = ren.path() - .addClass('highcharts-tooltip-connector ' + colorClass) - - .attr({ - 'stroke-width': series.options.lineWidth || 2, - 'stroke': point.color || series.color || '#666666' - }) - - .add(tooltip.label); - - addEvent(point.series, 'hide', function() { - this.tt = destroy(this.tt); - }); - } + .add(tooltipLabel); } + tt.isActive = true; tt.attr({ text: str }); + tt.css(options.style); + + // Get X position now, so we can move all to the other side in case of overflow bBox = tt.getBBox(); + boxWidth = bBox.width + tt.strokeWidth(); if (point.isHeader) { headerHeight = bBox.height; - x = point.plotX + chart.plotLeft - bBox.width / 2; + x = Math.max( + 0, // No left overflow + Math.min( + point.plotX + chart.plotLeft - boxWidth / 2, + chart.chartWidth - boxWidth // No right overflow (#5794) + ) + ); } else { x = point.plotX + chart.plotLeft - pick(options.distance, 16) - - bBox.width; + boxWidth; } // If overflow left, we don't use this x in the next loop if (x < 0) { @@ -10652,64 +11801,37 @@ tt: tt }); }); // Clean previous run (for missing points) - each(chart.series, function(series) { - var tt = series.tt; - if (tt) { - if (!tt.isActive) { - series.tt = destroy(tt); - } else { - tt.isActive = false; - } - } - }); + this.cleanSplit(); // Distribute and put in place H.distribute(boxes, chart.plotHeight + headerHeight); each(boxes, function(box) { - var point = box.point, - tt = box.tt, - attr; + var point = box.point; // Put the label in place - attr = { - display: box.pos === undefined ? 'none' : '', - x: (rightAligned || point.isHeader ? box.x : point.plotX + chart.plotLeft + pick(options.distance, 16)), - y: box.pos + chart.plotTop - }; - if (point.isHeader) { - attr.anchorX = point.plotX + chart.plotLeft; - attr.anchorY = attr.y - 100; - } - tt.attr(attr); - - // Draw the connector to the point - if (!point.isHeader) { - tt.connector.attr({ - d: [ - 'M', - point.plotX + chart.plotLeft, - point.plotY + point.series.yAxis.pos, - 'L', - rightAligned ? - point.plotX + chart.plotLeft - pick(options.distance, 16) : - point.plotX + chart.plotLeft + pick(options.distance, 16), - box.pos + chart.plotTop + tt.getBBox().height / 2 - ] - }); - } + box.tt.attr({ + visibility: box.pos === undefined ? 'hidden' : 'inherit', + x: (rightAligned || point.isHeader ? + box.x : + point.plotX + chart.plotLeft + pick(options.distance, 16)), + y: box.pos + chart.plotTop, + anchorX: point.plotX + chart.plotLeft, + anchorY: point.isHeader ? + box.pos + chart.plotTop - 15 : point.plotY + chart.plotTop + }); }); }, /** * Find the new position and perform the move */ updatePosition: function(point) { var chart = this.chart, - label = this.label, + label = this.getLabel(), pos = (this.options.positioner || this.getPosition).call( this, label.width, label.height, point @@ -10816,11 +11938,12 @@ * abstracting this functionality allows to easily overwrite and extend it. */ bodyFormatter: function(items) { return map(items, function(item) { var tooltipOptions = item.series.tooltipOptions; - return (tooltipOptions.pointFormatter || item.point.tooltipFormatter).call(item.point, tooltipOptions.pointFormat); + return (tooltipOptions.pointFormatter || item.point.tooltipFormatter) + .call(item.point, tooltipOptions.pointFormat); }); } }; @@ -10847,16 +11970,16 @@ removeEvent = H.removeEvent, splat = H.splat, Tooltip = H.Tooltip, win = H.win; - // Global flag for touch support - H.hasTouch = doc && doc.documentElement.ontouchstart !== undefined; - /** - * The mouse tracker object. All methods starting with "on" are primary DOM event handlers. - * Subsequent methods should be named differently from what they are doing. + * The mouse tracker object. All methods starting with "on" are primary DOM + * event handlers. Subsequent methods should be named differently from what they + * are doing. + * + * @constructor Pointer * @param {Object} chart The Chart instance * @param {Object} options The root options object */ H.Pointer = function(chart, options) { this.init(chart, options); @@ -10885,21 +12008,28 @@ this.setDOMEvents(); }, /** - * Resolve the zoomType option + * Resolve the zoomType option, this is reset on all touch start and mouse + * down events. */ - zoomOption: function() { + zoomOption: function(e) { var chart = this.chart, - zoomType = chart.options.chart.zoomType, - zoomX = /x/.test(zoomType), - zoomY = /y/.test(zoomType), - inverted = chart.inverted; + options = chart.options.chart, + zoomType = options.zoomType || '', + inverted = chart.inverted, + zoomX, + zoomY; - this.zoomX = zoomX; - this.zoomY = zoomY; + // Look for the pinchType option + if (/touch/.test(e.type)) { + zoomType = pick(options.pinchType, zoomType); + } + + this.zoomX = zoomX = /x/.test(zoomType); + this.zoomY = zoomY = /y/.test(zoomType); this.zoomHor = (zoomX && !inverted) || (zoomY && inverted); this.zoomVert = (zoomY && !inverted) || (zoomX && inverted); this.hasZoom = zoomX || zoomY; }, @@ -11024,46 +12154,49 @@ // Sort kdpoints by distance to mouse pointer kdpoints.sort(function(p1, p2) { var isCloserX = p1.distX - p2.distX, isCloser = p1.dist - p2.dist, - isAbove = p1.series.group.zIndex > p2.series.group.zIndex ? -1 : 1; + isAbove = p2.series.group.zIndex - p1.series.group.zIndex; + // We have two points which are not in the same place on xAxis and shared tooltip: - if (isCloserX !== 0) { + if (isCloserX !== 0 && shared) { // #5721 return isCloserX; } // Points are not exactly in the same place on x/yAxis: if (isCloser !== 0) { return isCloser; } // The same xAxis and yAxis position, sort by z-index: - return isAbove; + if (isAbove !== 0) { + return isAbove; + } + + // The same zIndex, sort by array index: + return p1.series.index > p2.series.index ? -1 : 1; }); } // Remove points with different x-positions, required for shared tooltip and crosshairs (#4645): if (shared) { i = kdpoints.length; while (i--) { - if (kdpoints[i].clientX !== kdpoints[0].clientX || kdpoints[i].series.noSharedTooltip) { + if (kdpoints[i].x !== kdpoints[0].x || kdpoints[i].series.noSharedTooltip) { kdpoints.splice(i, 1); } } } // Refresh tooltip for kdpoint if new hover point or tooltip was hidden // #3926, #4200 - if (kdpoints[0] && (kdpoints[0] !== pointer.hoverPoint || (tooltip && tooltip.isHidden))) { + if (kdpoints[0] && (kdpoints[0] !== this.prevKDPoint || (tooltip && tooltip.isHidden))) { // Draw tooltip if necessary if (shared && !kdpoints[0].series.noSharedTooltip) { - // Do mouseover on all points (#3919, #3985, #4410) - for (i = 0; i >= 0; i--) { + // Do mouseover on all points (#3919, #3985, #4410, #5622) + for (i = 0; i < kdpoints.length; i++) { kdpoints[i].onMouseOver(e, kdpoints[i] !== ((hoverSeries && hoverSeries.directTouch && hoverPoint) || kdpoints[0])); } - // Make sure that the hoverPoint and hoverSeries are stored for events (e.g. click), #5622 - if (hoverSeries && hoverSeries.directTouch && hoverPoint && hoverPoint !== kdpoints[0]) { - hoverPoint.onMouseOver(e, false); - } + if (kdpoints.length && tooltip) { // Keep the order of series in tooltip: tooltip.refresh(kdpoints.sort(function(p1, p2) { return p1.series.index - p2.series.index; }), e); @@ -11074,11 +12207,11 @@ } if (!hoverSeries || !hoverSeries.directTouch) { // #4448 kdpoints[0].onMouseOver(e); } } - pointer.prevKDPoint = kdpoints[0]; + this.prevKDPoint = kdpoints[0]; updatePosition = false; } // Update positions (regardless of kdpoint or hoverPoint) if (updatePosition) { followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer; @@ -11090,17 +12223,16 @@ }); } } // Start the event listener to pick up the tooltip and crosshairs - if (!pointer._onDocumentMouseMove) { - pointer._onDocumentMouseMove = function(e) { + if (!pointer.unDocMouseMove) { + pointer.unDocMouseMove = addEvent(doc, 'mousemove', function(e) { if (charts[H.hoverChartIndex]) { charts[H.hoverChartIndex].pointer.onDocumentMouseMove(e); } - }; - addEvent(doc, 'mousemove', pointer._onDocumentMouseMove); + }); } // Crosshair. For each hover point, loop over axes and draw cross if that point // belongs to the axis (#4927). each(shared ? kdpoints : [pick(hoverPoint, kdpoints[0])], function drawPointCrosshair(point) { // #5269 @@ -11169,13 +12301,12 @@ if (tooltip) { tooltip.hide(delay); } - if (pointer._onDocumentMouseMove) { - removeEvent(doc, 'mousemove', pointer._onDocumentMouseMove); - pointer._onDocumentMouseMove = null; + if (pointer.unDocMouseMove) { + pointer.unDocMouseMove = pointer.unDocMouseMove(); } // Remove crosshairs each(chart.axes, function(axis) { axis.hideCrosshair(); @@ -11194,11 +12325,11 @@ seriesAttribs; // Scale each series each(chart.series, function(series) { seriesAttribs = attribs || series.getPlotBox(); // #1701 - if (series.xAxis && series.xAxis.zoomEnabled) { + if (series.xAxis && series.xAxis.zoomEnabled && series.group) { series.group.attr(seriesAttribs); if (series.markerGroup) { series.markerGroup.attr(seriesAttribs); series.markerGroup.clip(clip ? chart.clipRect : null); } @@ -11394,11 +12525,11 @@ onContainerMouseDown: function(e) { e = this.normalize(e); - this.zoomOption(); + this.zoomOption(e); // issue #295, dragging not always working in Firefox if (e.preventDefault) { e.preventDefault(); } @@ -11490,11 +12621,14 @@ var series = this.chart.hoverSeries, relatedTarget = e.relatedTarget || e.toElement; if (series && relatedTarget && !series.options.stickyTracking && !this.inClass(relatedTarget, 'highcharts-tooltip') && - !this.inClass(relatedTarget, 'highcharts-series-' + series.index)) { // #2499, #4465 + (!this.inClass(relatedTarget, 'highcharts-series-' + series.index) || // #2499, #4465 + !this.inClass(relatedTarget, 'highcharts-tracker') // #5553 + ) + ) { series.onMouseOut(); } }, onContainerClick: function(e) { @@ -11607,28 +12741,29 @@ noop = H.noop, pick = H.pick, Pointer = H.Pointer; /* Support for touch devices */ - extend(Pointer.prototype, { + extend(Pointer.prototype, /** @lends Pointer.prototype */ { /** * Run translation operations */ pinchTranslate: function(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) { - if (this.zoomHor || this.pinchHor) { + if (this.zoomHor) { this.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); } - if (this.zoomVert || this.pinchVert) { + if (this.zoomVert) { this.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); } }, /** * Run translation operations for each direction (horizontal and vertical) independently */ - pinchTranslateDirection: function(horiz, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch, forcedScale) { + pinchTranslateDirection: function(horiz, pinchDown, touches, transform, + selectionMarker, clip, lastValidTouch, forcedScale) { var chart = this.chart, xy = horiz ? 'x' : 'y', XY = horiz ? 'X' : 'Y', sChartXY = 'chart' + XY, wh = horiz ? 'width' : 'height', @@ -11646,11 +12781,12 @@ touch1Now = !singleTouch && touches[1][sChartXY], outOfBounds, transformScale, scaleKey, setScale = function() { - if (!singleTouch && Math.abs(touch0Start - touch1Start) > 20) { // Don't zoom if fingers are too close on this axis + // Don't zoom if fingers are too close on this axis + if (!singleTouch && Math.abs(touch0Start - touch1Start) > 20) { scale = forcedScale || Math.abs(touch0Now - touch1Now) / Math.abs(touch0Start - touch1Start); } clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start; selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale; @@ -11761,10 +12897,14 @@ bounds.max = Math.max(axis.pos + axis.len, absMax + minPixelPadding); } }); self.res = true; // reset on next move + // Optionally move the tooltip on touchmove + } else if (self.followTouchMove && touchesLength === 1) { + this.runPointActions(self.normalize(e)); + // Event type is touchmove, handle panning and pinching } else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first // Set the marker @@ -11780,14 +12920,11 @@ self.hasPinched = hasZoom; // Scale and translate the groups to provide visual feedback during pinching self.scaleGroups(transform, clip); - // Optionally move the tooltip on touchmove - if (!hasZoom && self.followTouchMove && touchesLength === 1) { - this.runPointActions(self.normalize(e)); - } else if (self.res) { + if (self.res) { self.res = false; this.reset(false, 0); } } }, @@ -11796,19 +12933,24 @@ * General touch handler shared by touchstart and touchmove. */ touch: function(e, start) { var chart = this.chart, hasMoved, - pinchDown; + pinchDown, + isInside; H.hoverChartIndex = chart.index; if (e.touches.length === 1) { e = this.normalize(e); - if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop) && !chart.openMenu) { + isInside = chart.isInsidePlot( + e.chartX - chart.plotLeft, + e.chartY - chart.plotTop + ); + if (isInside && !chart.openMenu) { // Run mouse events and display tooltip etc if (start) { this.runPointActions(e); } @@ -11839,11 +12981,11 @@ this.pinch(e); } }, onContainerTouchStart: function(e) { - this.zoomOption(); + this.zoomOption(e); this.touch(e, true); }, onContainerTouchMove: function(e) { this.touch(e); @@ -11913,11 +13055,11 @@ }; /** * Extend the Pointer prototype with methods for each event handler and more */ - extend(Pointer.prototype, { + extend(Pointer.prototype, /** @lends Pointer.prototype */ { onContainerPointerDown: function(e) { translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function(e) { touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY, @@ -12000,11 +13142,12 @@ setAnimation = H.setAnimation, stableSort = H.stableSort, win = H.win, wrap = H.wrap; /** - * The overview of the chart's series + * The overview of the chart's series. + * @class */ Legend = H.Legend = function(chart, options) { this.init(chart, options); }; @@ -12191,17 +13334,18 @@ }); if (legendGroup) { legend.group = legendGroup.destroy(); } + legend.display = null; // Reset in .render on update. }, /** * Position the checkboxes after the width is determined */ positionCheckboxes: function(scrollOffset) { - var alignAttr = this.group.alignAttr, + var alignAttr = this.group && this.group.alignAttr, translateY, clipHeight = this.clipHeight || this.legendHeight, titleHeight = this.titleHeight; if (alignAttr) { @@ -12300,13 +13444,13 @@ if (!li) { // generate it once, later move it // Generate the group box // A group to hold the symbol and text. Text is to be appended in Legend class. item.legendGroup = renderer.g('legend-item') - .addClass('highcharts-' + series.type + '-series highcharts-color-' + item.colorIndex + ' ' + - (item.options.className || '') + - (isSeries ? 'highcharts-series-' + item.index : '') + .addClass('highcharts-' + series.type + '-series highcharts-color-' + item.colorIndex + + (item.options.className ? ' ' + item.options.className : '') + + (isSeries ? ' highcharts-series-' + item.index : '') ) .attr({ zIndex: 1 }) .add(legend.scrollGroup); @@ -12542,11 +13686,12 @@ box.isNew = true; } // Presentational - box.attr({ + box + .attr({ stroke: options.borderColor, 'stroke-width': options.borderWidth || 0, fill: options.backgroundColor || 'none' }) .shadow(options.shadow); @@ -12624,23 +13769,31 @@ pages = this.pages, padding = this.padding, lastY, allItems = this.allItems, clipToHeight = function(height) { - clipRect.attr({ - height: height - }); + if (height) { + clipRect.attr({ + height: height + }); + } else if (clipRect) { // Reset (#5912) + legend.clipRect = clipRect.destroy(); + legend.contentGroup.clip(); + } // useHTML if (legend.contentGroup.div) { - legend.contentGroup.div.style.clip = 'rect(' + padding + 'px,9999px,' + (padding + height) + 'px,0)'; + legend.contentGroup.div.style.clip = height ? + 'rect(' + padding + 'px,9999px,' + + (padding + height) + 'px,0)' : + 'auto'; } }; // Adjust the height - if (options.layout === 'horizontal') { + if (options.layout === 'horizontal' && options.verticalAlign !== 'middle' && !options.floating) { spaceHeight /= 2; } if (maxHeight) { spaceHeight = Math.min(spaceHeight, maxHeight); } @@ -12707,12 +13860,13 @@ // Set initial position legend.scroll(0); legendHeight = spaceHeight; + // Reset } else if (nav) { - clipToHeight(chart.chartHeight); + clipToHeight(); nav.hide(); this.scrollGroup.attr({ translateY: 1 }); this.clipHeight = 0; // #1379 @@ -12808,18 +13962,18 @@ */ drawRectangle: function(legend, item) { var options = legend.options, symbolHeight = options.symbolHeight || legend.fontMetrics.f, square = options.squareSymbol, - symbolWidth = square ? symbolHeight : legend.symbolWidth; // docs: square + symbolWidth = square ? symbolHeight : legend.symbolWidth; item.legendSymbol = this.chart.renderer.rect( square ? (legend.symbolWidth - symbolHeight) / 2 : 0, legend.baseline - symbolHeight + 1, // #3988 symbolWidth, symbolHeight, - pick(legend.options.symbolRadius, symbolHeight / 2) // docs: new default + pick(legend.options.symbolRadius, symbolHeight / 2) ) .addClass('highcharts-point') .attr({ zIndex: 3 }).add(item.legendGroup); @@ -12866,11 +14020,11 @@ .attr(attr) .add(legendItemGroup); // Draw the marker if (markerOptions && markerOptions.enabled !== false) { - radius = markerOptions.radius; + radius = this.symbol.indexOf('url') === 0 ? 0 : markerOptions.radius; this.legendSymbol = legendSymbol = renderer.symbol( this.symbol, (symbolWidth / 2) - radius, verticalCenter - radius, 2 * radius, @@ -12946,14 +14100,17 @@ svg = H.svg, syncTimeout = H.syncTimeout, win = H.win, Renderer = H.Renderer; /** - * The Chart class - * @param {String|Object} renderTo The DOM element to render to, or its id - * @param {Object} options - * @param {Function} callback Function to run when the chart has loaded + * The Chart class. + * @class Highcharts.Chart + * @memberOf Highcharts + * @param {String|HTMLDOMElement} renderTo - The DOM element to render to, or its + * id. + * @param {ChartOptions} options - The chart options structure. + * @param {Function} callback - Function to run when the chart has loaded. */ var Chart = H.Chart = function() { this.getArgs.apply(this, arguments); }; @@ -13110,11 +14267,11 @@ /** * Redraw legend, axes or series based on updated data * * @param {Boolean|Object} animation Whether to apply animation, and optionally animation - * configuration + * configuration */ redraw: function(animation) { var chart = this, axes = chart.axes, series = chart.series, @@ -13235,12 +14392,11 @@ } // redraw affected series each(series, function(serie) { - if (serie.isDirty && serie.visible && - (!serie.isCartesian || serie.xAxis)) { // issue #153 + if ((isDirtyBox || serie.isDirty) && serie.visible) { serie.redraw(); } }); // move tooltip or reset @@ -13543,11 +14699,11 @@ chartHeight, renderTo = chart.renderTo, indexAttrName = 'data-highcharts-chart', oldChartIndex, Ren, - containerId = 'highcharts-' + H.idCounter++, + containerId = H.uniqueKey(), containerStyle, key; if (!renderTo) { chart.renderTo = renderTo = optionsChart.renderTo; @@ -13575,15 +14731,15 @@ attr(renderTo, indexAttrName, chart.index); // remove previous chart renderTo.innerHTML = ''; - // If the container doesn't have an offsetWidth, it has or is a child of a node - // that has display:none. We need to temporarily move it out to a visible - // state to determine the size, else the legend and tooltips won't render - // properly. The allowClone option is used in sparklines as a micro optimization, - // saving about 1-2 ms each chart. + // If the container doesn't have an offsetWidth, it has or is a child of + // a node that has display:none. We need to temporarily move it out to a + // visible state to determine the size, else the legend and tooltips + // won't render properly. The skipClone option is used in sparklines as + // a micro optimization, saving about 1-2 ms each chart. if (!optionsChart.skipClone && !renderTo.offsetWidth) { chart.cloneRenderTo(); } // get the width and height @@ -13601,13 +14757,14 @@ height: chartHeight + 'px', textAlign: 'left', lineHeight: 'normal', // #427 zIndex: 0, // #1072 '-webkit-tap-highlight-color': 'rgba(0,0,0,0)' - }); + }, optionsChart.style); - chart.container = container = createElement('div', { + chart.container = container = createElement( + 'div', { id: containerId }, containerStyle, chart.renderToClone || renderTo ); @@ -13729,19 +14886,27 @@ /** * Add the event handlers necessary for auto resizing */ initReflow: function() { var chart = this, - reflow = function(e) { - chart.reflow(e); - }; + unbind; - - addEvent(win, 'resize', reflow); - addEvent(chart, 'destroy', function() { - removeEvent(win, 'resize', reflow); + unbind = addEvent(win, 'resize', function(e) { + chart.reflow(e); }); + addEvent(chart, 'destroy', unbind); + + // The following will add listeners to re-fit the chart before and after + // printing (#2284). However it only works in WebKit. Should have worked + // in Firefox, but not supported in IE. + /* + if (win.matchMedia) { + win.matchMedia('print').addListener(function reflow() { + chart.reflow(); + }); + } + */ }, /** * Resize the chart to a given width and height * @param {Number} width @@ -13785,15 +14950,10 @@ each(chart.axes, function(axis) { axis.isDirty = true; axis.setScale(); }); - // make sure non-cartesian series are also handled - each(chart.series, function(serie) { - serie.isDirty = true; - }); - chart.isDirtyLegend = true; // force legend redraw chart.isDirtyBox = true; // force redraw of plot and chart border chart.layOutTitles(); // #2857 chart.getMargins(); @@ -14442,11 +15602,13 @@ }, this); fireEvent(this, 'load'); // Set up auto resize - this.initReflow(); + if (this.options.chart.reflow !== false) { + this.initReflow(); + } // Don't run again this.onload = null; } @@ -14471,19 +15633,27 @@ isNumber = H.isNumber, pick = H.pick, removeEvent = H.removeEvent; /** - * The Point object and prototype. Inheritable and used as base for PiePoint + * The Point object. The point objects are generated from the series.data + * configuration objects or raw numbers. They can be accessed from the + * Series.points array. + * @constructor Point */ Point = H.Point = function() {}; Point.prototype = { /** - * Initialize the point - * @param {Object} series The series object containing this point - * @param {Object} options The data in either number, array or object format + * Initialize the point. Called internally based on the series.data option. + * @function #init + * @memberOf Point + * @param {Object} series The series object containing this point. + * @param {Object} options The data in either number, array or object + * format. + * @param {Number} x Optionally, the X value of the. + * @returns {Object} The Point instance. */ init: function(series, options, x) { var point = this, colors, @@ -14515,14 +15685,18 @@ series.chart.pointCount++; return point; }, /** - * Apply the options containing the x and y data and possible some extra properties. - * This is called on point init or from point.update. + * Apply the options containing the x and y data and possible some extra + * properties. Called on point init or from point.update. * - * @param {Object} options + * @function #applyOptions + * @memberOf Point + * @param {Object} options The point options as defined in series.data. + * @param {Number} x Optionally, the X value. + * @returns {Object} The Point instance. */ applyOptions: function(options, x) { var point = this, series = point.series, pointValKey = series.options.pointValKey || series.pointValKey; @@ -14545,10 +15719,15 @@ point.isNull = pick( point.isValid && !point.isValid(), point.x === null || !isNumber(point.y, true) ); // #3571, check for NaN + // The point is initially selected by options (#5777) + if (point.selected) { + point.state = 'select'; + } + // If no x is set by now, get auto incremented value. All points must have an // x value, however the y value can be null to create a gap in the series if ('name' in point && x === undefined && series.xAxis && series.xAxis.hasNames) { point.x = series.xAxis.nameToX(point); } @@ -14620,10 +15799,11 @@ */ getClassName: function() { return 'highcharts-point' + (this.selected ? ' highcharts-point-select' : '') + (this.negative ? ' highcharts-negative' : '') + + (this.isNull ? ' highcharts-null-point' : '') + (this.colorIndex !== undefined ? ' highcharts-color-' + this.colorIndex : '') + (this.options.className ? ' ' + this.options.className : ''); }, /** @@ -14817,11 +15997,11 @@ SVGElement = H.SVGElement, syncTimeout = H.syncTimeout, win = H.win; /** - * @classDescription The base function which all other series types inherit from. The data in the series is stored + * The base function which all other series types inherit from. The data in the series is stored * in various arrays. * * - First, series.options.data contains all the original config options for * each point whether added by options or methods like series.addPoint. * - Next, series.data contains those values converted to points, but in case the series data length @@ -14830,1900 +16010,1977 @@ * - Then there's series.points that contains all currently visible point objects. In case of cropping, * the cropped-away points are not part of this array. The series.points array starts at series.cropStart * compared to series.data and series.options.data. If however the series data is grouped, these can't * be correlated one to one. * - series.xData and series.processedXData contain clean x values, equivalent to series.data and series.points. - * - series.yData and series.processedYData contain clean x values, equivalent to series.data and series.points. + * - series.yData and series.processedYData contain clean y values, equivalent to series.data and series.points. * - * @param {Object} chart - * @param {Object} options + * @constructor Series + * @param {Object} chart - The chart instance. + * @param {Object} options - The series options. */ H.Series = H.seriesType('line', null, { // base series options - //cursor: 'default', - //dashStyle: null, - //linecap: 'round', - lineWidth: 2, - //shadow: false, + //cursor: 'default', + //dashStyle: null, + //linecap: 'round', + lineWidth: 2, + //shadow: false, - allowPointSelect: false, - showCheckbox: false, - animation: { - duration: 1000 - }, - //clip: true, - //connectNulls: false, - //enableMouseTracking: true, - events: {}, - //legendIndex: 0, - // stacking: null, - marker: { + allowPointSelect: false, + showCheckbox: false, + animation: { + duration: 1000 + }, + //clip: true, + //connectNulls: false, + //enableMouseTracking: true, + events: {}, + //legendIndex: 0, + // stacking: null, + marker: { - lineWidth: 0, - lineColor: '#ffffff', - //fillColor: null, + lineWidth: 0, + lineColor: '#ffffff', + //fillColor: null, - //enabled: true, - //symbol: null, - radius: 4, - states: { // states for a single point - hover: { - enabled: true, - radiusPlus: 2, - - lineWidthPlus: 1 - + //enabled: true, + //symbol: null, + radius: 4, + states: { // states for a single point + hover: { + animation: { + duration: 50 }, + enabled: true, + radiusPlus: 2, - select: { - fillColor: '#cccccc', - lineColor: '#000000', - lineWidth: 2 - } + lineWidthPlus: 1 - } - }, - point: { - events: {} - }, - dataLabels: { - align: 'center', - // defer: true, - // enabled: false, - formatter: function() { - return this.y === null ? '' : H.numberFormat(this.y, -1); }, - style: { - fontSize: '11px', - fontWeight: 'bold', - color: 'contrast', - textShadow: '1px 1px contrast, -1px -1px contrast, -1px 1px contrast, 1px -1px contrast' - }, - // backgroundColor: undefined, - // borderColor: undefined, - // borderWidth: undefined, - // shadow: false + select: { + fillColor: '#cccccc', + lineColor: '#000000', + lineWidth: 2 + } - verticalAlign: 'bottom', // above singular point - x: 0, - y: 0, - // borderRadius: undefined, - padding: 5 + } + }, + point: { + events: {} + }, + dataLabels: { + align: 'center', + // defer: true, + // enabled: false, + formatter: function() { + return this.y === null ? '' : H.numberFormat(this.y, -1); }, - cropThreshold: 300, // draw points outside the plot area when the number of points is less than this - pointRange: 0, - //pointStart: 0, - //pointInterval: 1, - //showInLegend: null, // auto: true for standalone series, false for linked series - softThreshold: true, - states: { // states for the entire series - hover: { - //enabled: false, - lineWidthPlus: 1, - marker: { - // lineWidth: base + 1, - // radius: base + 1 - }, - halo: { - size: 10, - opacity: 0.25 + style: { + fontSize: '11px', + fontWeight: 'bold', + color: 'contrast', + textOutline: '1px contrast' + }, + // backgroundColor: undefined, + // borderColor: undefined, + // borderWidth: undefined, + // shadow: false - } + verticalAlign: 'bottom', // above singular point + x: 0, + y: 0, + // borderRadius: undefined, + padding: 5 + }, + cropThreshold: 300, // draw points outside the plot area when the number of points is less than this + pointRange: 0, + //pointStart: 0, + //pointInterval: 1, + //showInLegend: null, // auto: true for standalone series, false for linked series + softThreshold: true, + states: { // states for the entire series + hover: { + //enabled: false, + lineWidthPlus: 1, + marker: { + // lineWidth: base + 1, + // radius: base + 1 }, - select: { - marker: {} + halo: { + size: 10, + + opacity: 0.25 + } }, - stickyTracking: true, - //tooltip: { - //pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b>' - //valueDecimals: null, - //xDateFormat: '%A, %b %e, %Y', - //valuePrefix: '', - //ySuffix: '' - //} - turboThreshold: 1000 - // zIndex: null + select: { + marker: {} + } }, + stickyTracking: true, + //tooltip: { + //pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b>' + //valueDecimals: null, + //xDateFormat: '%A, %b %e, %Y', + //valuePrefix: '', + //ySuffix: '' + //} + turboThreshold: 1000 + // zIndex: null - // Prototype properties - { - isCartesian: true, - pointClass: Point, - sorted: true, // requires the data to be sorted - requireSorting: true, - directTouch: false, - axisTypes: ['xAxis', 'yAxis'], - colorCounter: 0, - parallelArrays: ['x', 'y'], // each point's x and y values are stored in this.xData and this.yData - coll: 'series', - init: function(chart, options) { - var series = this, - eventType, - events, - chartSeries = chart.series, - sortByIndex = function(a, b) { - return pick(a.options.index, a._i) - pick(b.options.index, b._i); - }; - series.chart = chart; - series.options = options = series.setOptions(options); // merge with plotOptions - series.linkedSeries = []; + }, /** @lends Series.prototype */ { + isCartesian: true, + pointClass: Point, + sorted: true, // requires the data to be sorted + requireSorting: true, + directTouch: false, + axisTypes: ['xAxis', 'yAxis'], + colorCounter: 0, + parallelArrays: ['x', 'y'], // each point's x and y values are stored in this.xData and this.yData + coll: 'series', + init: function(chart, options) { + var series = this, + eventType, + events, + chartSeries = chart.series, + lastSeries, + sortByIndex = function(a, b) { + return pick(a.options.index, a._i) - pick(b.options.index, b._i); + }; - // bind the axes - series.bindAxes(); + series.chart = chart; + series.options = options = series.setOptions(options); // merge with plotOptions + series.linkedSeries = []; - // set some variables - extend(series, { - name: options.name, - state: '', - visible: options.visible !== false, // true by default - selected: options.selected === true // false by default - }); + // bind the axes + series.bindAxes(); - // register event listeners - events = options.events; - for (eventType in events) { - addEvent(series, eventType, events[eventType]); - } - if ( - (events && events.click) || - (options.point && options.point.events && options.point.events.click) || - options.allowPointSelect - ) { - chart.runTrackerClick = true; - } + // set some variables + extend(series, { + name: options.name, + state: '', + visible: options.visible !== false, // true by default + selected: options.selected === true // false by default + }); - series.getColor(); - series.getSymbol(); + // register event listeners + events = options.events; + for (eventType in events) { + addEvent(series, eventType, events[eventType]); + } + if ( + (events && events.click) || + (options.point && options.point.events && options.point.events.click) || + options.allowPointSelect + ) { + chart.runTrackerClick = true; + } - // Set the data - each(series.parallelArrays, function(key) { - series[key + 'Data'] = []; - }); - series.setData(options.data, false); + series.getColor(); + series.getSymbol(); - // Mark cartesian - if (series.isCartesian) { - chart.hasCartesianSeries = true; - } + // Set the data + each(series.parallelArrays, function(key) { + series[key + 'Data'] = []; + }); + series.setData(options.data, false); - // Register it in the chart - chartSeries.push(series); - series._i = chartSeries.length - 1; + // Mark cartesian + if (series.isCartesian) { + chart.hasCartesianSeries = true; + } - // Sort series according to index option (#248, #1123, #2456) - stableSort(chartSeries, sortByIndex); - if (this.yAxis) { - stableSort(this.yAxis.series, sortByIndex); - } + // Get the index and register the series in the chart. The index is one + // more than the current latest series index (#5960). + if (chartSeries.length) { + lastSeries = chartSeries[chartSeries.length - 1]; + } + series._i = pick(lastSeries && lastSeries._i, -1) + 1; + chartSeries.push(series); - each(chartSeries, function(series, i) { - series.index = i; - series.name = series.name || 'Series ' + (i + 1); - }); + // Sort series according to index option (#248, #1123, #2456) + stableSort(chartSeries, sortByIndex); + if (this.yAxis) { + stableSort(this.yAxis.series, sortByIndex); + } - }, + each(chartSeries, function(series, i) { + series.index = i; + series.name = series.name || 'Series ' + (i + 1); + }); - /** - * Set the xAxis and yAxis properties of cartesian series, and register the series - * in the axis.series array - */ - bindAxes: function() { - var series = this, - seriesOptions = series.options, - chart = series.chart, - axisOptions; + }, - each(series.axisTypes || [], function(AXIS) { // repeat for xAxis and yAxis + /** + * Set the xAxis and yAxis properties of cartesian series, and register the + * series in the `axis.series` array. + * + * @function #bindAxes + * @memberOf Series + * @returns {void} + */ + bindAxes: function() { + var series = this, + seriesOptions = series.options, + chart = series.chart, + axisOptions; - each(chart[AXIS], function(axis) { // loop through the chart's axis objects - axisOptions = axis.options; + each(series.axisTypes || [], function(AXIS) { // repeat for xAxis and yAxis - // apply if the series xAxis or yAxis option mathches the number of the - // axis, or if undefined, use the first axis - if ((seriesOptions[AXIS] === axisOptions.index) || - (seriesOptions[AXIS] !== undefined && seriesOptions[AXIS] === axisOptions.id) || - (seriesOptions[AXIS] === undefined && axisOptions.index === 0)) { + each(chart[AXIS], function(axis) { // loop through the chart's axis objects + axisOptions = axis.options; - // register this series in the axis.series lookup - axis.series.push(series); + // apply if the series xAxis or yAxis option mathches the number of the + // axis, or if undefined, use the first axis + if ((seriesOptions[AXIS] === axisOptions.index) || + (seriesOptions[AXIS] !== undefined && seriesOptions[AXIS] === axisOptions.id) || + (seriesOptions[AXIS] === undefined && axisOptions.index === 0)) { - // set this series.xAxis or series.yAxis reference - series[AXIS] = axis; + // register this series in the axis.series lookup + axis.series.push(series); - // mark dirty for redraw - axis.isDirty = true; - } - }); + // set this series.xAxis or series.yAxis reference + series[AXIS] = axis; - // The series needs an X and an Y axis - if (!series[AXIS] && series.optionalAxis !== AXIS) { - error(18, true); + // mark dirty for redraw + axis.isDirty = true; } - }); - }, - /** - * For simple series types like line and column, the data values are held in arrays like - * xData and yData for quick lookup to find extremes and more. For multidimensional series - * like bubble and map, this can be extended with arrays like zData and valueData by - * adding to the series.parallelArrays array. - */ - updateParallelArrays: function(point, i) { - var series = point.series, - args = arguments, - fn = isNumber(i) ? - // Insert the value in the given position - function(key) { - var val = key === 'y' && series.toYData ? series.toYData(point) : point[key]; - series[key + 'Data'][i] = val; - } : - // Apply the method specified in i with the following arguments as arguments - function(key) { - Array.prototype[i].apply(series[key + 'Data'], Array.prototype.slice.call(args, 2)); - }; + // The series needs an X and an Y axis + if (!series[AXIS] && series.optionalAxis !== AXIS) { + error(18, true); + } - each(series.parallelArrays, fn); - }, + }); + }, - /** - * Return an auto incremented x value based on the pointStart and pointInterval options. - * This is only used if an x value is not given for the point that calls autoIncrement. - */ - autoIncrement: function() { + /** + * For simple series types like line and column, the data values are held in arrays like + * xData and yData for quick lookup to find extremes and more. For multidimensional series + * like bubble and map, this can be extended with arrays like zData and valueData by + * adding to the series.parallelArrays array. + */ + updateParallelArrays: function(point, i) { + var series = point.series, + args = arguments, + fn = isNumber(i) ? + // Insert the value in the given position + function(key) { + var val = key === 'y' && series.toYData ? series.toYData(point) : point[key]; + series[key + 'Data'][i] = val; + } : + // Apply the method specified in i with the following arguments as arguments + function(key) { + Array.prototype[i].apply(series[key + 'Data'], Array.prototype.slice.call(args, 2)); + }; - var options = this.options, - xIncrement = this.xIncrement, - date, - pointInterval, - pointIntervalUnit = options.pointIntervalUnit; + each(series.parallelArrays, fn); + }, - xIncrement = pick(xIncrement, options.pointStart, 0); + /** + * Return an auto incremented x value based on the pointStart and pointInterval options. + * This is only used if an x value is not given for the point that calls autoIncrement. + */ + autoIncrement: function() { - this.pointInterval = pointInterval = pick(this.pointInterval, options.pointInterval, 1); + var options = this.options, + xIncrement = this.xIncrement, + date, + pointInterval, + pointIntervalUnit = options.pointIntervalUnit; - // Added code for pointInterval strings - if (pointIntervalUnit) { - date = new Date(xIncrement); + xIncrement = pick(xIncrement, options.pointStart, 0); - if (pointIntervalUnit === 'day') { - date = +date[Date.hcSetDate](date[Date.hcGetDate]() + pointInterval); - } else if (pointIntervalUnit === 'month') { - date = +date[Date.hcSetMonth](date[Date.hcGetMonth]() + pointInterval); - } else if (pointIntervalUnit === 'year') { - date = +date[Date.hcSetFullYear](date[Date.hcGetFullYear]() + pointInterval); - } - pointInterval = date - xIncrement; + this.pointInterval = pointInterval = pick(this.pointInterval, options.pointInterval, 1); + // Added code for pointInterval strings + if (pointIntervalUnit) { + date = new Date(xIncrement); + + if (pointIntervalUnit === 'day') { + date = +date[Date.hcSetDate](date[Date.hcGetDate]() + pointInterval); + } else if (pointIntervalUnit === 'month') { + date = +date[Date.hcSetMonth](date[Date.hcGetMonth]() + pointInterval); + } else if (pointIntervalUnit === 'year') { + date = +date[Date.hcSetFullYear](date[Date.hcGetFullYear]() + pointInterval); } + pointInterval = date - xIncrement; - this.xIncrement = xIncrement + pointInterval; - return xIncrement; - }, + } - /** - * Set the series options by merging from the options tree - * @param {Object} itemOptions - */ - setOptions: function(itemOptions) { - var chart = this.chart, - chartOptions = chart.options, - plotOptions = chartOptions.plotOptions, - userOptions = chart.userOptions || {}, - userPlotOptions = userOptions.plotOptions || {}, - typeOptions = plotOptions[this.type], - options, - zones; + this.xIncrement = xIncrement + pointInterval; + return xIncrement; + }, - this.userOptions = itemOptions; + /** + * Set the series options by merging from the options tree + * @param {Object} itemOptions + */ + setOptions: function(itemOptions) { + var chart = this.chart, + chartOptions = chart.options, + plotOptions = chartOptions.plotOptions, + userOptions = chart.userOptions || {}, + userPlotOptions = userOptions.plotOptions || {}, + typeOptions = plotOptions[this.type], + options, + zones; - // General series options take precedence over type options because otherwise, default - // type options like column.animation would be overwritten by the general option. - // But issues have been raised here (#3881), and the solution may be to distinguish - // between default option and userOptions like in the tooltip below. - options = merge( - typeOptions, - plotOptions.series, - itemOptions - ); + this.userOptions = itemOptions; - // The tooltip options are merged between global and series specific options - this.tooltipOptions = merge( - defaultOptions.tooltip, - defaultOptions.plotOptions[this.type].tooltip, - userOptions.tooltip, - userPlotOptions.series && userPlotOptions.series.tooltip, - userPlotOptions[this.type] && userPlotOptions[this.type].tooltip, - itemOptions.tooltip - ); + // General series options take precedence over type options because otherwise, default + // type options like column.animation would be overwritten by the general option. + // But issues have been raised here (#3881), and the solution may be to distinguish + // between default option and userOptions like in the tooltip below. + options = merge( + typeOptions, + plotOptions.series, + itemOptions + ); - // Delete marker object if not allowed (#1125) - if (typeOptions.marker === null) { - delete options.marker; - } + // The tooltip options are merged between global and series specific options + this.tooltipOptions = merge( + defaultOptions.tooltip, + defaultOptions.plotOptions[this.type].tooltip, + userOptions.tooltip, + userPlotOptions.series && userPlotOptions.series.tooltip, + userPlotOptions[this.type] && userPlotOptions[this.type].tooltip, + itemOptions.tooltip + ); - // Handle color zones - this.zoneAxis = options.zoneAxis; - zones = this.zones = (options.zones || []).slice(); - if ((options.negativeColor || options.negativeFillColor) && !options.zones) { - zones.push({ - value: options[this.zoneAxis + 'Threshold'] || options.threshold || 0, - className: 'highcharts-negative', + // Delete marker object if not allowed (#1125) + if (typeOptions.marker === null) { + delete options.marker; + } - color: options.negativeColor, - fillColor: options.negativeFillColor + // Handle color zones + this.zoneAxis = options.zoneAxis; + zones = this.zones = (options.zones || []).slice(); + if ((options.negativeColor || options.negativeFillColor) && !options.zones) { + zones.push({ + value: options[this.zoneAxis + 'Threshold'] || options.threshold || 0, + className: 'highcharts-negative', - }); - } - if (zones.length) { // Push one extra zone for the rest - if (defined(zones[zones.length - 1].value)) { - zones.push({ + color: options.negativeColor, + fillColor: options.negativeFillColor - color: this.color, - fillColor: this.fillColor + }); + } + if (zones.length) { // Push one extra zone for the rest + if (defined(zones[zones.length - 1].value)) { + zones.push({ - }); - } - } - return options; - }, + color: this.color, + fillColor: this.fillColor - getCyclic: function(prop, value, defaults) { - var i, - userOptions = this.userOptions, - indexName = prop + 'Index', - counterName = prop + 'Counter', - len = defaults ? defaults.length : pick(this.chart.options.chart[prop + 'Count'], this.chart[prop + 'Count']), - setting; - - if (!value) { - // Pick up either the colorIndex option, or the _colorIndex after Series.update() - setting = pick(userOptions[indexName], userOptions['_' + indexName]); - if (defined(setting)) { // after Series.update() - i = setting; - } else { - userOptions['_' + indexName] = i = this.chart[counterName] % len; - this.chart[counterName] += 1; - } - if (defaults) { - value = defaults[i]; - } + }); } - // Set the colorIndex - if (i !== undefined) { - this[indexName] = i; - } - this[prop] = value; - }, + } + return options; + }, - /** - * Get the series' color - */ + getCyclic: function(prop, value, defaults) { + var i, + userOptions = this.userOptions, + indexName = prop + 'Index', + counterName = prop + 'Counter', + len = defaults ? defaults.length : pick(this.chart.options.chart[prop + 'Count'], this.chart[prop + 'Count']), + setting; - getColor: function() { - if (this.options.colorByPoint) { - this.options.color = null; // #4359, selected slice got series.color even when colorByPoint was set. + if (!value) { + // Pick up either the colorIndex option, or the _colorIndex after Series.update() + setting = pick(userOptions[indexName], userOptions['_' + indexName]); + if (defined(setting)) { // after Series.update() + i = setting; } else { - this.getCyclic('color', this.options.color || defaultPlotOptions[this.type].color, this.chart.options.colors); + userOptions['_' + indexName] = i = this.chart[counterName] % len; + this.chart[counterName] += 1; } - }, + if (defaults) { + value = defaults[i]; + } + } + // Set the colorIndex + if (i !== undefined) { + this[indexName] = i; + } + this[prop] = value; + }, - /** - * Get the series' symbol - */ - getSymbol: function() { - var seriesMarkerOption = this.options.marker; + /** + * Get the series' color + */ - this.getCyclic('symbol', seriesMarkerOption.symbol, this.chart.options.symbols); + getColor: function() { + if (this.options.colorByPoint) { + this.options.color = null; // #4359, selected slice got series.color even when colorByPoint was set. + } else { + this.getCyclic('color', this.options.color || defaultPlotOptions[this.type].color, this.chart.options.colors); + } + }, - // don't substract radius in image symbols (#604) - if (/^url/.test(this.symbol)) { - seriesMarkerOption.radius = 0; - } - }, + /** + * Get the series' symbol + */ + getSymbol: function() { + var seriesMarkerOption = this.options.marker; - drawLegendSymbol: LegendSymbolMixin.drawLineMarker, + this.getCyclic('symbol', seriesMarkerOption.symbol, this.chart.options.symbols); + }, - /** - * Replace the series data with a new set of data - * @param {Object} data - * @param {Object} redraw - */ - setData: function(data, redraw, animation, updatePoints) { - var series = this, - oldData = series.points, - oldDataLength = (oldData && oldData.length) || 0, - dataLength, - options = series.options, - chart = series.chart, - firstPoint = null, - xAxis = series.xAxis, - i, - turboThreshold = options.turboThreshold, - pt, - xData = this.xData, - yData = this.yData, - pointArrayMap = series.pointArrayMap, - valueCount = pointArrayMap && pointArrayMap.length; + drawLegendSymbol: LegendSymbolMixin.drawLineMarker, - data = data || []; - dataLength = data.length; - redraw = pick(redraw, true); + /** + * Replace the series data with a new set of data + * @param {Object} data + * @param {Object} redraw + */ + setData: function(data, redraw, animation, updatePoints) { + var series = this, + oldData = series.points, + oldDataLength = (oldData && oldData.length) || 0, + dataLength, + options = series.options, + chart = series.chart, + firstPoint = null, + xAxis = series.xAxis, + i, + turboThreshold = options.turboThreshold, + pt, + xData = this.xData, + yData = this.yData, + pointArrayMap = series.pointArrayMap, + valueCount = pointArrayMap && pointArrayMap.length; - // If the point count is the same as is was, just run Point.update which is - // cheaper, allows animation, and keeps references to points. - if (updatePoints !== false && dataLength && oldDataLength === dataLength && !series.cropped && !series.hasGroupedData && series.visible) { - each(data, function(point, i) { - // .update doesn't exist on a linked, hidden series (#3709) - if (oldData[i].update && point !== options.data[i]) { - oldData[i].update(point, false, null, false); - } - }); + data = data || []; + dataLength = data.length; + redraw = pick(redraw, true); - } else { + // If the point count is the same as is was, just run Point.update which is + // cheaper, allows animation, and keeps references to points. + if (updatePoints !== false && dataLength && oldDataLength === dataLength && !series.cropped && !series.hasGroupedData && series.visible) { + each(data, function(point, i) { + // .update doesn't exist on a linked, hidden series (#3709) + if (oldData[i].update && point !== options.data[i]) { + oldData[i].update(point, false, null, false); + } + }); - // Reset properties - series.xIncrement = null; + } else { - series.colorCounter = 0; // for series with colorByPoint (#1547) + // Reset properties + series.xIncrement = null; - // Update parallel arrays - each(this.parallelArrays, function(key) { - series[key + 'Data'].length = 0; - }); + series.colorCounter = 0; // for series with colorByPoint (#1547) - // In turbo mode, only one- or twodimensional arrays of numbers are allowed. The - // first value is tested, and we assume that all the rest are defined the same - // way. Although the 'for' loops are similar, they are repeated inside each - // if-else conditional for max performance. - if (turboThreshold && dataLength > turboThreshold) { + // Update parallel arrays + each(this.parallelArrays, function(key) { + series[key + 'Data'].length = 0; + }); - // find the first non-null point - i = 0; - while (firstPoint === null && i < dataLength) { - firstPoint = data[i]; - i++; - } + // In turbo mode, only one- or twodimensional arrays of numbers are allowed. The + // first value is tested, and we assume that all the rest are defined the same + // way. Although the 'for' loops are similar, they are repeated inside each + // if-else conditional for max performance. + if (turboThreshold && dataLength > turboThreshold) { + // find the first non-null point + i = 0; + while (firstPoint === null && i < dataLength) { + firstPoint = data[i]; + i++; + } - if (isNumber(firstPoint)) { // assume all points are numbers + + if (isNumber(firstPoint)) { // assume all points are numbers + for (i = 0; i < dataLength; i++) { + xData[i] = this.autoIncrement(); + yData[i] = data[i]; + } + } else if (isArray(firstPoint)) { // assume all points are arrays + if (valueCount) { // [x, low, high] or [x, o, h, l, c] for (i = 0; i < dataLength; i++) { - xData[i] = this.autoIncrement(); - yData[i] = data[i]; + pt = data[i]; + xData[i] = pt[0]; + yData[i] = pt.slice(1, valueCount + 1); } - } else if (isArray(firstPoint)) { // assume all points are arrays - if (valueCount) { // [x, low, high] or [x, o, h, l, c] - for (i = 0; i < dataLength; i++) { - pt = data[i]; - xData[i] = pt[0]; - yData[i] = pt.slice(1, valueCount + 1); - } - } else { // [x, y] - for (i = 0; i < dataLength; i++) { - pt = data[i]; - xData[i] = pt[0]; - yData[i] = pt[1]; - } + } else { // [x, y] + for (i = 0; i < dataLength; i++) { + pt = data[i]; + xData[i] = pt[0]; + yData[i] = pt[1]; } - } else { - error(12); // Highcharts expects configs to be numbers or arrays in turbo mode } } else { - for (i = 0; i < dataLength; i++) { - if (data[i] !== undefined) { // stray commas in oldIE - pt = { - series: series - }; - series.pointClass.prototype.applyOptions.apply(pt, [data[i]]); - series.updateParallelArrays(pt, i); - } + error(12); // Highcharts expects configs to be numbers or arrays in turbo mode + } + } else { + for (i = 0; i < dataLength; i++) { + if (data[i] !== undefined) { // stray commas in oldIE + pt = { + series: series + }; + series.pointClass.prototype.applyOptions.apply(pt, [data[i]]); + series.updateParallelArrays(pt, i); } } + } - // Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON - if (isString(yData[0])) { - error(14, true); - } + // Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON + if (isString(yData[0])) { + error(14, true); + } - series.data = []; - series.options.data = series.userOptions.data = data; + series.data = []; + series.options.data = series.userOptions.data = data; - // destroy old points - i = oldDataLength; - while (i--) { - if (oldData[i] && oldData[i].destroy) { - oldData[i].destroy(); - } + // destroy old points + i = oldDataLength; + while (i--) { + if (oldData[i] && oldData[i].destroy) { + oldData[i].destroy(); } - - // reset minRange (#878) - if (xAxis) { - xAxis.minRange = xAxis.userMinRange; - } - - // redraw - series.isDirty = chart.isDirtyBox = true; - series.isDirtyData = !!oldData; - animation = false; } - // Typically for pie series, points need to be processed and generated - // prior to rendering the legend - if (options.legendType === 'point') { - this.processData(); - this.generatePoints(); + // reset minRange (#878) + if (xAxis) { + xAxis.minRange = xAxis.userMinRange; } - if (redraw) { - chart.redraw(animation); - } - }, + // redraw + series.isDirty = chart.isDirtyBox = true; + series.isDirtyData = !!oldData; + animation = false; + } - /** - * Process the data by cropping away unused data points if the series is longer - * than the crop threshold. This saves computing time for lage series. - */ - processData: function(force) { - var series = this, - processedXData = series.xData, // copied during slice operation below - processedYData = series.yData, - dataLength = processedXData.length, - croppedData, - cropStart = 0, - cropped, - distance, - closestPointRange, - xAxis = series.xAxis, - i, // loop variable - options = series.options, - cropThreshold = options.cropThreshold, - getExtremesFromAll = series.getExtremesFromAll || options.getExtremesFromAll, // #4599 - isCartesian = series.isCartesian, - xExtremes, - val2lin = xAxis && xAxis.val2lin, - isLog = xAxis && xAxis.isLog, - min, - max; + // Typically for pie series, points need to be processed and generated + // prior to rendering the legend + if (options.legendType === 'point') { + this.processData(); + this.generatePoints(); + } - // If the series data or axes haven't changed, don't go through this. Return false to pass - // the message on to override methods like in data grouping. - if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) { - return false; - } + if (redraw) { + chart.redraw(animation); + } + }, - if (xAxis) { - xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053) - min = xExtremes.min; - max = xExtremes.max; - } + /** + * Process the data by cropping away unused data points if the series is longer + * than the crop threshold. This saves computing time for lage series. + */ + processData: function(force) { + var series = this, + processedXData = series.xData, // copied during slice operation below + processedYData = series.yData, + dataLength = processedXData.length, + croppedData, + cropStart = 0, + cropped, + distance, + closestPointRange, + xAxis = series.xAxis, + i, // loop variable + options = series.options, + cropThreshold = options.cropThreshold, + getExtremesFromAll = series.getExtremesFromAll || options.getExtremesFromAll, // #4599 + isCartesian = series.isCartesian, + xExtremes, + val2lin = xAxis && xAxis.val2lin, + isLog = xAxis && xAxis.isLog, + min, + max; - // optionally filter out points outside the plot area - if (isCartesian && series.sorted && !getExtremesFromAll && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) { + // If the series data or axes haven't changed, don't go through this. Return false to pass + // the message on to override methods like in data grouping. + if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) { + return false; + } - // it's outside current extremes - if (processedXData[dataLength - 1] < min || processedXData[0] > max) { - processedXData = []; - processedYData = []; + if (xAxis) { + xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053) + min = xExtremes.min; + max = xExtremes.max; + } - // only crop if it's actually spilling out - } else if (processedXData[0] < min || processedXData[dataLength - 1] > max) { - croppedData = this.cropData(series.xData, series.yData, min, max); - processedXData = croppedData.xData; - processedYData = croppedData.yData; - cropStart = croppedData.start; - cropped = true; - } + // optionally filter out points outside the plot area + if (isCartesian && series.sorted && !getExtremesFromAll && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) { + + // it's outside current extremes + if (processedXData[dataLength - 1] < min || processedXData[0] > max) { + processedXData = []; + processedYData = []; + + // only crop if it's actually spilling out + } else if (processedXData[0] < min || processedXData[dataLength - 1] > max) { + croppedData = this.cropData(series.xData, series.yData, min, max); + processedXData = croppedData.xData; + processedYData = croppedData.yData; + cropStart = croppedData.start; + cropped = true; } + } - // Find the closest distance between processed points - i = processedXData.length || 1; - while (--i) { - distance = isLog ? - val2lin(processedXData[i]) - val2lin(processedXData[i - 1]) : - processedXData[i] - processedXData[i - 1]; + // Find the closest distance between processed points + i = processedXData.length || 1; + while (--i) { + distance = isLog ? + val2lin(processedXData[i]) - val2lin(processedXData[i - 1]) : + processedXData[i] - processedXData[i - 1]; - if (distance > 0 && (closestPointRange === undefined || distance < closestPointRange)) { - closestPointRange = distance; + if (distance > 0 && (closestPointRange === undefined || distance < closestPointRange)) { + closestPointRange = distance; - // Unsorted data is not supported by the line tooltip, as well as data grouping and - // navigation in Stock charts (#725) and width calculation of columns (#1900) - } else if (distance < 0 && series.requireSorting) { - error(15); - } + // Unsorted data is not supported by the line tooltip, as well as data grouping and + // navigation in Stock charts (#725) and width calculation of columns (#1900) + } else if (distance < 0 && series.requireSorting) { + error(15); } + } - // Record the properties - series.cropped = cropped; // undefined or true - series.cropStart = cropStart; - series.processedXData = processedXData; - series.processedYData = processedYData; + // Record the properties + series.cropped = cropped; // undefined or true + series.cropStart = cropStart; + series.processedXData = processedXData; + series.processedYData = processedYData; - series.closestPointRange = closestPointRange; + series.closestPointRange = closestPointRange; - }, + }, - /** - * Iterate over xData and crop values between min and max. Returns object containing crop start/end - * cropped xData with corresponding part of yData, dataMin and dataMax within the cropped range - */ - cropData: function(xData, yData, min, max) { - var dataLength = xData.length, - cropStart = 0, - cropEnd = dataLength, - cropShoulder = pick(this.cropShoulder, 1), // line-type series need one point outside - i, - j; + /** + * Iterate over xData and crop values between min and max. Returns object containing crop start/end + * cropped xData with corresponding part of yData, dataMin and dataMax within the cropped range + */ + cropData: function(xData, yData, min, max) { + var dataLength = xData.length, + cropStart = 0, + cropEnd = dataLength, + cropShoulder = pick(this.cropShoulder, 1), // line-type series need one point outside + i, + j; - // iterate up to find slice start - for (i = 0; i < dataLength; i++) { - if (xData[i] >= min) { - cropStart = Math.max(0, i - cropShoulder); - break; - } + // iterate up to find slice start + for (i = 0; i < dataLength; i++) { + if (xData[i] >= min) { + cropStart = Math.max(0, i - cropShoulder); + break; } + } - // proceed to find slice end - for (j = i; j < dataLength; j++) { - if (xData[j] > max) { - cropEnd = j + cropShoulder; - break; - } + // proceed to find slice end + for (j = i; j < dataLength; j++) { + if (xData[j] > max) { + cropEnd = j + cropShoulder; + break; } + } - return { - xData: xData.slice(cropStart, cropEnd), - yData: yData.slice(cropStart, cropEnd), - start: cropStart, - end: cropEnd - }; - }, + return { + xData: xData.slice(cropStart, cropEnd), + yData: yData.slice(cropStart, cropEnd), + start: cropStart, + end: cropEnd + }; + }, - /** - * Generate the data point after the data has been processed by cropping away - * unused points and optionally grouped in Highcharts Stock. - */ - generatePoints: function() { - var series = this, - options = series.options, - dataOptions = options.data, - data = series.data, - dataLength, - processedXData = series.processedXData, - processedYData = series.processedYData, - PointClass = series.pointClass, - processedDataLength = processedXData.length, - cropStart = series.cropStart || 0, - cursor, - hasGroupedData = series.hasGroupedData, - point, - points = [], - i; + /** + * Generate the data point after the data has been processed by cropping away + * unused points and optionally grouped in Highcharts Stock. + */ + generatePoints: function() { + var series = this, + options = series.options, + dataOptions = options.data, + data = series.data, + dataLength, + processedXData = series.processedXData, + processedYData = series.processedYData, + PointClass = series.pointClass, + processedDataLength = processedXData.length, + cropStart = series.cropStart || 0, + cursor, + hasGroupedData = series.hasGroupedData, + point, + points = [], + i; - if (!data && !hasGroupedData) { - var arr = []; - arr.length = dataOptions.length; - data = series.data = arr; - } + if (!data && !hasGroupedData) { + var arr = []; + arr.length = dataOptions.length; + data = series.data = arr; + } - for (i = 0; i < processedDataLength; i++) { - cursor = cropStart + i; - if (!hasGroupedData) { - if (data[cursor]) { - point = data[cursor]; - } else if (dataOptions[cursor] !== undefined) { // #970 - data[cursor] = point = (new PointClass()).init(series, dataOptions[cursor], processedXData[i]); - } - points[i] = point; - } else { - // splat the y data in case of ohlc data array - points[i] = (new PointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i]))); - points[i].dataGroup = series.groupMap[i]; + for (i = 0; i < processedDataLength; i++) { + cursor = cropStart + i; + if (!hasGroupedData) { + point = data[cursor]; + if (!point && dataOptions[cursor] !== undefined) { // #970 + data[cursor] = point = (new PointClass()).init(series, dataOptions[cursor], processedXData[i]); } - points[i].index = cursor; // For faster access in Point.update + } else { + // splat the y data in case of ohlc data array + point = (new PointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i]))); + point.dataGroup = series.groupMap[i]; } + point.index = cursor; // For faster access in Point.update + points[i] = point; + } - // Hide cropped-away points - this only runs when the number of points is above cropThreshold, or when - // swithching view from non-grouped data to grouped data (#637) - if (data && (processedDataLength !== (dataLength = data.length) || hasGroupedData)) { - for (i = 0; i < dataLength; i++) { - if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points - i += processedDataLength; - } - if (data[i]) { - data[i].destroyElements(); - data[i].plotX = undefined; // #1003 - } + // Hide cropped-away points - this only runs when the number of points is above cropThreshold, or when + // swithching view from non-grouped data to grouped data (#637) + if (data && (processedDataLength !== (dataLength = data.length) || hasGroupedData)) { + for (i = 0; i < dataLength; i++) { + if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points + i += processedDataLength; } + if (data[i]) { + data[i].destroyElements(); + data[i].plotX = undefined; // #1003 + } } + } - series.data = data; - series.points = points; - }, + series.data = data; + series.points = points; + }, - /** - * Calculate Y extremes for visible data - */ - getExtremes: function(yData) { - var xAxis = this.xAxis, - yAxis = this.yAxis, - xData = this.processedXData, - yDataLength, - activeYData = [], - activeCounter = 0, - xExtremes = xAxis.getExtremes(), // #2117, need to compensate for log X axis - xMin = xExtremes.min, - xMax = xExtremes.max, - validValue, - withinRange, - x, - y, - i, - j; + /** + * Calculate Y extremes for visible data + */ + getExtremes: function(yData) { + var xAxis = this.xAxis, + yAxis = this.yAxis, + xData = this.processedXData, + yDataLength, + activeYData = [], + activeCounter = 0, + xExtremes = xAxis.getExtremes(), // #2117, need to compensate for log X axis + xMin = xExtremes.min, + xMax = xExtremes.max, + validValue, + withinRange, + x, + y, + i, + j; - yData = yData || this.stackedYData || this.processedYData || []; - yDataLength = yData.length; + yData = yData || this.stackedYData || this.processedYData || []; + yDataLength = yData.length; - for (i = 0; i < yDataLength; i++) { + for (i = 0; i < yDataLength; i++) { - x = xData[i]; - y = yData[i]; + x = xData[i]; + y = yData[i]; - // For points within the visible range, including the first point outside the - // visible range, consider y extremes - validValue = (isNumber(y, true) || isArray(y)) && (!yAxis.isLog || (y.length || y > 0)); - withinRange = this.getExtremesFromAll || this.options.getExtremesFromAll || this.cropped || - ((xData[i + 1] || x) >= xMin && (xData[i - 1] || x) <= xMax); + // For points within the visible range, including the first point outside the + // visible range, consider y extremes + validValue = (isNumber(y, true) || isArray(y)) && (!yAxis.isLog || (y.length || y > 0)); + withinRange = this.getExtremesFromAll || this.options.getExtremesFromAll || this.cropped || + ((xData[i + 1] || x) >= xMin && (xData[i - 1] || x) <= xMax); - if (validValue && withinRange) { + if (validValue && withinRange) { - j = y.length; - if (j) { // array, like ohlc or range data - while (j--) { - if (y[j] !== null) { - activeYData[activeCounter++] = y[j]; - } + j = y.length; + if (j) { // array, like ohlc or range data + while (j--) { + if (y[j] !== null) { + activeYData[activeCounter++] = y[j]; } - } else { - activeYData[activeCounter++] = y; } + } else { + activeYData[activeCounter++] = y; } } - this.dataMin = arrayMin(activeYData); - this.dataMax = arrayMax(activeYData); - }, + } + this.dataMin = arrayMin(activeYData); + this.dataMax = arrayMax(activeYData); + }, - /** - * Translate data points from raw data values to chart specific positioning data - * needed later in drawPoints, drawGraph and drawTracker. - */ - translate: function() { - if (!this.processedXData) { // hidden series - this.processData(); - } - this.generatePoints(); - var series = this, - options = series.options, - stacking = options.stacking, - xAxis = series.xAxis, - categories = xAxis.categories, - yAxis = series.yAxis, - points = series.points, - dataLength = points.length, - hasModifyValue = !!series.modifyValue, - i, - pointPlacement = options.pointPlacement, - dynamicallyPlaced = pointPlacement === 'between' || isNumber(pointPlacement), - threshold = options.threshold, - stackThreshold = options.startFromThreshold ? threshold : 0, - plotX, - plotY, - lastPlotX, - stackIndicator, - closestPointRangePx = Number.MAX_VALUE; + /** + * Translate data points from raw data values to chart specific positioning + * data needed later in drawPoints, drawGraph and drawTracker. + * + * @function #translate + * @memberOf Series + * @returns {void} + */ + translate: function() { + if (!this.processedXData) { // hidden series + this.processData(); + } + this.generatePoints(); + var series = this, + options = series.options, + stacking = options.stacking, + xAxis = series.xAxis, + categories = xAxis.categories, + yAxis = series.yAxis, + points = series.points, + dataLength = points.length, + hasModifyValue = !!series.modifyValue, + i, + pointPlacement = options.pointPlacement, + dynamicallyPlaced = pointPlacement === 'between' || isNumber(pointPlacement), + threshold = options.threshold, + stackThreshold = options.startFromThreshold ? threshold : 0, + plotX, + plotY, + lastPlotX, + stackIndicator, + closestPointRangePx = Number.MAX_VALUE; - // Translate each point - for (i = 0; i < dataLength; i++) { - var point = points[i], - xValue = point.x, - yValue = point.y, - yBottom = point.low, - stack = stacking && yAxis.stacks[(series.negStacks && yValue < (stackThreshold ? 0 : threshold) ? '-' : '') + series.stackKey], - pointStack, - stackValues; + // Point placement is relative to each series pointRange (#5889) + if (pointPlacement === 'between') { + pointPlacement = 0.5; + } + if (isNumber(pointPlacement)) { + pointPlacement *= pick(options.pointRange || xAxis.pointRange); + } - // Discard disallowed y values for log axes (#3434) - if (yAxis.isLog && yValue !== null && yValue <= 0) { - point.isNull = true; - } + // Translate each point + for (i = 0; i < dataLength; i++) { + var point = points[i], + xValue = point.x, + yValue = point.y, + yBottom = point.low, + stack = stacking && yAxis.stacks[(series.negStacks && yValue < (stackThreshold ? 0 : threshold) ? '-' : '') + series.stackKey], + pointStack, + stackValues; - // Get the plotX translation - point.plotX = plotX = correctFloat( // #5236 - Math.min(Math.max(-1e5, xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement, this.type === 'flags')), 1e5) // #3923 - ); + // Discard disallowed y values for log axes (#3434) + if (yAxis.isLog && yValue !== null && yValue <= 0) { + point.isNull = true; + } - // Calculate the bottom y value for stacked series - if (stacking && series.visible && !point.isNull && stack && stack[xValue]) { - stackIndicator = series.getStackIndicator(stackIndicator, xValue, series.index); - pointStack = stack[xValue]; - stackValues = pointStack.points[stackIndicator.key]; - yBottom = stackValues[0]; - yValue = stackValues[1]; + // Get the plotX translation + point.plotX = plotX = correctFloat( // #5236 + Math.min(Math.max(-1e5, xAxis.translate( + xValue, + 0, + 0, + 0, + 1, + pointPlacement, + this.type === 'flags' + )), 1e5) // #3923 + ); - if (yBottom === stackThreshold && stackIndicator.key === stack[xValue].base) { - yBottom = pick(threshold, yAxis.min); - } - if (yAxis.isLog && yBottom <= 0) { // #1200, #1232 - yBottom = null; - } + // Calculate the bottom y value for stacked series + if (stacking && series.visible && !point.isNull && stack && stack[xValue]) { + stackIndicator = series.getStackIndicator(stackIndicator, xValue, series.index); + pointStack = stack[xValue]; + stackValues = pointStack.points[stackIndicator.key]; + yBottom = stackValues[0]; + yValue = stackValues[1]; - point.total = point.stackTotal = pointStack.total; - point.percentage = pointStack.total && (point.y / pointStack.total * 100); - point.stackY = yValue; + if (yBottom === stackThreshold && stackIndicator.key === stack[xValue].base) { + yBottom = pick(threshold, yAxis.min); + } + if (yAxis.isLog && yBottom <= 0) { // #1200, #1232 + yBottom = null; + } - // Place the stack label - pointStack.setOffset(series.pointXOffset || 0, series.barW || 0); + point.total = point.stackTotal = pointStack.total; + point.percentage = pointStack.total && (point.y / pointStack.total * 100); + point.stackY = yValue; - } + // Place the stack label + pointStack.setOffset(series.pointXOffset || 0, series.barW || 0); - // Set translated yBottom or remove it - point.yBottom = defined(yBottom) ? - yAxis.translate(yBottom, 0, 1, 0, 1) : - null; + } - // general hook, used for Highstock compare mode - if (hasModifyValue) { - yValue = series.modifyValue(yValue, point); - } + // Set translated yBottom or remove it + point.yBottom = defined(yBottom) ? + yAxis.translate(yBottom, 0, 1, 0, 1) : + null; - // Set the the plotY value, reset it for redraws - point.plotY = plotY = (typeof yValue === 'number' && yValue !== Infinity) ? - Math.min(Math.max(-1e5, yAxis.translate(yValue, 0, 1, 0, 1)), 1e5) : // #3201 - undefined; - point.isInside = plotY !== undefined && plotY >= 0 && plotY <= yAxis.len && // #3519 - plotX >= 0 && plotX <= xAxis.len; + // general hook, used for Highstock compare mode + if (hasModifyValue) { + yValue = series.modifyValue(yValue, point); + } + // Set the the plotY value, reset it for redraws + point.plotY = plotY = (typeof yValue === 'number' && yValue !== Infinity) ? + Math.min(Math.max(-1e5, yAxis.translate(yValue, 0, 1, 0, 1)), 1e5) : // #3201 + undefined; - // Set client related positions for mouse tracking - point.clientX = dynamicallyPlaced ? correctFloat(xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement)) : plotX; // #1514, #5383, #5518 + point.isInside = plotY !== undefined && plotY >= 0 && plotY <= yAxis.len && // #3519 + plotX >= 0 && plotX <= xAxis.len; - point.negative = point.y < (threshold || 0); - // some API data - point.category = categories && categories[point.x] !== undefined ? - categories[point.x] : point.x; + // Set client related positions for mouse tracking + point.clientX = dynamicallyPlaced ? correctFloat(xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement)) : plotX; // #1514, #5383, #5518 - // Determine auto enabling of markers (#3635, #5099) - if (!point.isNull) { - if (lastPlotX !== undefined) { - closestPointRangePx = Math.min(closestPointRangePx, Math.abs(plotX - lastPlotX)); - } - lastPlotX = plotX; - } + point.negative = point.y < (threshold || 0); - } - series.closestPointRangePx = closestPointRangePx; - }, + // some API data + point.category = categories && categories[point.x] !== undefined ? + categories[point.x] : point.x; - /** - * Return the series points with null points filtered out - */ - getValidPoints: function(points, insideOnly) { - var chart = this.chart; - return grep(points || this.points || [], function isValidPoint(point) { // #3916, #5029 - if (insideOnly && !chart.isInsidePlot(point.plotX, point.plotY, chart.inverted)) { // #5085 - return false; + // Determine auto enabling of markers (#3635, #5099) + if (!point.isNull) { + if (lastPlotX !== undefined) { + closestPointRangePx = Math.min(closestPointRangePx, Math.abs(plotX - lastPlotX)); } - return !point.isNull; - }); - }, + lastPlotX = plotX; + } - /** - * Set the clipping for the series. For animated series it is called twice, first to initiate - * animating the clip then the second time without the animation to set the final clip. - */ - setClip: function(animation) { - var chart = this.chart, - options = this.options, - renderer = chart.renderer, - inverted = chart.inverted, - seriesClipBox = this.clipBox, - clipBox = seriesClipBox || chart.clipBox, - sharedClipKey = this.sharedClipKey || ['_sharedClip', animation && animation.duration, animation && animation.easing, clipBox.height, options.xAxis, options.yAxis].join(','), // #4526 - clipRect = chart[sharedClipKey], - markerClipRect = chart[sharedClipKey + 'm']; + } + series.closestPointRangePx = closestPointRangePx; + }, - // If a clipping rectangle with the same properties is currently present in the chart, use that. - if (!clipRect) { + /** + * Return the series points with null points filtered out + */ + getValidPoints: function(points, insideOnly) { + var chart = this.chart; + return grep(points || this.points || [], function isValidPoint(point) { // #3916, #5029 + if (insideOnly && !chart.isInsidePlot(point.plotX, point.plotY, chart.inverted)) { // #5085 + return false; + } + return !point.isNull; + }); + }, - // When animation is set, prepare the initial positions - if (animation) { - clipBox.width = 0; + /** + * Set the clipping for the series. For animated series it is called twice, first to initiate + * animating the clip then the second time without the animation to set the final clip. + */ + setClip: function(animation) { + var chart = this.chart, + options = this.options, + renderer = chart.renderer, + inverted = chart.inverted, + seriesClipBox = this.clipBox, + clipBox = seriesClipBox || chart.clipBox, + sharedClipKey = this.sharedClipKey || ['_sharedClip', animation && animation.duration, animation && animation.easing, clipBox.height, options.xAxis, options.yAxis].join(','), // #4526 + clipRect = chart[sharedClipKey], + markerClipRect = chart[sharedClipKey + 'm']; - chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect(-99, // include the width of the first marker - inverted ? -chart.plotLeft : -chart.plotTop, - 99, - inverted ? chart.chartWidth : chart.chartHeight - ); - } - chart[sharedClipKey] = clipRect = renderer.clipRect(clipBox); - // Create hashmap for series indexes - clipRect.count = { - length: 0 - }; + // If a clipping rectangle with the same properties is currently present in the chart, use that. + if (!clipRect) { - } + // When animation is set, prepare the initial positions if (animation) { - if (!clipRect.count[this.index]) { - clipRect.count[this.index] = true; - clipRect.count.length += 1; - } + clipBox.width = 0; + + chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect(-99, // include the width of the first marker + inverted ? -chart.plotLeft : -chart.plotTop, + 99, + inverted ? chart.chartWidth : chart.chartHeight + ); } + chart[sharedClipKey] = clipRect = renderer.clipRect(clipBox); + // Create hashmap for series indexes + clipRect.count = { + length: 0 + }; - if (options.clip !== false) { - this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect); - this.markerGroup.clip(markerClipRect); - this.sharedClipKey = sharedClipKey; + } + if (animation) { + if (!clipRect.count[this.index]) { + clipRect.count[this.index] = true; + clipRect.count.length += 1; } + } - // Remove the shared clipping rectangle when all series are shown - if (!animation) { - if (clipRect.count[this.index]) { - delete clipRect.count[this.index]; - clipRect.count.length -= 1; - } + if (options.clip !== false) { + this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect); + this.markerGroup.clip(markerClipRect); + this.sharedClipKey = sharedClipKey; + } - if (clipRect.count.length === 0 && sharedClipKey && chart[sharedClipKey]) { - if (!seriesClipBox) { - chart[sharedClipKey] = chart[sharedClipKey].destroy(); - } - if (chart[sharedClipKey + 'm']) { - chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy(); - } - } + // Remove the shared clipping rectangle when all series are shown + if (!animation) { + if (clipRect.count[this.index]) { + delete clipRect.count[this.index]; + clipRect.count.length -= 1; } - }, - /** - * Animate in the series - */ - animate: function(init) { - var series = this, - chart = series.chart, - clipRect, - animation = animObject(series.options.animation), - sharedClipKey; - - // Initialize the animation. Set up the clipping rectangle. - if (init) { - - series.setClip(animation); - - // Run the animation - } else { - sharedClipKey = this.sharedClipKey; - clipRect = chart[sharedClipKey]; - if (clipRect) { - clipRect.animate({ - width: chart.plotSizeX - }, animation); + if (clipRect.count.length === 0 && sharedClipKey && chart[sharedClipKey]) { + if (!seriesClipBox) { + chart[sharedClipKey] = chart[sharedClipKey].destroy(); } if (chart[sharedClipKey + 'm']) { - chart[sharedClipKey + 'm'].animate({ - width: chart.plotSizeX + 99 - }, animation); + chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy(); } + } + } + }, - // Delete this function to allow it only once - series.animate = null; + /** + * Animate in the series + */ + animate: function(init) { + var series = this, + chart = series.chart, + clipRect, + animation = animObject(series.options.animation), + sharedClipKey; + // Initialize the animation. Set up the clipping rectangle. + if (init) { + + series.setClip(animation); + + // Run the animation + } else { + sharedClipKey = this.sharedClipKey; + clipRect = chart[sharedClipKey]; + if (clipRect) { + clipRect.animate({ + width: chart.plotSizeX + }, animation); } - }, + if (chart[sharedClipKey + 'm']) { + chart[sharedClipKey + 'm'].animate({ + width: chart.plotSizeX + 99 + }, animation); + } - /** - * This runs after animation to land on the final plot clipping - */ - afterAnimate: function() { - this.setClip(); - fireEvent(this, 'afterAnimate'); - }, + // Delete this function to allow it only once + series.animate = null; - /** - * Draw the markers - */ - drawPoints: function() { - var series = this, - points = series.points, - chart = series.chart, - plotX, - plotY, - i, - point, - radius, - symbol, - isImage, - graphic, - options = series.options, - seriesMarkerOptions = options.marker, - pointMarkerOptions, - hasPointMarker, - enabled, - isInside, - markerGroup = series.markerGroup, - xAxis = series.xAxis, - globallyEnabled = pick( - seriesMarkerOptions.enabled, - xAxis.isRadial ? true : null, - series.closestPointRangePx > 2 * seriesMarkerOptions.radius - ); + } + }, - if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) { + /** + * This runs after animation to land on the final plot clipping + */ + afterAnimate: function() { + this.setClip(); + fireEvent(this, 'afterAnimate'); + }, - i = points.length; - while (i--) { - point = points[i]; - plotX = Math.floor(point.plotX); // #1843 - plotY = point.plotY; - graphic = point.graphic; - pointMarkerOptions = point.marker || {}; - hasPointMarker = !!point.marker; - enabled = (globallyEnabled && pointMarkerOptions.enabled === undefined) || pointMarkerOptions.enabled; - isInside = point.isInside; + /** + * Draw the markers. + * + * @function #drawPoints + * @memberOf Series + * @returns {void} + */ + drawPoints: function() { + var series = this, + points = series.points, + chart = series.chart, + plotY, + i, + point, + symbol, + graphic, + options = series.options, + seriesMarkerOptions = options.marker, + pointMarkerOptions, + hasPointMarker, + enabled, + isInside, + markerGroup = series.markerGroup, + xAxis = series.xAxis, + markerAttribs, + globallyEnabled = pick( + seriesMarkerOptions.enabled, + xAxis.isRadial ? true : null, + series.closestPointRangePx > 2 * seriesMarkerOptions.radius + ); - // only draw the point if y is defined - if (enabled && isNumber(plotY) && point.y !== null) { + if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) { - // Shortcuts - radius = seriesMarkerOptions.radius; - symbol = pick(pointMarkerOptions.symbol, series.symbol); - isImage = symbol.indexOf('url') === 0; + i = points.length; + while (i--) { + point = points[i]; + plotY = point.plotY; + graphic = point.graphic; + pointMarkerOptions = point.marker || {}; + hasPointMarker = !!point.marker; + enabled = (globallyEnabled && pointMarkerOptions.enabled === undefined) || pointMarkerOptions.enabled; + isInside = point.isInside; - if (graphic) { // update - graphic[isInside ? 'show' : 'hide'](true) // Since the marker group isn't clipped, each individual marker must be toggled - //.attr(pointAttr) // #4759 - .animate(extend({ - x: plotX - radius, - y: plotY - radius - }, graphic.symbolName ? { // don't apply to image symbols #507 - width: 2 * radius, - height: 2 * radius - } : {})); - } else if (isInside && (radius > 0 || isImage)) { - point.graphic = graphic = chart.renderer.symbol( - symbol, - plotX - radius, - plotY - radius, - 2 * radius, - 2 * radius, - hasPointMarker ? pointMarkerOptions : seriesMarkerOptions - ) - .attr({ - r: radius - }) - .add(markerGroup); - } + // only draw the point if y is defined + if (enabled && isNumber(plotY) && point.y !== null) { + // Shortcuts + symbol = pick(pointMarkerOptions.symbol, series.symbol); + point.hasImage = symbol.indexOf('url') === 0; - // Presentational attributes - if (graphic) { - graphic.attr(series.pointAttribs(point, point.selected && 'select')); - } + markerAttribs = series.markerAttribs( + point, + point.selected && 'select' + ); + if (graphic) { // update + graphic[isInside ? 'show' : 'hide'](true) // Since the marker group isn't clipped, each individual marker must be toggled + .animate(markerAttribs); + } else if (isInside && (markerAttribs.width > 0 || point.hasImage)) { + point.graphic = graphic = chart.renderer.symbol( + symbol, + markerAttribs.x, + markerAttribs.y, + markerAttribs.width, + markerAttribs.height, + hasPointMarker ? pointMarkerOptions : seriesMarkerOptions + ) + .add(markerGroup); + } - if (graphic) { - graphic.addClass(point.getClassName(), true); - } - } else if (graphic) { - point.graphic = graphic.destroy(); // #1269 + // Presentational attributes + if (graphic) { + graphic.attr(series.pointAttribs(point, point.selected && 'select')); } + + + if (graphic) { + graphic.addClass(point.getClassName(), true); + } + + } else if (graphic) { + point.graphic = graphic.destroy(); // #1269 } } + } - }, + }, + /** + * Get non-presentational attributes for the point. + */ + markerAttribs: function(point, state) { + var seriesMarkerOptions = this.options.marker, + seriesStateOptions, + pointOptions = point && point.options, + pointMarkerOptions = (pointOptions && pointOptions.marker) || {}, + pointStateOptions, + radius = pick( + pointMarkerOptions.radius, + seriesMarkerOptions.radius + ), + attribs; - /** - * Get presentational attributes for marker-based series (line, spline, scatter, bubble, mappoint...) - */ - pointAttribs: function(point, state) { - var seriesMarkerOptions = this.options.marker, - seriesStateOptions, - pointOptions = point && point.options, - pointMarkerOptions = (pointOptions && pointOptions.marker) || {}, - pointStateOptions, - strokeWidth = seriesMarkerOptions.lineWidth, - color = this.color, - pointColorOption = pointOptions && pointOptions.color, - pointColor = point && point.color, - zoneColor, - fill, - stroke, - zone; + // Handle hover and select states + if (state) { + seriesStateOptions = seriesMarkerOptions.states[state]; + pointStateOptions = pointMarkerOptions.states && + pointMarkerOptions.states[state]; - if (point && this.zones.length) { - zone = point.getZone(); - if (zone && zone.color) { - zoneColor = zone.color; - } - } + radius = pick( + pointStateOptions && pointStateOptions.radius, + seriesStateOptions && seriesStateOptions.radius, + radius + (seriesStateOptions && seriesStateOptions.radiusPlus || 0) + ); + } - color = pointColorOption || zoneColor || pointColor || color; - fill = pointMarkerOptions.fillColor || seriesMarkerOptions.fillColor || color; - stroke = pointMarkerOptions.lineColor || seriesMarkerOptions.lineColor || color; + if (point.hasImage) { + radius = 0; // and subsequently width and height is not set + } - // Handle hover and select states - if (state) { - seriesStateOptions = seriesMarkerOptions.states[state]; - pointStateOptions = (pointMarkerOptions.states && pointMarkerOptions.states[state]) || {}; - strokeWidth = seriesStateOptions.lineWidth || strokeWidth + seriesStateOptions.lineWidthPlus; - fill = pointStateOptions.fillColor || seriesStateOptions.fillColor || fill; - stroke = pointStateOptions.lineColor || seriesStateOptions.lineColor || stroke; - } + attribs = { + x: Math.floor(point.plotX) - radius, // Math.floor for #1843 + y: point.plotY - radius + }; - return { - 'stroke': stroke, - 'stroke-width': strokeWidth, - 'fill': fill - }; - }, + if (radius) { + attribs.width = attribs.height = 2 * radius; + } - /** - * Clear DOM objects and free up memory - */ - destroy: function() { - var series = this, - chart = series.chart, - issue134 = /AppleWebKit\/533/.test(win.navigator.userAgent), - destroy, - i, - data = series.data || [], - point, - prop, - axis; + return attribs; - // add event hook - fireEvent(series, 'destroy'); + }, - // remove all events - removeEvent(series); - // erase from axes - each(series.axisTypes || [], function(AXIS) { - axis = series[AXIS]; - if (axis && axis.series) { - erase(axis.series, series); - axis.isDirty = axis.forceRedraw = true; - } - }); + /** + * Get presentational attributes for marker-based series (line, spline, scatter, bubble, mappoint...) + */ + pointAttribs: function(point, state) { + var seriesMarkerOptions = this.options.marker, + seriesStateOptions, + pointOptions = point && point.options, + pointMarkerOptions = (pointOptions && pointOptions.marker) || {}, + pointStateOptions, + color = this.color, + pointColorOption = pointOptions && pointOptions.color, + pointColor = point && point.color, + strokeWidth = pick( + pointMarkerOptions.lineWidth, + seriesMarkerOptions.lineWidth + ), + zoneColor, + fill, + stroke, + zone; - // remove legend items - if (series.legendItem) { - series.chart.legend.destroyItem(series); + if (point && this.zones.length) { + zone = point.getZone(); + if (zone && zone.color) { + zoneColor = zone.color; } + } - // destroy all points with their elements - i = data.length; - while (i--) { - point = data[i]; - if (point && point.destroy) { - point.destroy(); - } - } - series.points = null; + color = pointColorOption || zoneColor || pointColor || color; + fill = pointMarkerOptions.fillColor || seriesMarkerOptions.fillColor || color; + stroke = pointMarkerOptions.lineColor || seriesMarkerOptions.lineColor || color; - // Clear the animation timeout if we are destroying the series during initial animation - clearTimeout(series.animationTimeout); + // Handle hover and select states + if (state) { + seriesStateOptions = seriesMarkerOptions.states[state]; + pointStateOptions = (pointMarkerOptions.states && pointMarkerOptions.states[state]) || {}; + strokeWidth = pick( + pointStateOptions.lineWidth, + seriesStateOptions.lineWidth, + strokeWidth + pick( + pointStateOptions.lineWidthPlus, + seriesStateOptions.lineWidthPlus, + 0 + ) + ); + fill = pointStateOptions.fillColor || seriesStateOptions.fillColor || fill; + stroke = pointStateOptions.lineColor || seriesStateOptions.lineColor || stroke; + } - // Destroy all SVGElements associated to the series - for (prop in series) { - if (series[prop] instanceof SVGElement && !series[prop].survive) { // Survive provides a hook for not destroying + return { + 'stroke': stroke, + 'stroke-width': strokeWidth, + 'fill': fill + }; + }, - // issue 134 workaround - destroy = issue134 && prop === 'group' ? - 'hide' : - 'destroy'; + /** + * Clear DOM objects and free up memory + */ + destroy: function() { + var series = this, + chart = series.chart, + issue134 = /AppleWebKit\/533/.test(win.navigator.userAgent), + destroy, + i, + data = series.data || [], + point, + prop, + axis; - series[prop][destroy](); - } - } + // add event hook + fireEvent(series, 'destroy'); - // remove from hoverSeries - if (chart.hoverSeries === series) { - chart.hoverSeries = null; + // remove all events + removeEvent(series); + + // erase from axes + each(series.axisTypes || [], function(AXIS) { + axis = series[AXIS]; + if (axis && axis.series) { + erase(axis.series, series); + axis.isDirty = axis.forceRedraw = true; } - erase(chart.series, series); + }); - // clear all members - for (prop in series) { - delete series[prop]; + // remove legend items + if (series.legendItem) { + series.chart.legend.destroyItem(series); + } + + // destroy all points with their elements + i = data.length; + while (i--) { + point = data[i]; + if (point && point.destroy) { + point.destroy(); } - }, + } + series.points = null; - /** - * Get the graph path - */ - getGraphPath: function(points, nullsAsZeroes, connectCliffs) { - var series = this, - options = series.options, - step = options.step, - reversed, - graphPath = [], - xMap = [], - gap; + // Clear the animation timeout if we are destroying the series during initial animation + clearTimeout(series.animationTimeout); - points = points || series.points; + // Destroy all SVGElements associated to the series + for (prop in series) { + if (series[prop] instanceof SVGElement && !series[prop].survive) { // Survive provides a hook for not destroying - // Bottom of a stack is reversed - reversed = points.reversed; - if (reversed) { - points.reverse(); - } - // Reverse the steps (#5004) - step = { - right: 1, - center: 2 - }[step] || (step && 3); - if (step && reversed) { - step = 4 - step; - } + // issue 134 workaround + destroy = issue134 && prop === 'group' ? + 'hide' : + 'destroy'; - // Remove invalid points, especially in spline (#5015) - if (options.connectNulls && !nullsAsZeroes && !connectCliffs) { - points = this.getValidPoints(points); + series[prop][destroy](); } + } - // Build the line - each(points, function(point, i) { + // remove from hoverSeries + if (chart.hoverSeries === series) { + chart.hoverSeries = null; + } + erase(chart.series, series); - var plotX = point.plotX, - plotY = point.plotY, - lastPoint = points[i - 1], - pathToPoint; // the path to this point from the previous + // clear all members + for (prop in series) { + delete series[prop]; + } + }, - if ((point.leftCliff || (lastPoint && lastPoint.rightCliff)) && !connectCliffs) { - gap = true; // ... and continue - } + /** + * Get the graph path + */ + getGraphPath: function(points, nullsAsZeroes, connectCliffs) { + var series = this, + options = series.options, + step = options.step, + reversed, + graphPath = [], + xMap = [], + gap; - // Line series, nullsAsZeroes is not handled - if (point.isNull && !defined(nullsAsZeroes) && i > 0) { - gap = !options.connectNulls; + points = points || series.points; - // Area series, nullsAsZeroes is set - } else if (point.isNull && !nullsAsZeroes) { - gap = true; + // Bottom of a stack is reversed + reversed = points.reversed; + if (reversed) { + points.reverse(); + } + // Reverse the steps (#5004) + step = { + right: 1, + center: 2 + }[step] || (step && 3); + if (step && reversed) { + step = 4 - step; + } - } else { + // Remove invalid points, especially in spline (#5015) + if (options.connectNulls && !nullsAsZeroes && !connectCliffs) { + points = this.getValidPoints(points); + } - if (i === 0 || gap) { - pathToPoint = ['M', point.plotX, point.plotY]; + // Build the line + each(points, function(point, i) { - } else if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object + var plotX = point.plotX, + plotY = point.plotY, + lastPoint = points[i - 1], + pathToPoint; // the path to this point from the previous - pathToPoint = series.getPointSpline(points, point, i); + if ((point.leftCliff || (lastPoint && lastPoint.rightCliff)) && !connectCliffs) { + gap = true; // ... and continue + } - } else if (step) { + // Line series, nullsAsZeroes is not handled + if (point.isNull && !defined(nullsAsZeroes) && i > 0) { + gap = !options.connectNulls; - if (step === 1) { // right - pathToPoint = [ - 'L', - lastPoint.plotX, - plotY - ]; + // Area series, nullsAsZeroes is set + } else if (point.isNull && !nullsAsZeroes) { + gap = true; - } else if (step === 2) { // center - pathToPoint = [ - 'L', - (lastPoint.plotX + plotX) / 2, - lastPoint.plotY, - 'L', - (lastPoint.plotX + plotX) / 2, - plotY - ]; + } else { - } else { - pathToPoint = [ - 'L', - plotX, - lastPoint.plotY - ]; - } - pathToPoint.push('L', plotX, plotY); + if (i === 0 || gap) { + pathToPoint = ['M', point.plotX, point.plotY]; + } else if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object + + pathToPoint = series.getPointSpline(points, point, i); + + } else if (step) { + + if (step === 1) { // right + pathToPoint = [ + 'L', + lastPoint.plotX, + plotY + ]; + + } else if (step === 2) { // center + pathToPoint = [ + 'L', + (lastPoint.plotX + plotX) / 2, + lastPoint.plotY, + 'L', + (lastPoint.plotX + plotX) / 2, + plotY + ]; + } else { - // normal line to next point pathToPoint = [ 'L', plotX, - plotY + lastPoint.plotY ]; } + pathToPoint.push('L', plotX, plotY); - // Prepare for animation. When step is enabled, there are two path nodes for each x value. - xMap.push(point.x); - if (step) { - xMap.push(point.x); - } + } else { + // normal line to next point + pathToPoint = [ + 'L', + plotX, + plotY + ]; + } - graphPath.push.apply(graphPath, pathToPoint); - gap = false; + // Prepare for animation. When step is enabled, there are two path nodes for each x value. + xMap.push(point.x); + if (step) { + xMap.push(point.x); } - }); - graphPath.xMap = xMap; - series.graphPath = graphPath; + graphPath.push.apply(graphPath, pathToPoint); + gap = false; + } + }); - return graphPath; + graphPath.xMap = xMap; + series.graphPath = graphPath; - }, + return graphPath; - /** - * Draw the actual graph - */ - drawGraph: function() { - var series = this, - options = this.options, - graphPath = (this.gappedPath || this.getGraphPath).call(this), - props = [ - [ - 'graph', - 'highcharts-graph', + }, - options.lineColor || this.color, - options.dashStyle + /** + * Draw the actual graph + */ + drawGraph: function() { + var series = this, + options = this.options, + graphPath = (this.gappedPath || this.getGraphPath).call(this), + props = [ + [ + 'graph', + 'highcharts-graph', - ] - ]; + options.lineColor || this.color, + options.dashStyle - // Add the zone properties if any - each(this.zones, function(zone, i) { - props.push([ - 'zone-graph-' + i, - 'highcharts-graph highcharts-zone-graph-' + i + ' ' + (zone.className || ''), + ] + ]; - zone.color || series.color, - zone.dashStyle || options.dashStyle + // Add the zone properties if any + each(this.zones, function(zone, i) { + props.push([ + 'zone-graph-' + i, + 'highcharts-graph highcharts-zone-graph-' + i + ' ' + (zone.className || ''), - ]); - }); + zone.color || series.color, + zone.dashStyle || options.dashStyle - // Draw the graph - each(props, function(prop, i) { - var graphKey = prop[0], - graph = series[graphKey], - attribs; + ]); + }); - if (graph) { - graph.endX = graphPath.xMap; - graph.animate({ - d: graphPath - }); + // Draw the graph + each(props, function(prop, i) { + var graphKey = prop[0], + graph = series[graphKey], + attribs; - } else if (graphPath.length) { // #1487 + if (graph) { + graph.endX = graphPath.xMap; + graph.animate({ + d: graphPath + }); - series[graphKey] = series.chart.renderer.path(graphPath) - .addClass(prop[1]) - .attr({ - zIndex: 1 - }) // #1069 - .add(series.group); + } else if (graphPath.length) { // #1487 + series[graphKey] = series.chart.renderer.path(graphPath) + .addClass(prop[1]) + .attr({ + zIndex: 1 + }) // #1069 + .add(series.group); - attribs = { - 'stroke': prop[2], - 'stroke-width': options.lineWidth, - 'fill': (series.fillGraph && series.color) || 'none' // Polygon series use filled graph - }; - if (prop[3]) { - attribs.dashstyle = prop[3]; - } else if (options.linecap !== 'square') { - attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round'; - } + attribs = { + 'stroke': prop[2], + 'stroke-width': options.lineWidth, + 'fill': (series.fillGraph && series.color) || 'none' // Polygon series use filled graph + }; - graph = series[graphKey] - .attr(attribs) - .shadow((i < 2) && options.shadow); // add shadow to normal series (0) or to first zone (1) #3932 - + if (prop[3]) { + attribs.dashstyle = prop[3]; + } else if (options.linecap !== 'square') { + attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round'; } - // Helpers for animation - if (graph) { - graph.startX = graphPath.xMap; - //graph.shiftUnit = options.step ? 2 : 1; - graph.isArea = graphPath.isArea; // For arearange animation - } - }); - }, + graph = series[graphKey] + .attr(attribs) + .shadow((i < 2) && options.shadow); // add shadow to normal series (0) or to first zone (1) #3932 - /** - * Clip the graphs into the positive and negative coloured graphs - */ - applyZones: function() { - var series = this, - chart = this.chart, - renderer = chart.renderer, - zones = this.zones, - translatedFrom, - translatedTo, - clips = this.clips || [], - clipAttr, - graph = this.graph, - area = this.area, - chartSizeMax = Math.max(chart.chartWidth, chart.chartHeight), - axis = this[(this.zoneAxis || 'y') + 'Axis'], - extremes, - reversed, - inverted = chart.inverted, - horiz, - pxRange, - pxPosMin, - pxPosMax, - ignoreZones = false; + } - if (zones.length && (graph || area) && axis && axis.min !== undefined) { - reversed = axis.reversed; - horiz = axis.horiz; - // The use of the Color Threshold assumes there are no gaps - // so it is safe to hide the original graph and area - if (graph) { - graph.hide(); - } - if (area) { - area.hide(); - } + // Helpers for animation + if (graph) { + graph.startX = graphPath.xMap; + //graph.shiftUnit = options.step ? 2 : 1; + graph.isArea = graphPath.isArea; // For arearange animation + } + }); + }, - // Create the clips - extremes = axis.getExtremes(); - each(zones, function(threshold, i) { + /** + * Clip the graphs into the positive and negative coloured graphs + */ + applyZones: function() { + var series = this, + chart = this.chart, + renderer = chart.renderer, + zones = this.zones, + translatedFrom, + translatedTo, + clips = this.clips || [], + clipAttr, + graph = this.graph, + area = this.area, + chartSizeMax = Math.max(chart.chartWidth, chart.chartHeight), + axis = this[(this.zoneAxis || 'y') + 'Axis'], + extremes, + reversed, + inverted = chart.inverted, + horiz, + pxRange, + pxPosMin, + pxPosMax, + ignoreZones = false; - translatedFrom = reversed ? - (horiz ? chart.plotWidth : 0) : - (horiz ? 0 : axis.toPixels(extremes.min)); - translatedFrom = Math.min(Math.max(pick(translatedTo, translatedFrom), 0), chartSizeMax); - translatedTo = Math.min(Math.max(Math.round(axis.toPixels(pick(threshold.value, extremes.max), true)), 0), chartSizeMax); + if (zones.length && (graph || area) && axis && axis.min !== undefined) { + reversed = axis.reversed; + horiz = axis.horiz; + // The use of the Color Threshold assumes there are no gaps + // so it is safe to hide the original graph and area + if (graph) { + graph.hide(); + } + if (area) { + area.hide(); + } - if (ignoreZones) { - translatedFrom = translatedTo = axis.toPixels(extremes.max); + // Create the clips + extremes = axis.getExtremes(); + each(zones, function(threshold, i) { + + translatedFrom = reversed ? + (horiz ? chart.plotWidth : 0) : + (horiz ? 0 : axis.toPixels(extremes.min)); + translatedFrom = Math.min(Math.max(pick(translatedTo, translatedFrom), 0), chartSizeMax); + translatedTo = Math.min(Math.max(Math.round(axis.toPixels(pick(threshold.value, extremes.max), true)), 0), chartSizeMax); + + if (ignoreZones) { + translatedFrom = translatedTo = axis.toPixels(extremes.max); + } + + pxRange = Math.abs(translatedFrom - translatedTo); + pxPosMin = Math.min(translatedFrom, translatedTo); + pxPosMax = Math.max(translatedFrom, translatedTo); + if (axis.isXAxis) { + clipAttr = { + x: inverted ? pxPosMax : pxPosMin, + y: 0, + width: pxRange, + height: chartSizeMax + }; + if (!horiz) { + clipAttr.x = chart.plotHeight - clipAttr.x; } + } else { + clipAttr = { + x: 0, + y: inverted ? pxPosMax : pxPosMin, + width: chartSizeMax, + height: pxRange + }; + if (horiz) { + clipAttr.y = chart.plotWidth - clipAttr.y; + } + } - pxRange = Math.abs(translatedFrom - translatedTo); - pxPosMin = Math.min(translatedFrom, translatedTo); - pxPosMax = Math.max(translatedFrom, translatedTo); + + /// VML SUPPPORT + if (inverted && renderer.isVML) { if (axis.isXAxis) { clipAttr = { - x: inverted ? pxPosMax : pxPosMin, - y: 0, - width: pxRange, - height: chartSizeMax + x: 0, + y: reversed ? pxPosMin : pxPosMax, + height: clipAttr.width, + width: chart.chartWidth }; - if (!horiz) { - clipAttr.x = chart.plotHeight - clipAttr.x; - } } else { clipAttr = { - x: 0, - y: inverted ? pxPosMax : pxPosMin, - width: chartSizeMax, - height: pxRange + x: clipAttr.y - chart.plotLeft - chart.spacingBox.x, + y: 0, + width: clipAttr.height, + height: chart.chartHeight }; - if (horiz) { - clipAttr.y = chart.plotWidth - clipAttr.y; - } } + } + /// END OF VML SUPPORT - /// VML SUPPPORT - if (inverted && renderer.isVML) { - if (axis.isXAxis) { - clipAttr = { - x: 0, - y: reversed ? pxPosMin : pxPosMax, - height: clipAttr.width, - width: chart.chartWidth - }; - } else { - clipAttr = { - x: clipAttr.y - chart.plotLeft - chart.spacingBox.x, - y: 0, - width: clipAttr.height, - height: chart.chartHeight - }; - } + if (clips[i]) { + clips[i].animate(clipAttr); + } else { + clips[i] = renderer.clipRect(clipAttr); + + if (graph) { + series['zone-graph-' + i].clip(clips[i]); } - /// END OF VML SUPPORT - - if (clips[i]) { - clips[i].animate(clipAttr); - } else { - clips[i] = renderer.clipRect(clipAttr); - - if (graph) { - series['zone-graph-' + i].clip(clips[i]); - } - - if (area) { - series['zone-area-' + i].clip(clips[i]); - } + if (area) { + series['zone-area-' + i].clip(clips[i]); } - // if this zone extends out of the axis, ignore the others - ignoreZones = threshold.value > extremes.max; - }); - this.clips = clips; - } - }, + } + // if this zone extends out of the axis, ignore the others + ignoreZones = threshold.value > extremes.max; + }); + this.clips = clips; + } + }, - /** - * Initialize and perform group inversion on series.group and series.markerGroup - */ - invertGroups: function(inverted) { - var series = this, - chart = series.chart; + /** + * Initialize and perform group inversion on series.group and series.markerGroup + */ + invertGroups: function(inverted) { + var series = this, + chart = series.chart, + remover; - // Pie, go away (#1736) - if (!series.xAxis) { - return; - } + function setInvert() { + var size = { + width: series.yAxis.len, + height: series.xAxis.len + }; - // A fixed size is needed for inversion to work - function setInvert() { - var size = { - width: series.yAxis.len, - height: series.xAxis.len - }; - - each(['group', 'markerGroup'], function(groupName) { - if (series[groupName]) { - series[groupName].attr(size).invert(inverted); - } - }); - } - - addEvent(chart, 'resize', setInvert); // do it on resize - addEvent(series, 'destroy', function() { - removeEvent(chart, 'resize', setInvert); + each(['group', 'markerGroup'], function(groupName) { + if (series[groupName]) { + series[groupName].attr(size).invert(inverted); + } }); + } - // Do it now - setInvert(inverted); // do it now + // Pie, go away (#1736) + if (!series.xAxis) { + return; + } - // On subsequent render and redraw, just do setInvert without setting up events again - series.invertGroups = setInvert; - }, + // A fixed size is needed for inversion to work + remover = addEvent(chart, 'resize', setInvert); + addEvent(series, 'destroy', remover); - /** - * General abstraction for creating plot groups like series.group, series.dataLabelsGroup and - * series.markerGroup. On subsequent calls, the group will only be adjusted to the updated plot size. - */ - plotGroup: function(prop, name, visibility, zIndex, parent) { - var group = this[prop], - isNew = !group; + // Do it now + setInvert(inverted); // do it now - // Generate it on first call - if (isNew) { - this[prop] = group = this.chart.renderer.g(name) - .attr({ - zIndex: zIndex || 0.1 // IE8 and pointer logic use this - }) - .add(parent); + // On subsequent render and redraw, just do setInvert without setting up events again + series.invertGroups = setInvert; + }, - group.addClass('highcharts-series-' + this.index + ' highcharts-' + this.type + '-series highcharts-color-' + this.colorIndex + - ' ' + (this.options.className || '')); - } + /** + * General abstraction for creating plot groups like series.group, series.dataLabelsGroup and + * series.markerGroup. On subsequent calls, the group will only be adjusted to the updated plot size. + */ + plotGroup: function(prop, name, visibility, zIndex, parent) { + var group = this[prop], + isNew = !group; - // Place it on first and subsequent (redraw) calls - group.attr({ - visibility: visibility - })[isNew ? 'attr' : 'animate'](this.getPlotBox()); - return group; - }, + // Generate it on first call + if (isNew) { + this[prop] = group = this.chart.renderer.g(name) + .attr({ + zIndex: zIndex || 0.1 // IE8 and pointer logic use this + }) + .add(parent); - /** - * Get the translation and scale for the plot area of this series - */ - getPlotBox: function() { - var chart = this.chart, - xAxis = this.xAxis, - yAxis = this.yAxis; + group.addClass('highcharts-series-' + this.index + ' highcharts-' + this.type + '-series highcharts-color-' + this.colorIndex + + ' ' + (this.options.className || '')); + } - // Swap axes for inverted (#2339) - if (chart.inverted) { - xAxis = yAxis; - yAxis = this.xAxis; - } - return { - translateX: xAxis ? xAxis.left : chart.plotLeft, - translateY: yAxis ? yAxis.top : chart.plotTop, - scaleX: 1, // #1623 - scaleY: 1 - }; - }, + // Place it on first and subsequent (redraw) calls + group.attr({ + visibility: visibility + })[isNew ? 'attr' : 'animate'](this.getPlotBox()); + return group; + }, - /** - * Render the graph and markers - */ - render: function() { - var series = this, - chart = series.chart, - group, - options = series.options, - // Animation doesn't work in IE8 quirks when the group div is hidden, - // and looks bad in other oldIE - animDuration = !!series.animate && chart.renderer.isSVG && animObject(options.animation).duration, - visibility = series.visible ? 'inherit' : 'hidden', // #2597 - zIndex = options.zIndex, - hasRendered = series.hasRendered, - chartSeriesGroup = chart.seriesGroup, - inverted = chart.inverted; + /** + * Get the translation and scale for the plot area of this series + */ + getPlotBox: function() { + var chart = this.chart, + xAxis = this.xAxis, + yAxis = this.yAxis; - // the group - group = series.plotGroup( - 'group', - 'series', - visibility, - zIndex, - chartSeriesGroup - ); + // Swap axes for inverted (#2339) + if (chart.inverted) { + xAxis = yAxis; + yAxis = this.xAxis; + } + return { + translateX: xAxis ? xAxis.left : chart.plotLeft, + translateY: yAxis ? yAxis.top : chart.plotTop, + scaleX: 1, // #1623 + scaleY: 1 + }; + }, - series.markerGroup = series.plotGroup( - 'markerGroup', - 'markers', - visibility, - zIndex, - chartSeriesGroup - ); + /** + * Render the graph and markers + */ + render: function() { + var series = this, + chart = series.chart, + group, + options = series.options, + // Animation doesn't work in IE8 quirks when the group div is hidden, + // and looks bad in other oldIE + animDuration = !!series.animate && chart.renderer.isSVG && animObject(options.animation).duration, + visibility = series.visible ? 'inherit' : 'hidden', // #2597 + zIndex = options.zIndex, + hasRendered = series.hasRendered, + chartSeriesGroup = chart.seriesGroup, + inverted = chart.inverted; - // initiate the animation - if (animDuration) { - series.animate(true); - } + // the group + group = series.plotGroup( + 'group', + 'series', + visibility, + zIndex, + chartSeriesGroup + ); - // SVGRenderer needs to know this before drawing elements (#1089, #1795) - group.inverted = series.isCartesian ? inverted : false; + series.markerGroup = series.plotGroup( + 'markerGroup', + 'markers', + visibility, + zIndex, + chartSeriesGroup + ); - // draw the graph if any - if (series.drawGraph) { - series.drawGraph(); - series.applyZones(); - } + // initiate the animation + if (animDuration) { + series.animate(true); + } - /* each(series.points, function (point) { - if (point.redraw) { - point.redraw(); - } - });*/ + // SVGRenderer needs to know this before drawing elements (#1089, #1795) + group.inverted = series.isCartesian ? inverted : false; - // draw the data labels (inn pies they go before the points) - if (series.drawDataLabels) { - series.drawDataLabels(); - } + // draw the graph if any + if (series.drawGraph) { + series.drawGraph(); + series.applyZones(); + } - // draw the points - if (series.visible) { - series.drawPoints(); - } + /* each(series.points, function (point) { + if (point.redraw) { + point.redraw(); + } + });*/ + // draw the data labels (inn pies they go before the points) + if (series.drawDataLabels) { + series.drawDataLabels(); + } - // draw the mouse tracking area - if (series.drawTracker && series.options.enableMouseTracking !== false) { - series.drawTracker(); - } + // draw the points + if (series.visible) { + series.drawPoints(); + } - // Handle inverted series and tracker groups - series.invertGroups(inverted); - // Initial clipping, must be defined after inverting groups for VML. Applies to columns etc. (#3839). - if (options.clip !== false && !series.sharedClipKey && !hasRendered) { - group.clip(chart.clipRect); - } + // draw the mouse tracking area + if (series.drawTracker && series.options.enableMouseTracking !== false) { + series.drawTracker(); + } - // Run the animation - if (animDuration) { - series.animate(); - } + // Handle inverted series and tracker groups + series.invertGroups(inverted); - // Call the afterAnimate function on animation complete (but don't overwrite the animation.complete option - // which should be available to the user). - if (!hasRendered) { - series.animationTimeout = syncTimeout(function() { - series.afterAnimate(); - }, animDuration); - } + // Initial clipping, must be defined after inverting groups for VML. Applies to columns etc. (#3839). + if (options.clip !== false && !series.sharedClipKey && !hasRendered) { + group.clip(chart.clipRect); + } - series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see - // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see - series.hasRendered = true; - }, + // Run the animation + if (animDuration) { + series.animate(); + } - /** - * Redraw the series after an update in the axes. - */ - redraw: function() { - var series = this, - chart = series.chart, - wasDirty = series.isDirty || series.isDirtyData, // cache it here as it is set to false in render, but used after - group = series.group, - xAxis = series.xAxis, - yAxis = series.yAxis; + // Call the afterAnimate function on animation complete (but don't overwrite the animation.complete option + // which should be available to the user). + if (!hasRendered) { + series.animationTimeout = syncTimeout(function() { + series.afterAnimate(); + }, animDuration); + } - // reposition on resize - if (group) { - if (chart.inverted) { - group.attr({ - width: chart.plotWidth, - height: chart.plotHeight - }); - } + series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see + // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see + series.hasRendered = true; + }, - group.animate({ - translateX: pick(xAxis && xAxis.left, chart.plotLeft), - translateY: pick(yAxis && yAxis.top, chart.plotTop) + /** + * Redraw the series after an update in the axes. + */ + redraw: function() { + var series = this, + chart = series.chart, + wasDirty = series.isDirty || series.isDirtyData, // cache it here as it is set to false in render, but used after + group = series.group, + xAxis = series.xAxis, + yAxis = series.yAxis; + + // reposition on resize + if (group) { + if (chart.inverted) { + group.attr({ + width: chart.plotWidth, + height: chart.plotHeight }); } - series.translate(); - series.render(); - if (wasDirty) { // #3868, #3945 - delete this.kdTree; - } - }, + group.animate({ + translateX: pick(xAxis && xAxis.left, chart.plotLeft), + translateY: pick(yAxis && yAxis.top, chart.plotTop) + }); + } - /** - * KD Tree && PointSearching Implementation - */ + series.translate(); + series.render(); + if (wasDirty) { // #3868, #3945 + delete this.kdTree; + } + }, - kdDimensions: 1, - kdAxisArray: ['clientX', 'plotY'], + /** + * KD Tree && PointSearching Implementation + */ - searchPoint: function(e, compareX) { - var series = this, - xAxis = series.xAxis, - yAxis = series.yAxis, - inverted = series.chart.inverted; + kdDimensions: 1, + kdAxisArray: ['clientX', 'plotY'], - return this.searchKDTree({ - clientX: inverted ? xAxis.len - e.chartY + xAxis.pos : e.chartX - xAxis.pos, - plotY: inverted ? yAxis.len - e.chartX + yAxis.pos : e.chartY - yAxis.pos - }, compareX); - }, + searchPoint: function(e, compareX) { + var series = this, + xAxis = series.xAxis, + yAxis = series.yAxis, + inverted = series.chart.inverted; - buildKDTree: function() { - var series = this, - dimensions = series.kdDimensions; + return this.searchKDTree({ + clientX: inverted ? xAxis.len - e.chartY + xAxis.pos : e.chartX - xAxis.pos, + plotY: inverted ? yAxis.len - e.chartX + yAxis.pos : e.chartY - yAxis.pos + }, compareX); + }, - // Internal function - function _kdtree(points, depth, dimensions) { - var axis, - median, - length = points && points.length; + buildKDTree: function() { + var series = this, + dimensions = series.kdDimensions; - if (length) { + // Internal function + function _kdtree(points, depth, dimensions) { + var axis, + median, + length = points && points.length; - // alternate between the axis - axis = series.kdAxisArray[depth % dimensions]; + if (length) { - // sort point array - points.sort(function(a, b) { - return a[axis] - b[axis]; - }); + // alternate between the axis + axis = series.kdAxisArray[depth % dimensions]; - median = Math.floor(length / 2); + // sort point array + points.sort(function(a, b) { + return a[axis] - b[axis]; + }); - // build and return nod - return { - point: points[median], - left: _kdtree(points.slice(0, median), depth + 1, dimensions), - right: _kdtree(points.slice(median + 1), depth + 1, dimensions) - }; + median = Math.floor(length / 2); - } - } + // build and return nod + return { + point: points[median], + left: _kdtree(points.slice(0, median), depth + 1, dimensions), + right: _kdtree(points.slice(median + 1), depth + 1, dimensions) + }; - // Start the recursive build process with a clone of the points array and null points filtered out (#3873) - function startRecursive() { - series.kdTree = _kdtree( - series.getValidPoints( - null, !series.directTouch // For line-type series restrict to plot area, but column-type series not (#3916, #4511) - ), - dimensions, - dimensions - ); } - delete series.kdTree; + } - // For testing tooltips, don't build async - syncTimeout(startRecursive, series.options.kdNow ? 0 : 1); - }, + // Start the recursive build process with a clone of the points array and null points filtered out (#3873) + function startRecursive() { + series.kdTree = _kdtree( + series.getValidPoints( + null, !series.directTouch // For line-type series restrict to plot area, but column-type series not (#3916, #4511) + ), + dimensions, + dimensions + ); + } + delete series.kdTree; - searchKDTree: function(point, compareX) { - var series = this, - kdX = this.kdAxisArray[0], - kdY = this.kdAxisArray[1], - kdComparer = compareX ? 'distX' : 'dist'; + // For testing tooltips, don't build async + syncTimeout(startRecursive, series.options.kdNow ? 0 : 1); + }, - // Set the one and two dimensional distance on the point object - function setDistance(p1, p2) { - var x = (defined(p1[kdX]) && defined(p2[kdX])) ? Math.pow(p1[kdX] - p2[kdX], 2) : null, - y = (defined(p1[kdY]) && defined(p2[kdY])) ? Math.pow(p1[kdY] - p2[kdY], 2) : null, - r = (x || 0) + (y || 0); + searchKDTree: function(point, compareX) { + var series = this, + kdX = this.kdAxisArray[0], + kdY = this.kdAxisArray[1], + kdComparer = compareX ? 'distX' : 'dist'; - p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE; - p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE; - } + // Set the one and two dimensional distance on the point object + function setDistance(p1, p2) { + var x = (defined(p1[kdX]) && defined(p2[kdX])) ? Math.pow(p1[kdX] - p2[kdX], 2) : null, + y = (defined(p1[kdY]) && defined(p2[kdY])) ? Math.pow(p1[kdY] - p2[kdY], 2) : null, + r = (x || 0) + (y || 0); - function _search(search, tree, depth, dimensions) { - var point = tree.point, - axis = series.kdAxisArray[depth % dimensions], - tdist, - sideA, - sideB, - ret = point, - nPoint1, - nPoint2; + p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE; + p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE; + } - setDistance(search, point); + function _search(search, tree, depth, dimensions) { + var point = tree.point, + axis = series.kdAxisArray[depth % dimensions], + tdist, + sideA, + sideB, + ret = point, + nPoint1, + nPoint2; - // Pick side based on distance to splitting point - tdist = search[axis] - point[axis]; - sideA = tdist < 0 ? 'left' : 'right'; - sideB = tdist < 0 ? 'right' : 'left'; + setDistance(search, point); - // End of tree - if (tree[sideA]) { - nPoint1 = _search(search, tree[sideA], depth + 1, dimensions); + // Pick side based on distance to splitting point + tdist = search[axis] - point[axis]; + sideA = tdist < 0 ? 'left' : 'right'; + sideB = tdist < 0 ? 'right' : 'left'; - ret = (nPoint1[kdComparer] < ret[kdComparer] ? nPoint1 : point); - } - if (tree[sideB]) { - // compare distance to current best to splitting point to decide wether to check side B or not - if (Math.sqrt(tdist * tdist) < ret[kdComparer]) { - nPoint2 = _search(search, tree[sideB], depth + 1, dimensions); - ret = (nPoint2[kdComparer] < ret[kdComparer] ? nPoint2 : ret); - } - } + // End of tree + if (tree[sideA]) { + nPoint1 = _search(search, tree[sideA], depth + 1, dimensions); - return ret; + ret = (nPoint1[kdComparer] < ret[kdComparer] ? nPoint1 : point); } - - if (!this.kdTree) { - this.buildKDTree(); + if (tree[sideB]) { + // compare distance to current best to splitting point to decide wether to check side B or not + if (Math.sqrt(tdist * tdist) < ret[kdComparer]) { + nPoint2 = _search(search, tree[sideB], depth + 1, dimensions); + ret = (nPoint2[kdComparer] < ret[kdComparer] ? nPoint2 : ret); + } } - if (this.kdTree) { - return _search(point, - this.kdTree, this.kdDimensions, this.kdDimensions); - } + return ret; } - }); // end Series prototype + if (!this.kdTree) { + this.buildKDTree(); + } + if (this.kdTree) { + return _search(point, + this.kdTree, this.kdDimensions, this.kdDimensions); + } + } + + }); // end Series prototype + }(Highcharts)); (function(H) { /** * (c) 2010-2016 Torstein Honsi * @@ -16737,12 +17994,16 @@ destroyObjectProperties = H.destroyObjectProperties, each = H.each, format = H.format, pick = H.pick, Series = H.Series; + /** - * The class for stack items + * The class for stacks. Each stack, on a specific X value and either negative + * or positive, has its own stack item. + * + * @class */ function StackItem(axis, options, isNegative, x, stackOption) { var inverted = axis.chart.inverted; @@ -16758,29 +18019,34 @@ this.x = x; // Initialize total value this.total = null; - // This will keep each points' extremes stored by series.index and point index + // This will keep each points' extremes stored by series.index and point + // index this.points = {}; - // Save the stack option on the series configuration object, and whether to treat it as percent + // Save the stack option on the series configuration object, and whether to + // treat it as percent this.stack = stackOption; this.leftCliff = 0; this.rightCliff = 0; - // The align options and text align varies on whether the stack is negative and - // if the chart is inverted or not. + // The align options and text align varies on whether the stack is negative + // and if the chart is inverted or not. // First test the user supplied value, then use the dynamic. this.alignOptions = { - align: options.align || (inverted ? (isNegative ? 'left' : 'right') : 'center'), - verticalAlign: options.verticalAlign || (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')), + align: options.align || + (inverted ? (isNegative ? 'left' : 'right') : 'center'), + verticalAlign: options.verticalAlign || + (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')), y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)), x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0) }; - this.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center'); + this.textAlign = options.textAlign || + (inverted ? (isNegative ? 'right' : 'left') : 'center'); } StackItem.prototype = { destroy: function() { destroyObjectProperties(this, this.axis); @@ -16794,60 +18060,77 @@ formatOption = options.format, str = formatOption ? format(formatOption, this) : options.formatter.call(this); // format the text in the label - // Change the text to reflect the new total and set visibility to hidden in case the serie is hidden + // Change the text to reflect the new total and set visibility to hidden + // in case the serie is hidden if (this.label) { this.label.attr({ text: str, visibility: 'hidden' }); // Create new label } else { this.label = - this.axis.chart.renderer.text(str, null, null, options.useHTML) // dummy positions, actual position updated with setOffset method in columnseries - .css(options.style) // apply style + this.axis.chart.renderer.text(str, null, null, options.useHTML) + .css(options.style) .attr({ - align: this.textAlign, // fix the text-anchor - rotation: options.rotation, // rotation + align: this.textAlign, + rotation: options.rotation, visibility: 'hidden' // hidden until setOffset is called }) .add(group); // add to the labels-group } }, /** - * Sets the offset that the stack has from the x value and repositions the label. + * Sets the offset that the stack has from the x value and repositions the + * label. */ setOffset: function(xOffset, xWidth) { var stackItem = this, axis = stackItem.axis, chart = axis.chart, inverted = chart.inverted, reversed = axis.reversed, - neg = (this.isNegative && !reversed) || (!this.isNegative && reversed), // #4056 - y = axis.translate(axis.usePercentage ? 100 : this.total, 0, 0, 0, 1), // stack value translated mapped to chart coordinates + neg = (this.isNegative && !reversed) || + (!this.isNegative && reversed), // #4056 + // stack value translated mapped to chart coordinates + y = axis.translate( + axis.usePercentage ? 100 : this.total, + 0, + 0, + 0, + 1 + ), yZero = axis.translate(0), // stack origin h = Math.abs(y - yZero), // stack height x = chart.xAxis[0].translate(this.x) + xOffset, // stack x position plotHeight = chart.plotHeight, stackBox = { // this is the box for the complete stack x: inverted ? (neg ? y : y - h) : x, - y: inverted ? plotHeight - x - xWidth : (neg ? (plotHeight - y - h) : plotHeight - y), + y: inverted ? + plotHeight - x - xWidth : (neg ? (plotHeight - y - h) : + plotHeight - y), width: inverted ? h : xWidth, height: inverted ? xWidth : h }, label = this.label, alignAttr; if (label) { - label.align(this.alignOptions, null, stackBox); // align the label to the box + // Align the label to the box + label.align(this.alignOptions, null, stackBox); // Set visibility (#678) alignAttr = label.alignAttr; - label[this.options.crop === false || chart.isInsidePlot(alignAttr.x, alignAttr.y) ? 'show' : 'hide'](true); + label[ + this.options.crop === false || chart.isInsidePlot( + alignAttr.x, + alignAttr.y + ) ? 'show' : 'hide'](true); } } }; /** @@ -16862,11 +18145,12 @@ axis.oldStacks = axis.stacks; } }); each(chart.series, function(series) { - if (series.options.stacking && (series.visible === true || chart.options.chart.ignoreHiddenSeries === false)) { + if (series.options.stacking && (series.visible === true || + chart.options.chart.ignoreHiddenSeries === false)) { series.stackKey = series.type + pick(series.options.stack, ''); } }); }; @@ -16924,12 +18208,12 @@ zIndex: 6 }) .add(); } - // plotLeft/Top will change when y axis gets wider so we need to translate the - // stackTotalGroup at every render call. See bug #506 and #516 + // plotLeft/Top will change when y axis gets wider so we need to translate + // the stackTotalGroup at every render call. See bug #506 and #516 stackTotalGroup.translate(chart.plotLeft, chart.plotTop); // Render each stack total for (stackKey in stacks) { oneStack = stacks[stackKey]; @@ -16987,11 +18271,12 @@ /** * Adds series' points value to corresponding stack */ Series.prototype.setStackedPoints = function() { - if (!this.options.stacking || (this.visible !== true && this.chart.options.chart.ignoreHiddenSeries !== false)) { + if (!this.options.stacking || (this.visible !== true && + this.chart.options.chart.ignoreHiddenSeries !== false)) { return; } var series = this, xData = series.processedXData, @@ -17024,14 +18309,19 @@ // loop over the non-null y values and read them into a local array for (i = 0; i < yDataLength; i++) { x = xData[i]; y = yData[i]; - stackIndicator = series.getStackIndicator(stackIndicator, x, series.index); + stackIndicator = series.getStackIndicator( + stackIndicator, + x, + series.index + ); pointKey = stackIndicator.key; // Read stacked values into a stack based on the x value, - // the sign of y and the stack key. Stacking is also handled for null values (#739) + // the sign of y and the stack key. Stacking is also handled for null + // values (#739) isNegative = negStacks && y < (stackThreshold ? 0 : threshold); key = isNegative ? negKey : stackKey; // Create empty object for this stack if it doesn't exist yet if (!stacks[key]) { @@ -17042,11 +18332,17 @@ if (!stacks[key][x]) { if (oldStacks[key] && oldStacks[key][x]) { stacks[key][x] = oldStacks[key][x]; stacks[key][x].total = null; } else { - stacks[key][x] = new StackItem(yAxis, yAxis.options.stackLabels, isNegative, x, stackOption); + stacks[key][x] = new StackItem( + yAxis, + yAxis.options.stackLabels, + isNegative, + x, + stackOption + ); } } // If the StackItem doesn't exist, create it first stack = stacks[key][x]; @@ -17058,25 +18354,28 @@ stack.base = pointKey; } stack.touched = yAxis.stacksTouched; - // In area charts, if there are multiple points on the same X value, let the - // area fill the full span of those points + // In area charts, if there are multiple points on the same X value, + // let the area fill the full span of those points if (stackIndicator.index > 0 && series.singleStacks === false) { - stack.points[pointKey][0] = stack.points[series.index + ',' + x + ',0'][0]; + stack.points[pointKey][0] = + stack.points[series.index + ',' + x + ',0'][0]; } } // Add value to the stack total if (stacking === 'percent') { - // Percent stacked column, totals are the same for the positive and negative stacks + // Percent stacked column, totals are the same for the positive and + // negative stacks other = isNegative ? stackKey : negKey; if (negStacks && stacks[other] && stacks[other][x]) { other = stacks[other][x]; - stack.total = other.total = Math.max(other.total, stack.total) + Math.abs(y) || 0; + stack.total = other.total = + Math.max(other.total, stack.total) + Math.abs(y) || 0; // Percent stacked areas } else { stack.total = correctFloat(stack.total + (Math.abs(y) || 0)); } @@ -17120,31 +18419,44 @@ pointExtremes, totalFactor; while (i--) { x = processedXData[i]; - stackIndicator = series.getStackIndicator(stackIndicator, x, series.index); + stackIndicator = series.getStackIndicator( + stackIndicator, + x, + series.index, + key + ); stack = stacks[key] && stacks[key][x]; pointExtremes = stack && stack.points[stackIndicator.key]; if (pointExtremes) { totalFactor = stack.total ? 100 / stack.total : 0; - pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor); // Y bottom value - pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor); // Y value + // Y bottom value + pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor); + // Y value + pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor); series.stackedYData[i] = pointExtremes[1]; } } }); }; /** - * Get stack indicator, according to it's x-value, to determine points with the same x-value + * Get stack indicator, according to it's x-value, to determine points with the + * same x-value */ - Series.prototype.getStackIndicator = function(stackIndicator, x, index) { - if (!defined(stackIndicator) || stackIndicator.x !== x) { + Series.prototype.getStackIndicator = function(stackIndicator, x, index, key) { + // Update stack indicator, when: + // first point in a stack || x changed || stack type (negative vs positive) + // changed: + if (!defined(stackIndicator) || stackIndicator.x !== x || + (key && stackIndicator.key !== key)) { stackIndicator = { x: x, - index: 0 + index: 0, + key: key }; } else { stackIndicator.index++; } @@ -17171,21 +18483,22 @@ each = H.each, erase = H.erase, extend = H.extend, fireEvent = H.fireEvent, inArray = H.inArray, + isNumber = H.isNumber, isObject = H.isObject, merge = H.merge, pick = H.pick, Point = H.Point, Series = H.Series, seriesTypes = H.seriesTypes, setAnimation = H.setAnimation, splat = H.splat; // Extend the Chart prototype for dynamic methods - extend(Chart.prototype, { + extend(Chart.prototype, /** @lends Highcharts.Chart.prototype */ { /** * Add a series dynamically after time * * @param {Object} options The config options @@ -17275,14 +18588,13 @@ null, loadingDiv ); addEvent(chart, 'redraw', setLoadingSize); // #1080 } - setTimeout(function() { - loadingDiv.className = 'highcharts-loading'; - }); + loadingDiv.className = 'highcharts-loading'; + // Update text chart.loadingSpan.innerHTML = str || options.lang.loading; // Update visuals @@ -17342,13 +18654,16 @@ 'borderRadius', 'plotBackgroundColor', 'plotBackgroundImage', 'plotBorderColor', 'plotBorderWidth', 'plotShadow', 'shadow' ], /** - * These properties cause all series to be updated when updating. Can be extended from plugins. + * These properties cause all series to be updated when updating. Can be + * extended from plugins. */ - propsRequireUpdateSeries: ['chart.polar', 'chart.ignoreHiddenSeries', 'chart.type', 'colors', 'plotOptions'], + propsRequireUpdateSeries: ['chart.inverted', 'chart.polar', + 'chart.ignoreHiddenSeries', 'chart.type', 'colors', 'plotOptions' + ], /** * Chart.update function that takes the whole options stucture. */ update: function(options, redraw) { @@ -17358,11 +18673,13 @@ title: 'setTitle', subtitle: 'setSubtitle' }, optionsChart = options.chart, updateAllAxes, - updateAllSeries; + updateAllSeries, + newWidth, + newHeight; // If the top-level chart option is present, some special updates are required if (optionsChart) { merge(true, this.options.chart, optionsChart); @@ -17461,12 +18778,15 @@ if (options.loading) { merge(true, this.options.loading, options.loading); } // Update size. Redraw is forced. - if (optionsChart && ('width' in optionsChart || 'height' in optionsChart)) { - this.setSize(optionsChart.width, optionsChart.height); + newWidth = optionsChart && optionsChart.width; + newHeight = optionsChart && optionsChart.height; + if ((isNumber(newWidth) && newWidth !== this.chartWidth) || + (isNumber(newHeight) && newHeight !== this.chartHeight)) { + this.setSize(newWidth, newHeight); } else if (pick(redraw, true)) { this.redraw(); } }, @@ -17479,11 +18799,11 @@ }); // extend the Point prototype for dynamic methods - extend(Point.prototype, { + extend(Point.prototype, /** @lends Point.prototype */ { /** * Point.update with new options (typically x/y data) and optionally redraw the series. * * @param {Object} options Point options as defined in the series.data array * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call @@ -17562,18 +18882,18 @@ this.series.removePoint(inArray(this, this.series.data), redraw, animation); } }); // Extend the series prototype for dynamic methods - extend(Series.prototype, { + extend(Series.prototype, /** @lends Series.prototype */ { /** * Add a point dynamically after chart load time * @param {Object} options Point options as given in series.data * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call * @param {Boolean} shift If shift is true, a point is shifted off the start * of the series as one is appended to the end. - * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * @param {Boolean|AnimationOptions} animation Whether to apply animation, and optionally animation * configuration */ addPoint: function(options, redraw, shift, animation) { var series = this, seriesOptions = series.options, @@ -17779,11 +19099,11 @@ } } }); // Extend the Axis.prototype for dynamic methods - extend(Axis.prototype, { + extend(Axis.prototype, /** @lends Axis.prototype */ { /** * Axis.update with a new options structure */ update: function(newOptions, redraw) { @@ -17869,21 +19189,24 @@ LegendSymbolMixin = H.LegendSymbolMixin, map = H.map, pick = H.pick, Series = H.Series, seriesType = H.seriesType; + /** - * Area series type + * Area series type. + * @constructor seriesTypes.area + * @extends {Series} */ seriesType('area', 'line', { softThreshold: false, threshold: 0 // trackByArea: false, // lineColor: null, // overrides color, but lets fillColor be unaltered // fillOpacity: 0.75, // fillColor: null - }, { + }, /** @lends seriesTypes.area.prototype */ { singleStacks: false, /** * Return an array of stacked points, where null and missing points are replaced by * dummy points in order for gaps to be drawn correctly in stacks. */ @@ -18189,29 +19512,19 @@ * (c) 2010-2016 Torstein Honsi * * License: www.highcharts.com/license */ 'use strict'; - var defaultPlotOptions = H.defaultPlotOptions, - defaultSeriesOptions = H.defaultPlotOptions.line, - extendClass = H.extendClass, - merge = H.merge, - pick = H.pick, - Series = H.Series, - seriesTypes = H.seriesTypes; + var pick = H.pick, + seriesType = H.seriesType; /** - * Set the default options for spline + * Spline series type. + * @constructor seriesTypes.spline + * @extends {Series} */ - defaultPlotOptions.spline = merge(defaultSeriesOptions); - - /** - * SplineSeries object - */ - seriesTypes.spline = extendClass(Series, { - type: 'spline', - + seriesType('spline', 'line', {}, /** @lends seriesTypes.spline.prototype */ { /** * Get the spline segment from a given point's previous neighbour to the given point */ getPointSpline: function(points, point, i) { var smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc @@ -18372,11 +19685,14 @@ Series = H.Series, seriesType = H.seriesType, stop = H.stop, svg = H.svg; /** - * The column series type + * The column series type. + * + * @constructor seriesTypes.column + * @augments Series */ seriesType('column', 'line', { borderRadius: 0, //colorByPoint: undefined, groupPadding: 0.2, @@ -18418,20 +19734,24 @@ borderColor: '#ffffff' // borderWidth: 1 - // Prototype members - }, { + }, /** @lends seriesTypes.column.prototype */ { cropShoulder: 0, directTouch: true, // When tooltip is not shared, this series (and derivatives) requires direct touch/hover. KD-tree does not apply. trackerGroups: ['group', 'dataLabelsGroup'], negStacks: true, // use separate negative stacks, unlike area stacks where a negative // point is substracted from previous (#1910) /** - * Initialize the series + * Initialize the series. Extends the basic Series.init method by + * marking other series of the same type as dirty. + * + * @function #init + * @memberOf seriesTypes.column + * @returns {void} */ init: function() { Series.prototype.init.apply(this, arguments); var series = this, @@ -18656,11 +19976,12 @@ ret, p2o = this.pointAttrToOptions || {}, strokeOption = p2o.stroke || 'borderColor', strokeWidthOption = p2o['stroke-width'] || 'borderWidth', fill = (point && point.color) || this.color, - stroke = options[strokeOption] || this.color || fill, // set to fill when borderColor = null on pies + stroke = point[strokeOption] || options[strokeOption] || + this.color || fill, // set to fill when borderColor null dashstyle = options.dashStyle, zone, brightness; // Handle zone colors @@ -18813,11 +20134,13 @@ * (c) 2010-2016 Torstein Honsi * * License: www.highcharts.com/license */ 'use strict'; + var seriesType = H.seriesType; + /** * The Bar series class */ seriesType('bar', 'column', null, { inverted: true @@ -18931,12 +20254,16 @@ Point = H.Point, Series = H.Series, seriesType = H.seriesType, seriesTypes = H.seriesTypes, setAnimation = H.setAnimation; + /** - * Pie series type + * The pie series type. + * + * @constructor seriesTypes.pie + * @augments Series */ seriesType('pie', 'line', { center: [null, null], clip: false, colorByPoint: true, // always true for pies @@ -18974,12 +20301,11 @@ shadow: false } } - // Prototype members - }, { + }, /** @lends seriesTypes.pie.prototype */ { isCartesian: false, requireSorting: false, directTouch: true, noSharedTooltip: true, trackerGroups: ['group', 'dataLabelsGroup'], @@ -19278,13 +20604,17 @@ /** * Pies don't have point marker symbols */ getSymbol: noop - // Point class overrides - }, { + /** + * @constructor seriesTypes.pie.prototype.pointClass + * @extends {Point} + */ + }, /** @lends seriesTypes.pie.prototype.pointClass.prototype */ { + /** * Initiate the pie slice */ init: function() { Point.prototype.init.apply(this, arguments); @@ -19384,18 +20714,23 @@ } }, haloPath: function(size) { - var shapeArgs = this.shapeArgs, - chart = this.series.chart; + var shapeArgs = this.shapeArgs; - 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, { - innerR: this.shapeArgs.r, - start: shapeArgs.start, - end: shapeArgs.end - }); + return this.sliced || !this.visible ? [] : + this.series.chart.renderer.symbols.arc( + shapeArgs.x, + shapeArgs.y, + shapeArgs.r + size, + shapeArgs.r + size, { + innerR: this.shapeArgs.r, + start: shapeArgs.start, + end: shapeArgs.end + } + ); } }); }(Highcharts)); (function(H) { @@ -20088,11 +21423,11 @@ * and the pie slice. */ seriesTypes.pie.prototype.connectorPath = function(labelPos) { var x = labelPos.x, y = labelPos.y; - return pick(this.options.softConnector, true) ? [ + return pick(this.options.dataLabels.softConnector, true) ? [ 'M', x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label 'C', x, y, // first break, next to the label 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5], @@ -20431,15 +21766,21 @@ Point = H.Point, Series = H.Series, seriesTypes = H.seriesTypes, svg = H.svg, TrackerMixin; + /** - * TrackerMixin for points and graphs + * TrackerMixin for points and graphs. + * + * @mixin */ TrackerMixin = H.TrackerMixin = { + /** + * Draw the tracker for a point. + */ drawTrackerPoint: function() { var series = this, chart = series.chart, pointer = chart.pointer, onMouseOver = function(e) { @@ -20702,11 +22043,11 @@ /* * Extend the Chart object with interaction */ - extend(Chart.prototype, { + extend(Chart.prototype, /** @lends Chart.prototype */ { /** * Display the zoom button */ showResetZoom: function() { var chart = this, @@ -20763,11 +22104,11 @@ each(event.xAxis.concat(event.yAxis), function(axisData) { var axis = axisData.axis, isXAxis = axis.isXAxis; // don't zoom more than minRange - if (pointer[isXAxis ? 'zoomX' : 'zoomY'] || pointer[isXAxis ? 'pinchX' : 'pinchY']) { + if (pointer[isXAxis ? 'zoomX' : 'zoomY']) { hasZoomed = axis.zoom(axisData.min, axisData.max); if (axis.displayBtn) { displayButton = true; } } @@ -20843,11 +22184,11 @@ }); /* * Extend the Point object with interaction */ - extend(Point.prototype, { + extend(Point.prototype, /** @lends Point.prototype */ { /** * Toggle the selection status of a point * @param {Boolean} selected Whether to select or unselect the point. * @param {Boolean} accumulate Whether to add to the previous selection. By default, * this happens if the control key (Cmd on Mac) was pressed during clicking. @@ -20894,34 +22235,34 @@ series = point.series, chart = series.chart, tooltip = chart.tooltip, hoverPoint = chart.hoverPoint; - if (chart.hoverSeries !== series) { - series.onMouseOver(); - } - - // set normal state to previous series - if (hoverPoint && hoverPoint !== point) { - hoverPoint.onMouseOut(); - } - if (point.series) { // It may have been destroyed, #4130 + // In shared tooltip, call mouse over when point/series is actually hovered: (#5766) + if (!byProximity) { + // set normal state to previous series + if (hoverPoint && hoverPoint !== point) { + hoverPoint.onMouseOut(); + } + if (chart.hoverSeries !== series) { + series.onMouseOver(); + } + chart.hoverPoint = point; + } - // trigger the event - point.firePointEvent('mouseOver'); - // update the tooltip if (tooltip && (!tooltip.shared || series.noSharedTooltip)) { + // hover point only for non shared points: (#5766) + point.setState('hover'); tooltip.refresh(point, e); + } else if (!tooltip) { + point.setState('hover'); } - // hover this - point.setState('hover'); - if (!byProximity) { - chart.hoverPoint = point; - } + // trigger the event + point.firePointEvent('mouseOver'); } }, /** * Runs on mouse out from the point @@ -20967,21 +22308,23 @@ var point = this, plotX = Math.floor(point.plotX), // #4586 plotY = point.plotY, series = point.series, stateOptions = series.options.states[state] || {}, - markerOptions = (defaultPlotOptions[series.type].marker && series.options.marker) || {}, - normalDisabled = markerOptions.enabled === false, - markerStateOptions = (markerOptions.states && markerOptions.states[state]) || {}, + markerOptions = defaultPlotOptions[series.type].marker && + series.options.marker, + normalDisabled = markerOptions && markerOptions.enabled === false, + markerStateOptions = (markerOptions && markerOptions.states && + markerOptions.states[state]) || {}, stateDisabled = markerStateOptions.enabled === false, stateMarkerGraphic = series.stateMarkerGraphic, pointMarker = point.marker || {}, chart = series.chart, - radius, halo = series.halo, haloOptions, - attribs, + markerAttribs, + hasMarkers = markerOptions && series.markerAttribs, newSymbol; state = state || ''; // empty string if ( @@ -20998,11 +22341,13 @@ ) { return; } - radius = markerStateOptions.radius || (markerOptions.radius + (markerStateOptions.radiusPlus || 0)); + if (hasMarkers) { + markerAttribs = series.markerAttribs(point, state); + } // Apply hover styles to the existing point if (point.graphic) { if (point.state) { @@ -21010,22 +22355,32 @@ } if (state) { point.graphic.addClass('highcharts-point-' + state); } - attribs = radius ? { // new symbol attributes (#507, #612) - x: plotX - radius, - y: plotY - radius, - width: 2 * radius, - height: 2 * radius - } : {}; + /*attribs = radius ? { // new symbol attributes (#507, #612) + x: plotX - radius, + y: plotY - radius, + width: 2 * radius, + height: 2 * radius + } : {};*/ - attribs = merge(series.pointAttribs(point, state), attribs); + //attribs = merge(series.pointAttribs(point, state), attribs); + point.graphic.attr(series.pointAttribs(point, state)); - point.graphic.attr(attribs); + if (markerAttribs) { + point.graphic.animate( + markerAttribs, + pick( + chart.options.chart.animation, // Turn off globally + markerStateOptions.animation, + markerOptions.animation + ) + ); + } // Zooming in from a range with no markers to a range with markers if (stateMarkerGraphic) { stateMarkerGraphic.hide(); } @@ -21044,24 +22399,24 @@ // Add a new state marker graphic if (!stateMarkerGraphic) { if (newSymbol) { series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol( newSymbol, - plotX - radius, - plotY - radius, - 2 * radius, - 2 * radius + markerAttribs.x, + markerAttribs.y, + markerAttribs.width, + markerAttribs.height ) .add(series.markerGroup); stateMarkerGraphic.currentSymbol = newSymbol; } // Move the existing graphic } else { stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054 - x: plotX - radius, - y: plotY - radius + x: markerAttribs.x, + y: markerAttribs.y }); } if (stateMarkerGraphic) { stateMarkerGraphic.attr(series.pointAttribs(point, state)); @@ -21078,64 +22433,60 @@ // Show me your halo haloOptions = stateOptions.halo; if (haloOptions && haloOptions.size) { if (!halo) { series.halo = halo = chart.renderer.path() - .add(chart.seriesGroup); + // #5818, #5903 + .add(hasMarkers ? series.markerGroup : series.group); } + H.stop(halo); halo[move ? 'animate' : 'attr']({ d: point.haloPath(haloOptions.size) }); halo.attr({ 'class': 'highcharts-halo highcharts-color-' + pick(point.colorIndex, series.colorIndex) }); halo.attr(extend({ - 'fill': point.color || series.color, - 'fill-opacity': haloOptions.opacity, - 'zIndex': -1 // #4929, IE8 added halo above everything - }, - haloOptions.attributes))[move ? 'animate' : 'attr']({ - d: point.haloPath(haloOptions.size) - }); + 'fill': point.color || series.color, + 'fill-opacity': haloOptions.opacity, + 'zIndex': -1 // #4929, IE8 added halo above everything + }, haloOptions.attributes)); } else if (halo) { - halo.attr({ - d: [] - }); + halo.animate({ + d: point.haloPath(0) + }); // Hide } point.state = state; }, /** * Get the circular path definition for the halo - * @param {Number} size The radius of the circular halo + * @param {Number} size The radius of the circular halo. * @returns {Array} The path definition */ haloPath: function(size) { var series = this.series, - chart = series.chart, - plotBox = series.getPlotBox(), - inverted = chart.inverted, - plotX = Math.floor(this.plotX); + chart = series.chart; return chart.renderer.symbols.circle( - plotBox.translateX + (inverted ? series.yAxis.len - this.plotY : plotX) - size, - plotBox.translateY + (inverted ? series.xAxis.len - plotX : this.plotY) - size, + Math.floor(this.plotX) - size, + this.plotY - size, size * 2, size * 2 ); } }); /* * Extend the Series object with interaction */ - extend(Series.prototype, { + extend(Series.prototype, /** @lends Series.prototype */ { /** * Series mouse over handler */ onMouseOver: function() { var series = this, @@ -21265,11 +22616,11 @@ // if called without an argument, toggle visibility series.visible = vis = series.options.visible = series.userOptions.visible = vis === undefined ? !oldVisibility : vis; // #5618 showOrHide = vis ? 'show' : 'hide'; // show or hide elements - each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker'], function(key) { + each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker', 'tt'], function(key) { if (series[key]) { series[key][showOrHide](); } }); @@ -21380,19 +22731,19 @@ */ Chart.prototype.matchResponsiveRule = function(rule, redraw) { var respRules = this.respRules, condition = rule.condition, matches, - fn = rule.callback || function() { + fn = condition.callback || function() { return this.chartWidth <= pick(condition.maxWidth, Number.MAX_VALUE) && this.chartHeight <= pick(condition.maxHeight, Number.MAX_VALUE) && this.chartWidth >= pick(condition.minWidth, 0) && this.chartHeight >= pick(condition.minHeight, 0); }; if (rule._id === undefined) { - rule._id = H.idCounter++; + rule._id = H.uniqueKey(); } matches = fn.call(this); // Apply a rule if (!respRules[rule._id] && matches) {