app/assets/javascripts/highcharts.js in highcharts-rails-4.1.9 vs app/assets/javascripts/highcharts.js in highcharts-rails-4.1.10

- old
+ new

@@ -1,18906 +1,19072 @@ // ==ClosureCompiler== // @compilation_level SIMPLE_OPTIMIZATIONS /** - * @license Highcharts JS v4.1.9 (2015-10-07) + * @license Highcharts JS v4.1.10 (2015-12-07) * * (c) 2009-2014 Torstein Honsi * * License: www.highcharts.com/license */ -// JSLint options: -/*global Highcharts, HighchartsAdapter, document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $, console, each, grep */ -/*jslint ass: true, sloppy: true, forin: true, plusplus: true, nomen: true, vars: true, regexp: true, newcap: true, browser: true, continue: true, white: true */ -(function () { +(function (root, factory) { + if (typeof module === 'object' && module.exports) { + module.exports = root.document ? + factory(root) : + function (w) { + return factory(w); + }; + } else { + root.Highcharts = factory(); + } +}(typeof window !== 'undefined' ? window : this, function (w) { // encapsulated variables -var UNDEFINED, - doc = document, - win = window, - math = Math, - mathRound = math.round, - mathFloor = math.floor, - mathCeil = math.ceil, - mathMax = math.max, - mathMin = math.min, - mathAbs = math.abs, - mathCos = math.cos, - mathSin = math.sin, - mathPI = math.PI, - deg2rad = mathPI * 2 / 360, + var UNDEFINED, + win = w || window, + doc = win.document, + math = Math, + mathRound = math.round, + mathFloor = math.floor, + mathCeil = math.ceil, + mathMax = math.max, + mathMin = math.min, + mathAbs = math.abs, + mathCos = math.cos, + mathSin = math.sin, + mathPI = math.PI, + deg2rad = mathPI * 2 / 360, - // some variables - userAgent = navigator.userAgent, - isOpera = win.opera, - isMS = /(msie|trident|edge)/i.test(userAgent) && !isOpera, - docMode8 = doc.documentMode === 8, - isWebKit = !isMS && /AppleWebKit/.test(userAgent), - isFirefox = /Firefox/.test(userAgent), - isTouchDevice = /(Mobile|Android|Windows Phone)/.test(userAgent), - SVG_NS = 'http://www.w3.org/2000/svg', - hasSVG = !!doc.createElementNS && !!doc.createElementNS(SVG_NS, 'svg').createSVGRect, - hasBidiBug = isFirefox && parseInt(userAgent.split('Firefox/')[1], 10) < 4, // issue #38 - useCanVG = !hasSVG && !isMS && !!doc.createElement('canvas').getContext, - Renderer, - hasTouch, - symbolSizes = {}, - idCounter = 0, - garbageBin, - defaultOptions, - dateFormat, // function - pathAnim, - timeUnits, - noop = function () { return UNDEFINED; }, - charts = [], - chartCount = 0, - PRODUCT = 'Highcharts', - VERSION = '4.1.9', + // some variables + userAgent = navigator.userAgent, + isOpera = win.opera, + isMS = /(msie|trident|edge)/i.test(userAgent) && !isOpera, + docMode8 = doc.documentMode === 8, + isWebKit = !isMS && /AppleWebKit/.test(userAgent), + isFirefox = /Firefox/.test(userAgent), + isTouchDevice = /(Mobile|Android|Windows Phone)/.test(userAgent), + SVG_NS = 'http://www.w3.org/2000/svg', + hasSVG = !!doc.createElementNS && !!doc.createElementNS(SVG_NS, 'svg').createSVGRect, + hasBidiBug = isFirefox && parseInt(userAgent.split('Firefox/')[1], 10) < 4, // issue #38 + useCanVG = !hasSVG && !isMS && !!doc.createElement('canvas').getContext, + Renderer, + hasTouch, + symbolSizes = {}, + idCounter = 0, + garbageBin, + defaultOptions, + dateFormat, // function + pathAnim, + timeUnits, + noop = function () {}, + charts = [], + chartCount = 0, + PRODUCT = 'Highcharts', + VERSION = '4.1.10', - // some constants for frequently used strings - DIV = 'div', - ABSOLUTE = 'absolute', - RELATIVE = 'relative', - HIDDEN = 'hidden', - PREFIX = 'highcharts-', - VISIBLE = 'visible', - PX = 'px', - NONE = 'none', - M = 'M', - L = 'L', - numRegex = /^[0-9]+$/, - NORMAL_STATE = '', - HOVER_STATE = 'hover', - SELECT_STATE = 'select', - marginNames = ['plotTop', 'marginRight', 'marginBottom', 'plotLeft'], - - // Object for extending Axis - AxisPlotLineOrBandExtension, + // some constants for frequently used strings + DIV = 'div', + ABSOLUTE = 'absolute', + RELATIVE = 'relative', + HIDDEN = 'hidden', + PREFIX = 'highcharts-', + VISIBLE = 'visible', + PX = 'px', + NONE = 'none', + M = 'M', + L = 'L', + numRegex = /^[0-9]+$/, + NORMAL_STATE = '', + HOVER_STATE = 'hover', + SELECT_STATE = 'select', + marginNames = ['plotTop', 'marginRight', 'marginBottom', 'plotLeft'], - // constants for attributes - STROKE_WIDTH = 'stroke-width', + // Object for extending Axis + AxisPlotLineOrBandExtension, - // time methods, changed based on whether or not UTC is used - Date, // Allow using a different Date class - makeTime, - timezoneOffset, - getTimezoneOffset, - getMinutes, - getHours, - getDay, - getDate, - getMonth, - getFullYear, - setMilliseconds, - setSeconds, - setMinutes, - setHours, - setDate, - setMonth, - setFullYear, + // constants for attributes + STROKE_WIDTH = 'stroke-width', + // time methods, changed based on whether or not UTC is used + Date, // Allow using a different Date class + makeTime, + timezoneOffset, + getTimezoneOffset, + getMinutes, + getHours, + getDay, + getDate, + getMonth, + getFullYear, + setMilliseconds, + setSeconds, + setMinutes, + setHours, + setDate, + setMonth, + setFullYear, - // lookup over the types and the associated classes - seriesTypes = {}, - Highcharts; -// The Highcharts namespace -Highcharts = win.Highcharts = win.Highcharts ? error(16, true) : {}; + // lookup over the types and the associated classes + seriesTypes = {}, + Highcharts; -Highcharts.seriesTypes = seriesTypes; + /** + * Provide error messages for debugging, with links to online explanation + */ + function error(code, stop) { + 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 + } + } -/** - * 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 - */ -var extend = Highcharts.extend = function (a, b) { - var n; - if (!a) { - a = {}; - } - for (n in b) { - a[n] = b[n]; - } - 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. - */ -function merge() { - var i, - args = arguments, - len, - ret = {}, - doCopy = function (copy, original) { - var value, key; + // The Highcharts namespace + Highcharts = win.Highcharts ? error(16, true) : function (adapter) { + Highcharts.loadAdapter(adapter); + return Highcharts; + }; - // An object is replacing a primitive - if (typeof copy !== 'object') { - copy = {}; - } + Highcharts.seriesTypes = seriesTypes; - for (key in original) { - if (original.hasOwnProperty(key)) { - value = original[key]; + /** + * 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 + */ + var extend = Highcharts.extend = function (a, b) { + var n; + if (!a) { + a = {}; + } + for (n in b) { + a[n] = b[n]; + } + return a; + }; - // Copy the contents of objects, but not arrays or DOM nodes - if (value && typeof value === 'object' && Object.prototype.toString.call(value) !== '[object Array]' && - key !== 'renderTo' && typeof value.nodeType !== 'number') { - copy[key] = doCopy(copy[key] || {}, value); - - // Primitives and arrays are copied over directly - } else { - copy[key] = original[key]; - } - } - } - return copy; - }; + /** + * 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. + */ + function merge() { + var i, + args = arguments, + len, + ret = {}, + doCopy = function (copy, original) { + var value, key; - // 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); - } + // An object is replacing a primitive + if (typeof copy !== 'object') { + copy = {}; + } - // For each argument, extend the return - len = args.length; - for (i = 0; i < len; i++) { - ret = doCopy(ret, args[i]); - } + for (key in original) { + if (original.hasOwnProperty(key)) { + value = original[key]; - return ret; -} + // Copy the contents of objects, but not arrays or DOM nodes + if (value && typeof value === 'object' && Object.prototype.toString.call(value) !== '[object Array]' && + key !== 'renderTo' && typeof value.nodeType !== 'number') { + copy[key] = doCopy(copy[key] || {}, value); -/** - * Shortcut for parseInt - * @param {Object} s - * @param {Number} mag Magnitude - */ -function pInt(s, mag) { - return parseInt(s, mag || 10); -} + // Primitives and arrays are copied over directly + } else { + copy[key] = original[key]; + } + } + } + return copy; + }; -/** - * Check for string - * @param {Object} s - */ -function isString(s) { - return typeof s === 'string'; -} + // 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); + } -/** - * Check for object - * @param {Object} obj - */ -function isObject(obj) { - return obj && typeof obj === 'object'; -} + // For each argument, extend the return + len = args.length; + for (i = 0; i < len; i++) { + ret = doCopy(ret, args[i]); + } -/** - * Check for array - * @param {Object} obj - */ -function isArray(obj) { - return Object.prototype.toString.call(obj) === '[object Array]'; -} + return ret; + } -/** - * Check for number - * @param {Object} n - */ -function isNumber(n) { - return typeof n === 'number'; -} + /** + * Shortcut for parseInt + * @param {Object} s + * @param {Number} mag Magnitude + */ + function pInt(s, mag) { + return parseInt(s, mag || 10); + } -function log2lin(num) { - return math.log(num) / math.LN10; -} -function lin2log(num) { - return math.pow(10, num); -} + /** + * Check for string + * @param {Object} s + */ + function isString(s) { + return typeof s === 'string'; + } -/** - * Remove last occurence of an item from an array - * @param {Array} arr - * @param {Mixed} item - */ -function erase(arr, item) { - var i = arr.length; - while (i--) { - if (arr[i] === item) { - arr.splice(i, 1); - break; - } - } - //return arr; -} + /** + * Check for object + * @param {Object} obj + */ + function isObject(obj) { + return obj && typeof obj === 'object'; + } -/** - * Returns true if the object is not null or undefined. Like MooTools' $.defined. - * @param {Object} obj - */ -function defined(obj) { - return obj !== UNDEFINED && obj !== null; -} + /** + * Check for array + * @param {Object} obj + */ + function isArray(obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + } -/** - * 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. - * - * @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(elem, prop, value) { - var key, - ret; + /** + * Check for number + * @param {Object} n + */ + function isNumber(n) { + return typeof n === 'number'; + } - // if the prop is a string - if (isString(prop)) { - // set the value - if (defined(value)) { - elem.setAttribute(prop, value); + function log2lin(num) { + return math.log(num) / math.LN10; + } + function lin2log(num) { + return math.pow(10, num); + } - // get the value - } else if (elem && elem.getAttribute) { // elem not defined when printing pie demo... - ret = elem.getAttribute(prop); - } + /** + * Remove last occurence of an item from an array + * @param {Array} arr + * @param {Mixed} item + */ + function erase(arr, item) { + var i = arr.length; + while (i--) { + if (arr[i] === item) { + arr.splice(i, 1); + break; + } + } + //return arr; + } - // else if prop is defined, it is a hash of key/value pairs - } else if (defined(prop) && isObject(prop)) { - for (key in prop) { - elem.setAttribute(key, prop[key]); - } - } - return ret; -} -/** - * Check if an element is an array, and if not, make it into an array. Like - * MooTools' $.splat. - */ -function splat(obj) { - return isArray(obj) ? obj : [obj]; -} + /** + * Returns true if the object is not null or undefined. Like MooTools' $.defined. + * @param {Object} obj + */ + function defined(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. + * + * @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(elem, prop, value) { + var key, + ret; -/** - * Return the first value that is defined. Like MooTools' $.pick. - */ -var pick = Highcharts.pick = function () { - var args = arguments, - i, - arg, - length = args.length; - for (i = 0; i < length; i++) { - arg = args[i]; - if (arg !== UNDEFINED && arg !== null) { - return arg; - } - } -}; + // if the prop is a string + if (isString(prop)) { + // set the value + if (defined(value)) { + elem.setAttribute(prop, value); -/** - * Set CSS on a given element - * @param {Object} el - * @param {Object} styles Style object with camel case property names - */ -function css(el, styles) { - if (isMS && !hasSVG) { // #2686 - if (styles && styles.opacity !== UNDEFINED) { - styles.filter = 'alpha(opacity=' + (styles.opacity * 100) + ')'; - } - } - extend(el.style, styles); -} + // get the value + } else if (elem && elem.getAttribute) { // elem not defined when printing pie demo... + ret = elem.getAttribute(prop); + } -/** - * Utility function to create element with attributes and styles - * @param {Object} tag - * @param {Object} attribs - * @param {Object} styles - * @param {Object} parent - * @param {Object} nopad - */ -function createElement(tag, attribs, styles, parent, nopad) { - var el = doc.createElement(tag); - if (attribs) { - extend(el, attribs); - } - if (nopad) { - css(el, {padding: 0, border: NONE, margin: 0}); - } - if (styles) { - css(el, styles); - } - if (parent) { - parent.appendChild(el); - } - return el; -} + // else if prop is defined, it is a hash of key/value pairs + } else if (defined(prop) && isObject(prop)) { + for (key in prop) { + elem.setAttribute(key, prop[key]); + } + } + return ret; + } + /** + * Check if an element is an array, and if not, make it into an array. Like + * MooTools' $.splat. + */ + function splat(obj) { + return isArray(obj) ? obj : [obj]; + } -/** - * Extend a prototyped class by new members - * @param {Object} parent - * @param {Object} members - */ -function extendClass(parent, members) { - var object = function () { return UNDEFINED; }; - object.prototype = new parent(); - extend(object.prototype, members); - return object; -} + /** + * 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 + */ + function syncTimeout(fn, delay, context) { + if (delay) { + return setTimeout(fn, delay, context); + } + fn.call(0, context); + } -/** - * Pad a string to a given length by adding 0 to the beginning - * @param {Number} number - * @param {Number} length - */ -function pad(number, length) { - // Create an array of the remaining length +1 and join it with 0's - return new Array((length || 2) + 1 - String(number).length).join(0) + number; -} -/** - * Return a length based on either the integer value, or a percentage of a base. - */ -function relativeLength (value, base) { - return (/%$/).test(value) ? base * parseFloat(value) / 100 : parseFloat(value); -} + /** + * Return the first value that is defined. Like MooTools' $.pick. + */ + var pick = Highcharts.pick = function () { + var args = arguments, + i, + arg, + length = args.length; + for (i = 0; i < length; i++) { + arg = args[i]; + if (arg !== UNDEFINED && arg !== null) { + return arg; + } + } + }; -/** - * 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. - */ -var wrap = Highcharts.wrap = function (obj, method, func) { - var proceed = obj[method]; - obj[method] = function () { - var args = Array.prototype.slice.call(arguments); - args.unshift(proceed); - return func.apply(this, args); - }; -}; + /** + * Set CSS on a given element + * @param {Object} el + * @param {Object} styles Style object with camel case property names + */ + function css(el, styles) { + if (isMS && !hasSVG) { // #2686 + if (styles && styles.opacity !== UNDEFINED) { + styles.filter = 'alpha(opacity=' + (styles.opacity * 100) + ')'; + } + } + 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 + */ + function createElement(tag, attribs, styles, parent, nopad) { + var el = doc.createElement(tag); + if (attribs) { + extend(el, attribs); + } + if (nopad) { + css(el, { padding: 0, border: 'none', margin: 0 }); + } + if (styles) { + css(el, styles); + } + if (parent) { + parent.appendChild(el); + } + return el; + } -function getTZOffset(timestamp) { - return ((getTimezoneOffset && getTimezoneOffset(timestamp)) || timezoneOffset || 0) * 60000; -} + /** + * Extend a prototyped class by new members + * @param {Object} parent + * @param {Object} members + */ + function extendClass(Parent, members) { + var object = function () { + }; + object.prototype = new Parent(); + extend(object.prototype, members); + return object; + } -/** - * Based on http://www.php.net/manual/en/function.strftime.php - * @param {String} format - * @param {Number} timestamp - * @param {Boolean} capitalize - */ -dateFormat = function (format, timestamp, capitalize) { - if (!defined(timestamp) || isNaN(timestamp)) { - return defaultOptions.lang.invalidDate || ''; - } - format = pick(format, '%Y-%m-%d %H:%M:%S'); + /** + * Pad a string to a given length by adding 0 to the beginning + * @param {Number} number + * @param {Number} length + */ + function pad(number, length) { + return new Array((length || 2) + 1 - String(number).length).join(0) + number; + } - var date = new Date(timestamp - getTZOffset(timestamp)), - key, // used in for constuct below - // get the basic time values - hours = date[getHours](), - day = date[getDay](), - dayOfMonth = date[getDate](), - month = date[getMonth](), - fullYear = date[getFullYear](), - lang = defaultOptions.lang, - langWeekdays = lang.weekdays, + /** + * Return a length based on either the integer value, or a percentage of a base. + */ + function relativeLength(value, base) { + return (/%$/).test(value) ? base * parseFloat(value) / 100 : parseFloat(value); + } - // List all format keys. Custom formats can be added from the outside. - replacements = extend({ + /** + * 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. + */ + var wrap = Highcharts.wrap = function (obj, method, func) { + var proceed = obj[method]; + obj[method] = function () { + var args = Array.prototype.slice.call(arguments); + args.unshift(proceed); + return func.apply(this, args); + }; + }; - // Day - 'a': 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': dayOfMonth, // Day of the month, 1 through 31 - 'w': day, - // Week (none implemented) - //'W': weekNumber(), + function getTZOffset(timestamp) { + return ((getTimezoneOffset && getTimezoneOffset(timestamp)) || timezoneOffset || 0) * 60000; + } - // 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 + /** + * Based on http://www.php.net/manual/en/function.strftime.php + * @param {String} format + * @param {Number} timestamp + * @param {Boolean} capitalize + */ + dateFormat = function (format, timestamp, capitalize) { + if (!defined(timestamp) || isNaN(timestamp)) { + return defaultOptions.lang.invalidDate || ''; + } + format = pick(format, '%Y-%m-%d %H:%M:%S'); - // Year - 'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009 - 'Y': fullYear, // Four digits year, like 2009 + var date = new Date(timestamp - getTZOffset(timestamp)), + key, // used in for constuct below + // get the basic time values + hours = date[getHours](), + day = date[getDay](), + dayOfMonth = date[getDate](), + month = date[getMonth](), + fullYear = date[getFullYear](), + lang = defaultOptions.lang, + langWeekdays = lang.weekdays, - // 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[getMinutes]()), // 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(mathRound(timestamp % 1000), 3) // Milliseconds (naming from Ruby) - }, Highcharts.dateFormats); + // List all format keys. Custom formats can be added from the outside. + replacements = extend({ + // Day + 'a': 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': dayOfMonth, // Day of the month, 1 through 31 + 'w': day, - // 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]); - } - } + // Week (none implemented) + //'W': weekNumber(), - // Optionally capitalize the string and return - return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format; -}; + // 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 -/** - * Format a single variable. Similar to sprintf, without the % prefix. - */ -function formatSingle(format, val) { - var floatRegex = /f$/, - decRegex = /\.([0-9])/, - lang = defaultOptions.lang, - decimals; + // Year + 'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009 + 'Y': fullYear, // Four digits year, like 2009 - if (floatRegex.test(format)) { // float - decimals = format.match(decRegex); - decimals = decimals ? decimals[1] : -1; - if (val !== null) { - val = Highcharts.numberFormat( - val, - decimals, - lang.decimalPoint, - format.indexOf(',') > -1 ? lang.thousandsSep : '' - ); - } - } else { - val = dateFormat(format, val); - } - return val; -} + // 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[getMinutes]()), // 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(mathRound(timestamp % 1000), 3) // Milliseconds (naming from Ruby) + }, Highcharts.dateFormats); -/** - * Format a string according to a subset of the rules of Python's String.format method. - */ -function format(str, ctx) { - var splitter = '{', - isInside = false, - segment, - valueAndFormat, - path, - i, - len, - ret = [], - val, - index; - - while ((index = str.indexOf(splitter)) !== -1) { - - 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 - len = path.length; - val = ctx; - // Assign deeper paths - for (i = 0; i < len; i++) { - val = val[path[i]]; - } + // 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]); + } + } - // Format the replacement - if (valueAndFormat.length) { - val = formatSingle(valueAndFormat.join(':'), val); - } + // Optionally capitalize the string and return + return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format; + }; - // Push the result and advance the cursor - ret.push(val); - - } else { - ret.push(segment); - - } - str = str.slice(index + 1); // the rest - isInside = !isInside; // toggle - splitter = isInside ? '}' : '{'; // now look for next matching bracket - } - ret.push(str); - return ret.join(''); -} + /** + * Format a single variable. Similar to sprintf, without the % prefix. + */ + function formatSingle(format, val) { + var floatRegex = /f$/, + decRegex = /\.([0-9])/, + lang = defaultOptions.lang, + decimals; -/** - * Get the magnitude of a number - */ -function getMagnitude(num) { - return math.pow(10, mathFloor(math.log(num) / math.LN10)); -} + if (floatRegex.test(format)) { // float + decimals = format.match(decRegex); + decimals = decimals ? decimals[1] : -1; + if (val !== null) { + val = Highcharts.numberFormat( + val, + decimals, + lang.decimalPoint, + format.indexOf(',') > -1 ? lang.thousandsSep : '' + ); + } + } else { + val = dateFormat(format, val); + } + return val; + } -/** - * 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 - */ -function normalizeTickInterval(interval, multiples, magnitude, allowDecimals, preventExceed) { - var normalized, - i, - retInterval = interval; + /** + * Format a string according to a subset of the rules of Python's String.format method. + */ + function format(str, ctx) { + var splitter = '{', + isInside = false, + segment, + valueAndFormat, + path, + i, + len, + ret = [], + val, + index; - // round to a tenfold of 1, 2, 2.5 or 5 - magnitude = pick(magnitude, 1); - normalized = interval / magnitude; + while ((index = str.indexOf(splitter)) !== -1) { - // multiples for a linear scale - if (!multiples) { - multiples = [1, 2, 2.5, 5, 10]; + segment = str.slice(0, index); + if (isInside) { // we're on the closing bracket looking back - // the allowDecimals option - if (allowDecimals === false) { - if (magnitude === 1) { - multiples = [1, 2, 5, 10]; - } else if (magnitude <= 0.1) { - multiples = [1 / magnitude]; - } - } - } + valueAndFormat = segment.split(':'); + path = valueAndFormat.shift().split('.'); // get first and leave format + len = path.length; + val = ctx; - // 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))) { - break; - } - } + // Assign deeper paths + for (i = 0; i < len; i++) { + val = val[path[i]]; + } - // multiply back to the correct magnitude - retInterval *= magnitude; - - return retInterval; -} + // Format the replacement + if (valueAndFormat.length) { + val = formatSingle(valueAndFormat.join(':'), val); + } + // Push the result and advance the cursor + ret.push(val); -/** - * 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. - */ -function stableSort(arr, sortFunction) { - var length = arr.length, - sortValue, - i; + } else { + ret.push(segment); - // Add index to each item - for (i = 0; i < length; i++) { - arr[i].ss_i = i; // stable sort index - } + } + str = str.slice(index + 1); // the rest + isInside = !isInside; // toggle + splitter = isInside ? '}' : '{'; // now look for next matching bracket + } + ret.push(str); + return ret.join(''); + } - arr.sort(function (a, b) { - sortValue = sortFunction(a, b); - return sortValue === 0 ? a.ss_i - b.ss_i : sortValue; - }); + /** + * Get the magnitude of a number + */ + function getMagnitude(num) { + return math.pow(10, mathFloor(math.log(num) / math.LN10)); + } - // Remove index from items - for (i = 0; i < length; i++) { - delete arr[i].ss_i; // stable sort index - } -} + /** + * 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 + */ + function normalizeTickInterval(interval, multiples, magnitude, allowDecimals, preventExceed) { + var normalized, + i, + retInterval = interval; -/** - * 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(data) { - var i = data.length, - min = data[0]; + // round to a tenfold of 1, 2, 2.5 or 5 + magnitude = pick(magnitude, 1); + normalized = interval / magnitude; - while (i--) { - if (data[i] < min) { - min = data[i]; - } - } - return min; -} + // multiples for a linear scale + if (!multiples) { + multiples = [1, 2, 2.5, 5, 10]; -/** - * 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 arrayMax(data) { - var i = data.length, - max = data[0]; + // the allowDecimals option + if (allowDecimals === false) { + if (magnitude === 1) { + multiples = [1, 2, 5, 10]; + } else if (magnitude <= 0.1) { + multiples = [1 / magnitude]; + } + } + } - while (i--) { - if (data[i] > max) { - max = data[i]; - } - } - return max; -} + // 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))) { + break; + } + } -/** - * 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. - */ -function destroyObjectProperties(obj, except) { - var n; - for (n in obj) { - // If the object is non-null and destroy is defined - if (obj[n] && obj[n] !== except && obj[n].destroy) { - // Invoke the destroy - obj[n].destroy(); - } + // multiply back to the correct magnitude + retInterval *= magnitude; - // Delete the property from the object. - delete obj[n]; - } -} + return retInterval; + } -/** - * Discard an element by moving it to the bin and delete - * @param {Object} The HTML node to discard - */ -function discardElement(element) { - // create a garbage bin element, not part of the DOM - if (!garbageBin) { - garbageBin = createElement(DIV); - } + /** + * 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. + */ + function stableSort(arr, sortFunction) { + var length = arr.length, + sortValue, + i; - // move the node and empty bin - if (element) { - garbageBin.appendChild(element); - } - garbageBin.innerHTML = ''; -} + // Add index to each item + for (i = 0; i < length; i++) { + arr[i].safeI = i; // stable sort index + } -/** - * Provide error messages for debugging, with links to online explanation - */ -function error (code, stop) { - var msg = 'Highcharts error #' + code + ': www.highcharts.com/errors/' + code; - if (stop) { - throw msg; - } - // else ... - if (win.console) { - console.log(msg); - } -} + arr.sort(function (a, b) { + sortValue = sortFunction(a, b); + return sortValue === 0 ? a.safeI - b.safeI : sortValue; + }); -/** - * Fix JS round off float errors - * @param {Number} num - */ -function correctFloat(num, prec) { - return parseFloat( - num.toPrecision(prec || 14) - ); -} + // Remove index from items + for (i = 0; i < length; i++) { + delete arr[i].safeI; // stable sort index + } + } -/** - * 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 - */ -function setAnimation(animation, chart) { - chart.renderer.globalAnimation = pick(animation, chart.animation); -} + /** + * 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(data) { + var i = data.length, + min = data[0]; -/** - * The time unit lookup - */ -timeUnits = { - millisecond: 1, - second: 1000, - minute: 60000, - hour: 3600000, - day: 24 * 3600000, - week: 7 * 24 * 3600000, - month: 28 * 24 * 3600000, - year: 364 * 24 * 3600000 -}; + while (i--) { + if (data[i] < min) { + min = data[i]; + } + } + 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. + */ + function arrayMax(data) { + var i = data.length, + max = data[0]; -/** - * 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} decPoint 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 - */ -Highcharts.numberFormat = function (number, decimals, decPoint, thousandsSep) { - var lang = defaultOptions.lang, - // http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_number_format/ - n = +number || 0, - c = decimals === -1 ? - mathMin((n.toString().split('.')[1] || '').length, 20) : // Preserve decimals. Not huge numbers (#3793). - (isNaN(decimals = mathAbs(decimals)) ? 2 : decimals), - d = decPoint === undefined ? lang.decimalPoint : decPoint, - t = thousandsSep === undefined ? lang.thousandsSep : thousandsSep, - s = n < 0 ? "-" : "", - i = String(pInt(n = mathAbs(n).toFixed(c))), - j = i.length > 3 ? i.length % 3 : 0; + while (i--) { + if (data[i] > max) { + max = data[i]; + } + } + return max; + } - return (s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + - (c ? d + mathAbs(n - i).toFixed(c).slice(2) : "")); -}; -/** - * Path interpolation algorithm used across adapters - */ -pathAnim = { - /** - * Prepare start and end values so that the path can be animated one to one - */ - init: function (elem, fromD, toD) { - fromD = fromD || ''; - var shift = elem.shift, - bezier = fromD.indexOf('C') > -1, - numParams = bezier ? 7 : 3, - endLength, - slice, - i, - start = fromD.split(' '), - end = [].concat(toD), // copy - startBaseLine, - endBaseLine, - sixify = function (arr) { // in splines make move points have six parameters like bezier curves - i = arr.length; - while (i--) { - if (arr[i] === M) { - arr.splice(i + 1, 0, arr[i + 1], arr[i + 2], arr[i + 1], arr[i + 2]); - } - } - }; + /** + * 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. + */ + function destroyObjectProperties(obj, except) { + var n; + for (n in obj) { + // If the object is non-null and destroy is defined + if (obj[n] && obj[n] !== except && obj[n].destroy) { + // Invoke the destroy + obj[n].destroy(); + } - if (bezier) { - sixify(start); - sixify(end); - } + // Delete the property from the object. + delete obj[n]; + } + } - // pull out the base lines before padding - if (elem.isArea) { - startBaseLine = start.splice(start.length - 6, 6); - endBaseLine = end.splice(end.length - 6, 6); - } - // if shifting points, prepend a dummy point to the end path - if (shift <= end.length / numParams && start.length === end.length) { - while (shift--) { - end = [].concat(end).splice(0, numParams).concat(end); - } - } - elem.shift = 0; // reset for following animations + /** + * Discard an element by moving it to the bin and delete + * @param {Object} The HTML node to discard + */ + function discardElement(element) { + // create a garbage bin element, not part of the DOM + if (!garbageBin) { + garbageBin = createElement(DIV); + } - // copy and append last point until the length matches the end length - if (start.length) { - endLength = end.length; - while (start.length < endLength) { + // move the node and empty bin + if (element) { + garbageBin.appendChild(element); + } + garbageBin.innerHTML = ''; + } - //bezier && sixify(start); - slice = [].concat(start).splice(start.length - numParams, numParams); - if (bezier) { // disable first control point - slice[numParams - 6] = slice[numParams - 2]; - slice[numParams - 5] = slice[numParams - 1]; - } - start = start.concat(slice); - } - } + /** + * Fix JS round off float errors + * @param {Number} num + */ + function correctFloat(num, prec) { + return parseFloat( + num.toPrecision(prec || 14) + ); + } - if (startBaseLine) { // append the base lines for areas - start = start.concat(startBaseLine); - end = end.concat(endBaseLine); - } - return [start, end]; - }, + /** + * 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 + */ + function setAnimation(animation, chart) { + chart.renderer.globalAnimation = pick(animation, chart.animation); + } - /** - * Interpolate each value of the path and return the array - */ - step: function (start, end, pos, complete) { - var ret = [], - i = start.length, - startVal; + /** + * The time unit lookup + */ + timeUnits = { + millisecond: 1, + second: 1000, + minute: 60000, + hour: 3600000, + day: 24 * 3600000, + week: 7 * 24 * 3600000, + month: 28 * 24 * 3600000, + year: 364 * 24 * 3600000 + }; - if (pos === 1) { // land on the final path without adjustment points appended in the ends - ret = complete; - } else if (i === end.length && pos < 1) { - while (i--) { - startVal = parseFloat(start[i]); - ret[i] = - isNaN(startVal) ? // a letter instruction like M or L - start[i] : - pos * (parseFloat(end[i] - startVal)) + startVal; + /** + * 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} decPoint 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 + */ + Highcharts.numberFormat = function (number, decimals, decPoint, thousandsSep) { + var lang = defaultOptions.lang, + // http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_number_format/ + n = +number || 0, + c = decimals === -1 ? + Math.min((n.toString().split('.')[1] || '').length, 20) : // Preserve decimals. Not huge numbers (#3793). + (isNaN(decimals = Math.abs(decimals)) ? 2 : decimals), + d = decPoint === undefined ? lang.decimalPoint : decPoint, + t = thousandsSep === undefined ? lang.thousandsSep : thousandsSep, + s = n < 0 ? '-' : '', + i = String(pInt(n = mathAbs(n).toFixed(c))), + j = i.length > 3 ? i.length % 3 : 0; - } - } else { // if animation is finished or length not matching, land on right value - ret = end; - } - return ret; - } -}; + return (s + (j ? i.substr(0, j) + t : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + t) + + (c ? d + mathAbs(n - i).toFixed(c).slice(2) : '')); + }; + /** + * Path interpolation algorithm used across adapters + */ + pathAnim = { + /** + * Prepare start and end values so that the path can be animated one to one + */ + init: function (elem, fromD, toD) { + fromD = fromD || ''; + var shift = elem.shift, + bezier = fromD.indexOf('C') > -1, + numParams = bezier ? 7 : 3, + endLength, + slice, + i, + start = fromD.split(' '), + end = [].concat(toD), // copy + startBaseLine, + endBaseLine, + sixify = function (arr) { // in splines make move points have six parameters like bezier curves + i = arr.length; + while (i--) { + if (arr[i] === M) { + arr.splice(i + 1, 0, arr[i + 1], arr[i + 2], arr[i + 1], arr[i + 2]); + } + } + }; -(function ($) { - /** - * The default HighchartsAdapter for jQuery - */ - win.HighchartsAdapter = win.HighchartsAdapter || ($ && { - - /** - * Initialize the adapter by applying some extensions to jQuery - */ - init: function (pathAnim) { - - // extend the animate function to allow SVG animations - var Fx = $.fx; - - /*jslint unparam: true*//* allow unused param x in this function */ - $.extend($.easing, { - easeOutQuad: function (x, t, b, c, d) { - return -c * (t /= d) * (t - 2) + b; - } - }); - /*jslint unparam: false*/ - - // extend some methods to check for elem.attr, which means it is a Highcharts SVG object - $.each(['cur', '_default', 'width', 'height', 'opacity'], function (i, fn) { - var obj = Fx.step, - base; - - // Handle different parent objects - if (fn === 'cur') { - obj = Fx.prototype; // 'cur', the getter, relates to Fx.prototype - - } else if (fn === '_default' && $.Tween) { // jQuery 1.8 model - obj = $.Tween.propHooks[fn]; - fn = 'set'; - } - - // Overwrite the method - base = obj[fn]; - if (base) { // step.width and step.height don't exist in jQuery < 1.7 - - // create the extended function replacement - obj[fn] = function (fx) { + if (bezier) { + sixify(start); + sixify(end); + } - var elem; - - // Fx.prototype.cur does not use fx argument - fx = i ? fx : this; + // pull out the base lines before padding + if (elem.isArea) { + startBaseLine = start.splice(start.length - 6, 6); + endBaseLine = end.splice(end.length - 6, 6); + } - // Don't run animations on textual properties like align (#1821) - if (fx.prop === 'align') { - return; - } - - // shortcut - elem = fx.elem; - - // Fx.prototype.cur returns the current value. The other ones are setters - // and returning a value has no effect. - return elem.attr ? // is SVG element wrapper - elem.attr(fx.prop, fn === 'cur' ? UNDEFINED : fx.now) : // apply the SVG wrapper's method - base.apply(this, arguments); // use jQuery's built-in method - }; - } - }); + // if shifting points, prepend a dummy point to the end path + if (shift <= end.length / numParams && start.length === end.length) { + while (shift--) { + end = [].concat(end).splice(0, numParams).concat(end); + } + } + elem.shift = 0; // reset for following animations - // Extend the opacity getter, needed for fading opacity with IE9 and jQuery 1.10+ - wrap($.cssHooks.opacity, 'get', function (proceed, elem, computed) { - return elem.attr ? (elem.opacity || 0) : proceed.call(this, elem, computed); - }); - - // Define the setter function for d (path definitions) - this.addAnimSetter('d', function (fx) { - var elem = fx.elem, - ends; - - // Normally start and end should be set in state == 0, but sometimes, - // for reasons unknown, this doesn't happen. Perhaps state == 0 is skipped - // in these cases - if (!fx.started) { - ends = pathAnim.init(elem, elem.d, elem.toD); - fx.start = ends[0]; - fx.end = ends[1]; - fx.started = true; - } - - // Interpolate each value of the path - elem.attr('d', pathAnim.step(fx.start, fx.end, fx.pos, elem.toD)); - }); - - /** - * Utility for iterating over an array. Parameters are reversed compared to jQuery. - * @param {Array} arr - * @param {Function} fn - */ - this.each = Array.prototype.forEach ? - function (arr, fn) { // modern browsers - return Array.prototype.forEach.call(arr, fn); - - } : - function (arr, fn) { // legacy - var i, - len = arr.length; - for (i = 0; i < len; i++) { - if (fn.call(arr[i], arr[i], i, arr) === false) { - return i; - } - } - }; - - /** - * Register Highcharts as a plugin in the respective framework - */ - $.fn.highcharts = function () { - var constr = 'Chart', // default constructor - args = arguments, - options, - ret, - chart; + // copy and append last point until the length matches the end length + if (start.length) { + endLength = end.length; + while (start.length < endLength) { - if (this[0]) { + //bezier && sixify(start); + slice = [].concat(start).splice(start.length - numParams, numParams); + if (bezier) { // disable first control point + slice[numParams - 6] = slice[numParams - 2]; + slice[numParams - 5] = slice[numParams - 1]; + } + start = start.concat(slice); + } + } - if (isString(args[0])) { - constr = args[0]; - args = Array.prototype.slice.call(args, 1); - } - options = args[0]; + if (startBaseLine) { // append the base lines for areas + start = start.concat(startBaseLine); + end = end.concat(endBaseLine); + } + return [start, end]; + }, - // Create the chart - if (options !== UNDEFINED) { - /*jslint unused:false*/ - options.chart = options.chart || {}; - options.chart.renderTo = this[0]; - chart = new Highcharts[constr](options, args[1]); - ret = this; - /*jslint unused:true*/ - } + /** + * Interpolate each value of the path and return the array + */ + step: function (start, end, pos, complete) { + var ret = [], + i = start.length, + startVal; - // When called without parameters or with the return argument, get a predefined chart - if (options === UNDEFINED) { - ret = charts[attr(this[0], 'data-highcharts-chart')]; - } - } - - return ret; - }; + if (pos === 1) { // land on the final path without adjustment points appended in the ends + ret = complete; - }, + } else if (i === end.length && pos < 1) { + while (i--) { + startVal = parseFloat(start[i]); + ret[i] = + isNaN(startVal) ? // a letter instruction like M or L + start[i] : + pos * (parseFloat(end[i] - startVal)) + startVal; - /** - * Add an animation setter for a specific property - */ - addAnimSetter: function (prop, setter) { - // jQuery 1.8 style - if ($.Tween) { - $.Tween.propHooks[prop] = { - set: setter - }; - // pre 1.8 - } else { - $.fx.step[prop] = setter; - } - }, - - /** - * Downloads a script and executes a callback when done. - * @param {String} scriptLocation - * @param {Function} callback - */ - getScript: $.getScript, - - /** - * Return the index of an item in an array, or -1 if not found - */ - inArray: $.inArray, - - /** - * A direct link to jQuery methods. MooTools and Prototype adapters must be implemented for each case of method. - * @param {Object} elem The HTML element - * @param {String} method Which method to run on the wrapped element - */ - adapterRun: function (elem, method) { - return $(elem)[method](); - }, - - /** - * Filter an array - */ - grep: $.grep, - - /** - * Map an array - * @param {Array} arr - * @param {Function} fn - */ - map: function (arr, fn) { - //return jQuery.map(arr, fn); - var results = [], - i = 0, - len = arr.length; - for (; i < len; i++) { - results[i] = fn.call(arr[i], arr[i], i, arr); - } - return results; - - }, - - /** - * Get the position of an element relative to the top left of the page - */ - offset: function (el) { - return $(el).offset(); - }, - - /** - * Add an event listener - * @param {Object} el A HTML element or custom object - * @param {String} event The event type - * @param {Function} fn The event handler - */ - addEvent: function (el, event, fn) { - $(el).bind(event, fn); - }, - - /** - * Remove event added with addEvent - * @param {Object} el The object - * @param {String} eventType The event type. Leave blank to remove all events. - * @param {Function} handler The function to remove - */ - removeEvent: function (el, eventType, handler) { - // workaround for jQuery issue with unbinding custom events: - // http://forum.jQuery.com/topic/javascript-error-when-unbinding-a-custom-event-using-jQuery-1-4-2 - var func = doc.removeEventListener ? 'removeEventListener' : 'detachEvent'; - if (doc[func] && el && !el[func]) { - el[func] = function () {}; - } - - $(el).unbind(eventType, handler); - }, - - /** - * Fire an event on a custom object - * @param {Object} el - * @param {String} type - * @param {Object} eventArguments - * @param {Function} defaultFunction - */ - fireEvent: function (el, type, eventArguments, defaultFunction) { - var event = $.Event(type), - detachedType = 'detached' + type, - defaultPrevented; - - // Remove warnings in Chrome when accessing returnValue (#2790), layerX and layerY. Although Highcharts - // never uses these properties, Chrome includes them in the default click event and - // raises the warning when they are copied over in the extend statement below. - // - // To avoid problems in IE (see #1010) where we cannot delete the properties and avoid - // testing if they are there (warning in chrome) the only option is to test if running IE. - if (!isMS && eventArguments) { - delete eventArguments.layerX; - delete eventArguments.layerY; - delete eventArguments.returnValue; - } - - extend(event, eventArguments); - - // Prevent jQuery from triggering the object method that is named the - // same as the event. For example, if the event is 'select', jQuery - // attempts calling el.select and it goes into a loop. - if (el[type]) { - el[detachedType] = el[type]; - el[type] = null; - } - - // Wrap preventDefault and stopPropagation in try/catch blocks in - // order to prevent JS errors when cancelling events on non-DOM - // objects. #615. - /*jslint unparam: true*/ - $.each(['preventDefault', 'stopPropagation'], function (i, fn) { - var base = event[fn]; - event[fn] = function () { - try { - base.call(event); - } catch (e) { - if (fn === 'preventDefault') { - defaultPrevented = true; - } - } - }; - }); - /*jslint unparam: false*/ - - // trigger it - $(el).trigger(event); - - // attach the method - if (el[detachedType]) { - el[type] = el[detachedType]; - el[detachedType] = null; - } - - if (defaultFunction && !event.isDefaultPrevented() && !defaultPrevented) { - defaultFunction(event); - } - }, - - /** - * Extension method needed for MooTools - */ - washMouseEvent: function (e) { - var ret = e.originalEvent || e; - - // computed by jQuery, needed by IE8 - if (ret.pageX === UNDEFINED) { // #1236 - ret.pageX = e.pageX; - ret.pageY = e.pageY; - } - - return ret; - }, - - /** - * Animate a HTML element or SVG element wrapper - * @param {Object} el - * @param {Object} params - * @param {Object} options jQuery-like animation options: duration, easing, callback - */ - animate: function (el, params, options) { - var $el = $(el); - if (!el.style) { - el.style = {}; // #1881 - } - if (params.d) { - el.toD = params.d; // keep the array form for paths, used in $.fx.step.d - params.d = 1; // because in jQuery, animating to an array has a different meaning - } - - $el.stop(); - if (params.opacity !== UNDEFINED && el.attr) { - params.opacity += 'px'; // force jQuery to use same logic as width and height (#2161) - } - el.hasAnim = 1; // #3342 - $el.animate(params, options); - - }, - /** - * Stop running animation - */ - stop: function (el) { - if (el.hasAnim) { // #3342, memory leak on calling $(el) from destroy - $(el).stop(); - } - } - }); -}(win.jQuery)); + } + } else { // if animation is finished or length not matching, land on right value + ret = end; + } + return ret; + } + }; + function loadJQueryAdapter($) { + /** + * The default HighchartsAdapter for jQuery + */ + return { -// check for a custom HighchartsAdapter defined prior to this file -var globalAdapter = win.HighchartsAdapter, - adapter = globalAdapter || {}; - -// Initialize the adapter -if (globalAdapter) { - globalAdapter.init.call(globalAdapter, pathAnim); -} + /** + * Initialize the adapter by applying some extensions to jQuery + */ + init: function (pathAnim) { + // extend the animate function to allow SVG animations + var Fx = $.fx; -// Utility functions. If the HighchartsAdapter is not defined, adapter is an empty object -// and all the utility functions will be null. In that case they are populated by the -// default adapters below. -var adapterRun = adapter.adapterRun, - getScript = adapter.getScript, - inArray = adapter.inArray, - each = Highcharts.each = adapter.each, - grep = adapter.grep, - offset = adapter.offset, - map = adapter.map, - addEvent = adapter.addEvent, - removeEvent = adapter.removeEvent, - fireEvent = adapter.fireEvent, - washMouseEvent = adapter.washMouseEvent, - animate = adapter.animate, - stop = adapter.stop; + $.extend($.easing, { + easeOutQuad: function (x, t, b, c, d) { + return -c * (t /= d) * (t - 2) + b; + } + }); + // extend some methods to check for elem.attr, which means it is a Highcharts SVG object + $.each(['cur', '_default', 'width', 'height', 'opacity'], function (i, fn) { + var obj = Fx.step, + base; + // Handle different parent objects + if (fn === 'cur') { + obj = Fx.prototype; // 'cur', the getter, relates to Fx.prototype -/* **************************************************************************** - * Handle the options * - *****************************************************************************/ -defaultOptions = { - colors: ['#7cb5ec', '#434348', '#90ed7d', '#f7a35c', - '#8085e9', '#f15c80', '#e4d354', '#2b908f', '#f45b5b', '#91e8e1'], - symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'], - lang: { - loading: 'Loading...', - months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', - 'August', 'September', 'October', 'November', 'December'], - shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], - weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], - // invalidDate: '', - decimalPoint: '.', - numericSymbols: ['k', 'M', 'G', 'T', 'P', 'E'], // SI prefixes used in axis labels - resetZoom: 'Reset zoom', - resetZoomTitle: 'Reset zoom level 1:1', - thousandsSep: ' ' - }, - global: { - useUTC: true, - //timezoneOffset: 0, - canvasToolsURL: 'http://code.highcharts.com/4.1.9/modules/canvas-tools.js', - VMLRadialGradientURL: 'http://code.highcharts.com/4.1.9/gfx/vml-radial-gradient.png' - }, - chart: { - //animation: true, - //alignTicks: false, - //reflow: true, - //className: null, - //events: { load, selection }, - //margin: [null], - //marginTop: null, - //marginRight: null, - //marginBottom: null, - //marginLeft: null, - borderColor: '#4572A7', - //borderWidth: 0, - borderRadius: 0, - defaultSeriesType: 'line', - ignoreHiddenSeries: true, - //inverted: false, - //shadow: false, - spacing: [10, 10, 15, 10], - //spacingTop: 10, - //spacingRight: 10, - //spacingBottom: 15, - //spacingLeft: 10, - //style: { - // fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font - // fontSize: '12px' - //}, - backgroundColor: '#FFFFFF', - //plotBackgroundColor: null, - plotBorderColor: '#C0C0C0', - //plotBorderWidth: 0, - //plotShadow: false, - //zoomType: '' - resetZoomButton: { - theme: { - zIndex: 20 - }, - position: { - align: 'right', - x: -10, - //verticalAlign: 'top', - y: 10 - } - // relativeTo: 'plot' - } - }, - title: { - text: 'Chart title', - align: 'center', - // floating: false, - margin: 15, - // x: 0, - // verticalAlign: 'top', - // y: null, - style: { - color: '#333333', - fontSize: '18px' - } + } else if (fn === '_default' && $.Tween) { // jQuery 1.8 model + obj = $.Tween.propHooks[fn]; + fn = 'set'; + } - }, - subtitle: { - text: '', - align: 'center', - // floating: false - // x: 0, - // verticalAlign: 'top', - // y: null, - style: { - color: '#555555' - } - }, + // Overwrite the method + base = obj[fn]; + if (base) { // step.width and step.height don't exist in jQuery < 1.7 - plotOptions: { - line: { // base series options - allowPointSelect: false, - showCheckbox: false, - animation: { - duration: 1000 - }, - //connectNulls: false, - //cursor: 'default', - //clip: true, - //dashStyle: null, - //enableMouseTracking: true, - events: {}, - //legendIndex: 0, - //linecap: 'round', - lineWidth: 2, - //shadow: false, - // stacking: null, - marker: { - //enabled: true, - //symbol: null, - lineWidth: 0, - radius: 4, - lineColor: '#FFFFFF', - //fillColor: null, - states: { // states for a single point - hover: { - enabled: true, - lineWidthPlus: 1, - radiusPlus: 2 - }, - select: { - fillColor: '#FFFFFF', - lineColor: '#000000', - lineWidth: 2 - } - } - }, - point: { - events: {} - }, - dataLabels: { - align: 'center', - // defer: true, - // enabled: false, - formatter: function () { - return this.y === null ? '' : Highcharts.numberFormat(this.y, -1); - }, - style: { - color: 'contrast', - fontSize: '11px', - fontWeight: 'bold', - textShadow: '0 0 6px contrast, 0 0 3px contrast' - }, - verticalAlign: 'bottom', // above singular point - x: 0, - y: 0, - // backgroundColor: undefined, - // borderColor: undefined, - // borderRadius: undefined, - // borderWidth: undefined, - padding: 5 - // shadow: false - }, - 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 - } - }, - 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 - } - }, - labels: { - //items: [], - style: { - //font: defaultFont, - position: ABSOLUTE, - color: '#3E576F' - } - }, - legend: { - enabled: true, - align: 'center', - //floating: false, - layout: 'horizontal', - labelFormatter: function () { - return this.name; - }, - //borderWidth: 0, - borderColor: '#909090', - borderRadius: 0, - navigation: { - // animation: true, - activeColor: '#274b6d', - // arrowSize: 12 - inactiveColor: '#CCC' - // style: {} // text styles - }, - // margin: 20, - // reversed: false, - shadow: false, - // backgroundColor: null, - /*style: { - padding: '5px' - },*/ - itemStyle: { - color: '#333333', - fontSize: '12px', - fontWeight: 'bold' - }, - itemHoverStyle: { - //cursor: 'pointer', removed as of #601 - color: '#000' - }, - itemHiddenStyle: { - color: '#CCC' - }, - itemCheckboxStyle: { - position: ABSOLUTE, - width: '13px', // for IE precision - height: '13px' - }, - // itemWidth: undefined, - // symbolRadius: 0, - // symbolWidth: 16, - symbolPadding: 5, - verticalAlign: 'bottom', - // width: undefined, - x: 0, - y: 0, - title: { - //text: null, - style: { - fontWeight: 'bold' - } - } - }, + // create the extended function replacement + obj[fn] = function (effects) { - loading: { - // hideDuration: 100, - labelStyle: { - fontWeight: 'bold', - position: RELATIVE, - top: '45%' - }, - // showDuration: 0, - style: { - position: ABSOLUTE, - backgroundColor: 'white', - opacity: 0.5, - textAlign: 'center' - } - }, + var elem, fx; - tooltip: { - enabled: true, - animation: hasSVG, - //crosshairs: null, - backgroundColor: 'rgba(249, 249, 249, .85)', - borderWidth: 1, - borderRadius: 3, - dateTimeLabelFormats: { - millisecond: '%A, %b %e, %H:%M:%S.%L', - second: '%A, %b %e, %H:%M:%S', - minute: '%A, %b %e, %H:%M', - hour: '%A, %b %e, %H:%M', - day: '%A, %b %e, %Y', - week: 'Week from %A, %b %e, %Y', - month: '%B %Y', - year: '%Y' - }, - footerFormat: '', - //formatter: defaultFormatter, - headerFormat: '<span style="font-size: 10px">{point.key}</span><br/>', - pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b><br/>', - shadow: true, - //shape: 'callout', - //shared: false, - snap: isTouchDevice ? 25 : 10, - style: { - color: '#333333', - cursor: 'default', - fontSize: '12px', - padding: '8px', - pointerEvents: 'none', // #1686 http://caniuse.com/#feat=pointer-events - whiteSpace: 'nowrap' - } - //xDateFormat: '%A, %b %e, %Y', - //valueDecimals: null, - //valuePrefix: '', - //valueSuffix: '' - }, + // Fx.prototype.cur does not use fx argument + fx = i ? effects : this; - credits: { - enabled: true, - text: 'Highcharts.com', - href: 'http://www.highcharts.com', - position: { - align: 'right', - x: -10, - verticalAlign: 'bottom', - y: -5 - }, - style: { - cursor: 'pointer', - color: '#909090', - fontSize: '9px' - } - } -}; + // Don't run animations on textual properties like align (#1821) + if (fx.prop === 'align') { + return; + } + // shortcut + elem = fx.elem; + // Fx.prototype.cur returns the current value. The other ones are setters + // and returning a value has no effect. + if (elem.attr) { // is SVG element wrapper + return elem.attr( + fx.prop.replace('strokeWidth', 'stroke-width'), // #4721 + fn === 'cur' ? undefined : fx.now + ); + } + return base.apply(this, arguments); // use jQuery's built-in method + }; + } + }); -// Series defaults -var defaultPlotOptions = defaultOptions.plotOptions, - defaultSeriesOptions = defaultPlotOptions.line; + // Extend the opacity getter, needed for fading opacity with IE9 and jQuery 1.10+ + wrap($.cssHooks.opacity, 'get', function (proceed, elem, computed) { + return elem.attr ? (elem.opacity || 0) : proceed.call(this, elem, computed); + }); -// set the default time methods -setTimeMethods(); + // Define the setter function for d (path definitions) + this.addAnimSetter('d', function (fx) { + var elem = fx.elem, + ends; + // Normally start and end should be set in state == 0, but sometimes, + // for reasons unknown, this doesn't happen. Perhaps state == 0 is skipped + // in these cases + if (!fx.started) { + ends = pathAnim.init(elem, elem.d, elem.toD); + fx.start = ends[0]; + fx.end = ends[1]; + fx.started = true; + } + // Interpolate each value of the path + elem.attr('d', pathAnim.step(fx.start, fx.end, fx.pos, elem.toD)); + }); -/** - * Set the time methods globally based on the useUTC option. Time method can be either - * local time or UTC (default). - */ -function setTimeMethods() { - var globalOptions = defaultOptions.global, - useUTC = globalOptions.useUTC, - GET = useUTC ? 'getUTC' : 'get', - SET = useUTC ? 'setUTC' : 'set'; + /** + * Utility for iterating over an array. Parameters are reversed compared to jQuery. + * @param {Array} arr + * @param {Function} fn + */ + this.each = Array.prototype.forEach ? + function each(arr, fn) { // modern browsers + return Array.prototype.forEach.call(arr, fn); + } : + function each(arr, fn) { // legacy + var i, + len = arr.length; + for (i = 0; i < len; i++) { + if (fn.call(arr[i], arr[i], i, arr) === false) { + return i; + } + } + }; - Date = globalOptions.Date || window.Date; - timezoneOffset = useUTC && globalOptions.timezoneOffset; - getTimezoneOffset = useUTC && globalOptions.getTimezoneOffset; - makeTime = function (year, month, date, hours, minutes, seconds) { - var d; - if (useUTC) { - d = Date.UTC.apply(0, arguments); - d += getTZOffset(d); - } else { - d = new Date( - year, - month, - pick(date, 1), - pick(hours, 0), - pick(minutes, 0), - pick(seconds, 0) - ).getTime(); - } - return d; - }; - getMinutes = GET + 'Minutes'; - getHours = GET + 'Hours'; - getDay = GET + 'Day'; - getDate = GET + 'Date'; - getMonth = GET + 'Month'; - getFullYear = GET + 'FullYear'; - setMilliseconds = SET + 'Milliseconds'; - setSeconds = SET + 'Seconds'; - setMinutes = SET + 'Minutes'; - setHours = SET + 'Hours'; - setDate = SET + 'Date'; - setMonth = SET + 'Month'; - setFullYear = SET + 'FullYear'; + /** + * Register Highcharts as a plugin in the respective framework + */ + $.fn.highcharts = function () { + var constr = 'Chart', // default constructor + args = arguments, + options, + ret; -} + if (this[0]) { -/** - * Merge the default options with custom options and return the new options structure - * @param {Object} options The new custom options - */ -function setOptions(options) { - - // Copy in the default options - defaultOptions = merge(true, defaultOptions, options); - - // Apply UTC - setTimeMethods(); + if (isString(args[0])) { + constr = args[0]; + args = Array.prototype.slice.call(args, 1); + } + options = args[0]; - return defaultOptions; -} + // Create the chart + if (options !== UNDEFINED) { + options.chart = options.chart || {}; + options.chart.renderTo = this[0]; + ret = new Highcharts[constr](options, args[1]); + ret = this; + } -/** - * Get the updated default options. Until 3.0.7, merely exposing defaultOptions for outside modules - * wasn't enough because the setOptions method created a new object. - */ -function getOptions() { - return defaultOptions; -} + // When called without parameters or with the return argument, get a predefined chart + if (options === UNDEFINED) { + ret = charts[attr(this[0], 'data-highcharts-chart')]; + } + } + return ret; + }; -/** - * Handle color operations. The object methods are chainable. - * @param {String} input The input color in either rbga or hex format - */ -var rgbaRegEx = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/, - hexRegEx = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/, - rgbRegEx = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/; + }, -var Color = function (input) { - // declare variables - var rgba = [], result, stops; + /** + * Add an animation setter for a specific property + */ + addAnimSetter: function (prop, setter) { + // jQuery 1.8 style + if ($.Tween) { + $.Tween.propHooks[prop] = { + set: setter + }; + // pre 1.8 + } else { + $.fx.step[prop] = setter; + } + }, - /** - * Parse the input color to rgba array - * @param {String} input - */ - function init(input) { + /** + * Downloads a script and executes a callback when done. + * @param {String} scriptLocation + * @param {Function} callback + */ + getScript: $.getScript, - // Gradients - if (input && input.stops) { - stops = map(input.stops, function (stop) { - return Color(stop[1]); - }); + /** + * Return the index of an item in an array, or -1 if not found + */ + inArray: $.inArray, - // Solid colors - } else { - // rgba - result = rgbaRegEx.exec(input); - if (result) { - rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)]; - } else { - // hex - result = hexRegEx.exec(input); - if (result) { - rgba = [pInt(result[1], 16), pInt(result[2], 16), pInt(result[3], 16), 1]; - } else { - // rgb - result = rgbRegEx.exec(input); - if (result) { - rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), 1]; - } - } - } - } + /** + * A direct link to jQuery methods. MooTools and Prototype adapters must be implemented for each case of method. + * @param {Object} elem The HTML element + * @param {String} method Which method to run on the wrapped element + */ + adapterRun: function (elem, method) { + return $(elem)[method](); + }, - } - /** - * Return the color a specified format - * @param {String} format - */ - function get(format) { - var ret; + /** + * Filter an array + */ + grep: $.grep, - if (stops) { - ret = merge(input); - ret.stops = [].concat(ret.stops); - each(stops, function (stop, i) { - ret.stops[i] = [ret.stops[i][0], stop.get(format)]; - }); + /** + * Map an array + * @param {Array} arr + * @param {Function} fn + */ + map: function (arr, fn) { + //return jQuery.map(arr, fn); + var results = [], + i, + len = arr.length; + for (i = 0; i < len; i++) { + results[i] = fn.call(arr[i], arr[i], i, arr); + } + return results; - // it's NaN if gradient colors on a column chart - } else if (rgba && !isNaN(rgba[0])) { - if (format === 'rgb') { - ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')'; - } else if (format === 'a') { - ret = rgba[3]; - } else { - ret = 'rgba(' + rgba.join(',') + ')'; - } - } else { - ret = input; - } - return ret; - } + }, - /** - * Brighten the color - * @param {Number} alpha - */ - function brighten(alpha) { - if (stops) { - each(stops, function (stop) { - stop.brighten(alpha); - }); - - } else if (isNumber(alpha) && alpha !== 0) { - var i; - for (i = 0; i < 3; i++) { - rgba[i] += pInt(alpha * 255); + /** + * Get the position of an element relative to the top left of the page + */ + offset: function (el) { + return $(el).offset(); + }, - if (rgba[i] < 0) { - rgba[i] = 0; - } - if (rgba[i] > 255) { - rgba[i] = 255; - } - } - } - return this; - } - /** - * Set the color's opacity to a given alpha value - * @param {Number} alpha - */ - function setOpacity(alpha) { - rgba[3] = alpha; - return this; - } + /** + * Add an event listener + * @param {Object} el A HTML element or custom object + * @param {String} event The event type + * @param {Function} fn The event handler + */ + addEvent: function (el, event, fn) { + $(el).bind(event, fn); + }, - // initialize: parse the input - init(input); + /** + * Remove event added with addEvent + * @param {Object} el The object + * @param {String} eventType The event type. Leave blank to remove all events. + * @param {Function} handler The function to remove + */ + removeEvent: function (el, eventType, handler) { + // workaround for jQuery issue with unbinding custom events: + // http://forum.jQuery.com/topic/javascript-error-when-unbinding-a-custom-event-using-jQuery-1-4-2 + var func = doc.removeEventListener ? 'removeEventListener' : 'detachEvent'; + if (doc[func] && el && !el[func]) { + el[func] = function () {}; + } - // public methods - return { - get: get, - brighten: brighten, - rgba: rgba, - setOpacity: setOpacity, - raw: input - }; -}; + $(el).unbind(eventType, handler); + }, + /** + * Fire an event on a custom object + * @param {Object} el + * @param {String} type + * @param {Object} eventArguments + * @param {Function} defaultFunction + */ + fireEvent: function (el, type, eventArguments, defaultFunction) { + var event = $.Event(type), + detachedType = 'detached' + type, + defaultPrevented; -/** - * A wrapper object for SVG elements - */ -function SVGElement() {} + // Remove warnings in Chrome when accessing returnValue (#2790), layerX and layerY. Although Highcharts + // never uses these properties, Chrome includes them in the default click event and + // raises the warning when they are copied over in the extend statement below. + // + // To avoid problems in IE (see #1010) where we cannot delete the properties and avoid + // testing if they are there (warning in chrome) the only option is to test if running IE. + if (!isMS && eventArguments) { + delete eventArguments.layerX; + delete eventArguments.layerY; + delete eventArguments.returnValue; + } -SVGElement.prototype = { - - // Default base for animation - opacity: 1, - // For labels, these CSS properties are applied to the <text> node directly - textProps: ['fontSize', 'fontWeight', 'fontFamily', 'fontStyle', 'color', - 'lineHeight', 'width', 'textDecoration', 'textOverflow', 'textShadow'], - - /** - * Initialize the SVG renderer - * @param {Object} renderer - * @param {String} nodeName - */ - init: function (renderer, nodeName) { - var wrapper = this; - wrapper.element = nodeName === 'span' ? - createElement(nodeName) : - doc.createElementNS(SVG_NS, nodeName); - wrapper.renderer = renderer; - }, - - /** - * Animate a given attribute - * @param {Object} params - * @param {Number} options The same options as in jQuery animation - * @param {Function} complete Function to perform at the end of animation - */ - 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) { - animOptions = merge(animOptions, {}); //#2625 - if (complete) { // allows using a callback with the global animation without overwriting it - animOptions.complete = complete; - } - animate(this, params, animOptions); - } else { - this.attr(params, null, complete); - } - return this; - }, + extend(event, eventArguments); - /** - * Build an SVG gradient out of a common JavaScript configuration object - */ - colorGradient: function (color, prop, elem) { - var renderer = this.renderer, - colorObject, - gradName, - gradAttr, - radAttr, - gradients, - gradientObject, - stops, - stopColor, - stopOpacity, - radialReference, - n, - id, - key = []; + // Prevent jQuery from triggering the object method that is named the + // same as the event. For example, if the event is 'select', jQuery + // attempts calling el.select and it goes into a loop. + if (el[type]) { + el[detachedType] = el[type]; + el[type] = null; + } - // Apply linear or radial gradients - if (color.linearGradient) { - gradName = 'linearGradient'; - } else if (color.radialGradient) { - gradName = 'radialGradient'; - } + // Wrap preventDefault and stopPropagation in try/catch blocks in + // order to prevent JS errors when cancelling events on non-DOM + // objects. #615. + $.each(['preventDefault', 'stopPropagation'], function (i, fn) { + var base = event[fn]; + event[fn] = function () { + try { + base.call(event); + } catch (e) { + if (fn === 'preventDefault') { + defaultPrevented = true; + } + } + }; + }); - if (gradName) { - gradAttr = color[gradName]; - gradients = renderer.gradients; - stops = color.stops; - radialReference = elem.radialReference; + // trigger it + $(el).trigger(event); - // Keep < 2.2 kompatibility - if (isArray(gradAttr)) { - color[gradName] = gradAttr = { - x1: gradAttr[0], - y1: gradAttr[1], - x2: gradAttr[2], - y2: gradAttr[3], - gradientUnits: 'userSpaceOnUse' - }; - } + // attach the method + if (el[detachedType]) { + el[type] = el[detachedType]; + el[detachedType] = null; + } - // Correct the radial gradient for the radial reference system - if (gradName === 'radialGradient' && radialReference && !defined(gradAttr.gradientUnits)) { - radAttr = gradAttr; // Save the radial attributes for updating - gradAttr = merge(gradAttr, - renderer.getRadialAttr(radialReference, radAttr), - { gradientUnits: 'userSpaceOnUse' } - ); - } + if (defaultFunction && !event.isDefaultPrevented() && !defaultPrevented) { + defaultFunction(event); + } + }, - // Build the unique key to detect whether we need to create a new element (#1282) - for (n in gradAttr) { - if (n !== 'id') { - key.push(n, gradAttr[n]); - } - } - for (n in stops) { - key.push(stops[n]); - } - key = key.join(','); + /** + * Extension method needed for MooTools + */ + washMouseEvent: function (e) { + var ret = e.originalEvent || e; - // Check if a gradient object with the same config object is created within this renderer - if (gradients[key]) { - id = gradients[key].attr('id'); + // computed by jQuery, needed by IE8 + if (ret.pageX === UNDEFINED) { // #1236 + ret.pageX = e.pageX; + ret.pageY = e.pageY; + } - } else { + return ret; + }, - // Set the id and create the element - gradAttr.id = id = PREFIX + idCounter++; - gradients[key] = gradientObject = renderer.createElement(gradName) - .attr(gradAttr) - .add(renderer.defs); + /** + * Animate a HTML element or SVG element wrapper + * @param {Object} el + * @param {Object} params + * @param {Object} options jQuery-like animation options: duration, easing, callback + */ + animate: function (el, params, options) { + var $el = $(el); + if (!el.style) { + el.style = {}; // #1881 + } + if (params.d) { + el.toD = params.d; // keep the array form for paths, used in $.fx.step.d + params.d = 1; // because in jQuery, animating to an array has a different meaning + } - gradientObject.radAttr = radAttr; + $el.stop(); + if (params.opacity !== UNDEFINED && el.attr) { + params.opacity += 'px'; // force jQuery to use same logic as width and height (#2161) + } + el.hasAnim = 1; // #3342 + $el.animate(params, options); - // The gradient needs to keep a list of stops to be able to destroy them - gradientObject.stops = []; - each(stops, function (stop) { - var stopObject; - if (stop[1].indexOf('rgba') === 0) { - colorObject = Color(stop[1]); - stopColor = colorObject.get('rgb'); - stopOpacity = colorObject.get('a'); - } else { - stopColor = stop[1]; - stopOpacity = 1; - } - stopObject = renderer.createElement('stop').attr({ - offset: stop[0], - 'stop-color': stopColor, - 'stop-opacity': stopOpacity - }).add(gradientObject); + }, + /** + * Stop running animation + */ + stop: function (el) { + if (el.hasAnim) { // #3342, memory leak on calling $(el) from destroy + $(el).stop(); + } + } + }; + } + // Utility functions. If the HighchartsAdapter is not defined, adapter is an empty object + // and all the utility functions will be null. In that case they are populated by the + // default adapters below. + var adapterRun, + inArray, + each, + grep, + offset, + map, + addEvent, + removeEvent, + fireEvent, + washMouseEvent, + animate, + stop; - // Add the stop element to the gradient - gradientObject.stops.push(stopObject); - }); - } + /** + * Helper function to load and extend Highcharts with adapter functionality. + * @param {object|function} adapter - HighchartsAdapter or jQuery + */ + Highcharts.loadAdapter = function (adapter) { + + if (adapter) { + // If jQuery, then load our default jQueryAdapter + if (adapter.fn && adapter.fn.jquery) { + adapter = loadJQueryAdapter(adapter); + } + // Initialize the adapter. + if (adapter.init) { + adapter.init(pathAnim); + delete adapter.init; // Avoid copying to Highcharts object + } + // Extend Highcharts with adapter functionality. + Highcharts.extend(Highcharts, adapter); - // Set the reference to the gradient object - elem.setAttribute(prop, 'url(' + renderer.url + '#' + id + ')'); - elem.gradient = key; - } - }, + // Assign values to local functions. + adapterRun = Highcharts.adapterRun; + inArray = Highcharts.inArray; + each = Highcharts.each; + grep = Highcharts.grep; + offset = Highcharts.offset; + map = Highcharts.map; + addEvent = Highcharts.addEvent; + removeEvent = Highcharts.removeEvent; + fireEvent = Highcharts.fireEvent; + washMouseEvent = Highcharts.washMouseEvent; + animate = Highcharts.animate; + stop = Highcharts.stop; + } + }; - /** - * Apply a polyfill to the text-stroke CSS property, by copying the text element - * and apply strokes to the copy. - * - * Contrast checks at http://jsfiddle.net/highcharts/43soe9m1/2/ - * - * docs: update default, document the polyfill and the limitations on hex colors and pixel values, document contrast pseudo-color - */ - applyTextShadow: function (textShadow) { - var elem = this.element, - tspans, - hasContrast = textShadow.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 = forExport || (elem.style.textShadow !== UNDEFINED && !isMS); + // Load adapter if HighchartsAdapter or jQuery is set on the window. + Highcharts.loadAdapter(win.HighchartsAdapter || win.jQuery); + /* **************************************************************************** + * Handle the options * + *****************************************************************************/ + defaultOptions = { + colors: ['#7cb5ec', '#434348', '#90ed7d', '#f7a35c', + '#8085e9', '#f15c80', '#e4d354', '#2b908f', '#f45b5b', '#91e8e1'], + symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'], + lang: { + loading: 'Loading...', + months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', + 'August', 'September', 'October', 'November', 'December'], + shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + // invalidDate: '', + decimalPoint: '.', + numericSymbols: ['k', 'M', 'G', 'T', 'P', 'E'], // SI prefixes used in axis labels + resetZoom: 'Reset zoom', + resetZoomTitle: 'Reset zoom level 1:1', + thousandsSep: ' ' + }, + global: { + useUTC: true, + //timezoneOffset: 0, + canvasToolsURL: 'http://code.highcharts.com/modules/canvas-tools.js', + VMLRadialGradientURL: 'http://code.highcharts.com/4.1.10/gfx/vml-radial-gradient.png' + }, + chart: { + //animation: true, + //alignTicks: false, + //reflow: true, + //className: null, + //events: { load, selection }, + //margin: [null], + //marginTop: null, + //marginRight: null, + //marginBottom: null, + //marginLeft: null, + borderColor: '#4572A7', + //borderWidth: 0, + borderRadius: 0, + defaultSeriesType: 'line', + ignoreHiddenSeries: true, + //inverted: false, + //shadow: false, + spacing: [10, 10, 15, 10], + //spacingTop: 10, + //spacingRight: 10, + //spacingBottom: 15, + //spacingLeft: 10, + //style: { + // fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font + // fontSize: '12px' + //}, + backgroundColor: '#FFFFFF', + //plotBackgroundColor: null, + plotBorderColor: '#C0C0C0', + //plotBorderWidth: 0, + //plotShadow: false, + //zoomType: '' + resetZoomButton: { + theme: { + zIndex: 20 + }, + position: { + align: 'right', + x: -10, + //verticalAlign: 'top', + y: 10 + } + // relativeTo: 'plot' + } + }, + title: { + text: 'Chart title', + align: 'center', + // floating: false, + margin: 15, + // x: 0, + // verticalAlign: 'top', + // y: null, + style: { + color: '#333333', + fontSize: '18px' + } - // 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)); - } + }, + subtitle: { + text: '', + align: 'center', + // floating: false + // x: 0, + // verticalAlign: 'top', + // y: null, + style: { + color: '#555555' + } + }, - // 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'; - } + plotOptions: { + line: { // base series options + allowPointSelect: false, + showCheckbox: false, + animation: { + duration: 1000 + }, + //connectNulls: false, + //cursor: 'default', + //clip: true, + //dashStyle: null, + //enableMouseTracking: true, + events: {}, + //legendIndex: 0, + //linecap: 'round', + lineWidth: 2, + //shadow: false, + // stacking: null, + marker: { + //enabled: true, + //symbol: null, + lineWidth: 0, + radius: 4, + lineColor: '#FFFFFF', + //fillColor: null, + states: { // states for a single point + hover: { + enabled: true, + lineWidthPlus: 1, + radiusPlus: 2 + }, + select: { + fillColor: '#FFFFFF', + lineColor: '#000000', + lineWidth: 2 + } + } + }, + point: { + events: {} + }, + dataLabels: { + align: 'center', + // defer: true, + // enabled: false, + formatter: function () { + return this.y === null ? '' : Highcharts.numberFormat(this.y, -1); + }, + style: { + color: 'contrast', + fontSize: '11px', + fontWeight: 'bold', + textShadow: '0 0 6px contrast, 0 0 3px contrast' + }, + verticalAlign: 'bottom', // above singular point + x: 0, + y: 0, + // backgroundColor: undefined, + // borderColor: undefined, + // borderRadius: undefined, + // borderWidth: undefined, + padding: 5 + // shadow: false + }, + 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 + } + }, + 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 + } + }, + labels: { + //items: [], + style: { + //font: defaultFont, + position: ABSOLUTE, + color: '#3E576F' + } + }, + legend: { + enabled: true, + align: 'center', + //floating: false, + layout: 'horizontal', + labelFormatter: function () { + return this.name; + }, + //borderWidth: 0, + borderColor: '#909090', + borderRadius: 0, + navigation: { + // animation: true, + activeColor: '#274b6d', + // arrowSize: 12 + inactiveColor: '#CCC' + // style: {} // text styles + }, + // margin: 20, + // reversed: false, + shadow: false, + // backgroundColor: null, + /*style: { + padding: '5px' + },*/ + itemStyle: { + color: '#333333', + fontSize: '12px', + fontWeight: 'bold' + }, + itemHoverStyle: { + //cursor: 'pointer', removed as of #601 + color: '#000' + }, + itemHiddenStyle: { + color: '#CCC' + }, + itemCheckboxStyle: { + position: ABSOLUTE, + width: '13px', // for IE precision + height: '13px' + }, + // itemWidth: undefined, + // symbolRadius: 0, + // symbolWidth: 16, + symbolPadding: 5, + verticalAlign: 'bottom', + // width: undefined, + x: 0, + y: 0, + title: { + //text: null, + style: { + fontWeight: 'bold' + } + } + }, - /* 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; - } - // */ + loading: { + // hideDuration: 100, + labelStyle: { + fontWeight: 'bold', + position: RELATIVE, + top: '45%' + }, + // showDuration: 0, + style: { + position: ABSOLUTE, + backgroundColor: 'white', + opacity: 0.5, + textAlign: 'center' + } + }, - // No reason to polyfill, we've got native support - if (supports) { - this.css(styles); // Apply altered textShadow or textRendering workaround - } else { + tooltip: { + enabled: true, + animation: hasSVG, + //crosshairs: null, + backgroundColor: 'rgba(249, 249, 249, .85)', + borderWidth: 1, + borderRadius: 3, + dateTimeLabelFormats: { + millisecond: '%A, %b %e, %H:%M:%S.%L', + second: '%A, %b %e, %H:%M:%S', + minute: '%A, %b %e, %H:%M', + hour: '%A, %b %e, %H:%M', + day: '%A, %b %e, %Y', + week: 'Week from %A, %b %e, %Y', + month: '%B %Y', + year: '%Y' + }, + footerFormat: '', + //formatter: defaultFormatter, + headerFormat: '<span style="font-size: 10px">{point.key}</span><br/>', + pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b><br/>', + shadow: true, + //shape: 'callout', + //shared: false, + snap: isTouchDevice ? 25 : 10, + style: { + color: '#333333', + cursor: 'default', + fontSize: '12px', + padding: '8px', + pointerEvents: 'none', // #1686 http://caniuse.com/#feat=pointer-events + whiteSpace: 'nowrap' + } + //xDateFormat: '%A, %b %e, %Y', + //valueDecimals: null, + //valuePrefix: '', + //valueSuffix: '' + }, - this.fakeTS = true; // Fake text shadow + credits: { + enabled: true, + text: 'Highcharts.com', + href: 'http://www.highcharts.com', + position: { + align: 'right', + x: -10, + verticalAlign: 'bottom', + y: -5 + }, + style: { + cursor: 'pointer', + color: '#909090', + fontSize: '9px' + } + } + }; - // In order to get the right y position of the clones, - // copy over the y setter - this.ySetter = this.xSetter; - tspans = [].slice.call(elem.getElementsByTagName('tspan')); - each(textShadow.split(/\s?,\s?/g), function (textShadow) { - var firstChild = elem.firstChild, - color, - strokeWidth; - - textShadow = textShadow.split(' '); - color = textShadow[textShadow.length - 1]; - // Approximately tune the settings to the text-shadow behaviour - strokeWidth = textShadow[textShadow.length - 2]; + /** + * Set the time methods globally based on the useUTC option. Time method can be either + * local time or UTC (default). + */ + function setTimeMethods() { + var globalOptions = defaultOptions.global, + useUTC = globalOptions.useUTC, + GET = useUTC ? 'getUTC' : 'get', + SET = useUTC ? 'setUTC' : 'set'; - if (strokeWidth) { - 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); - } - } + Date = globalOptions.Date || window.Date; + timezoneOffset = useUTC && globalOptions.timezoneOffset; + getTimezoneOffset = useUTC && globalOptions.getTimezoneOffset; + makeTime = function (year, month, date, hours, minutes, seconds) { + var d; + if (useUTC) { + d = Date.UTC.apply(0, arguments); + d += getTZOffset(d); + } else { + d = new Date( + year, + month, + pick(date, 1), + pick(hours, 0), + pick(minutes, 0), + pick(seconds, 0) + ).getTime(); + } + return d; + }; + getMinutes = GET + 'Minutes'; + getHours = GET + 'Hours'; + getDay = GET + 'Day'; + getDate = GET + 'Date'; + getMonth = GET + 'Month'; + getFullYear = GET + 'FullYear'; + setMilliseconds = SET + 'Milliseconds'; + setSeconds = SET + 'Seconds'; + setMinutes = SET + 'Minutes'; + setHours = SET + 'Hours'; + setDate = SET + 'Date'; + setMonth = SET + 'Month'; + setFullYear = SET + 'FullYear'; - // Create the clone and apply shadow properties - clone = tspan.cloneNode(1); - attr(clone, { - 'class': PREFIX + 'text-shadow', - 'fill': color, - 'stroke': color, - 'stroke-opacity': 1 / mathMax(pInt(strokeWidth), 3), - 'stroke-width': strokeWidth, - 'stroke-linejoin': 'round' - }); - elem.insertBefore(clone, firstChild); - }); - } - }); - } - }, + } - /** - * Set or get a given attribute - * @param {Object|String} hash - * @param {Mixed|Undefined} val - */ - attr: function (hash, val, complete) { - var key, - value, - element = this.element, - hasSetSymbolSize, - ret = this, - skipAttr; + /** + * Merge the default options with custom options and return the new options structure + * @param {Object} options The new custom options + */ + function setOptions(options) { - // single key-value pair - if (typeof hash === 'string' && val !== UNDEFINED) { - key = hash; - hash = {}; - hash[key] = val; - } + // Copy in the default options + defaultOptions = merge(true, defaultOptions, options); - // used as a getter: first argument is a string, second is undefined - if (typeof hash === 'string') { - ret = (this[hash + 'Getter'] || this._defaultGetter).call(this, hash, element); - - // setter - } else { + // Apply UTC + setTimeMethods(); - for (key in hash) { - value = hash[key]; - skipAttr = false; + return defaultOptions; + } + /** + * Get the updated default options. Until 3.0.7, merely exposing defaultOptions for outside modules + * wasn't enough because the setOptions method created a new object. + */ + function getOptions() { + return defaultOptions; + } - if (this.symbolName && /^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)/.test(key)) { - if (!hasSetSymbolSize) { - this.symbolAttr(hash); - hasSetSymbolSize = true; - } - skipAttr = true; - } - if (this.rotation && (key === 'x' || key === 'y')) { - this.doTransform = true; - } - - if (!skipAttr) { - (this[key + 'Setter'] || this._defaultSetter).call(this, value, key, element); - } - // Let the shadow follow the main element - if (this.shadows && /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(key)) { - this.updateShadows(key, value); - } - } - // Update transform. Do this outside the loop to prevent redundant updating for batch setting - // of attributes. - if (this.doTransform) { - this.updateTransform(); - this.doTransform = false; - } - } + // Series defaults + var defaultPlotOptions = defaultOptions.plotOptions, + defaultSeriesOptions = defaultPlotOptions.line; - // In accordance with animate, run a complete callback - if (complete) { - complete(); - } + // set the default time methods + setTimeMethods(); - return ret; - }, - updateShadows: function (key, value) { - var shadows = this.shadows, - i = shadows.length; - while (i--) { - shadows[i].setAttribute( - key, - key === 'height' ? - mathMax(value - (shadows[i].cutHeight || 0), 0) : - key === 'd' ? this.d : value - ); - } - }, + /** + * Handle color operations. The object methods are chainable. + * @param {String} input The input color in either rbga or hex format + */ + function Color(input) { + // Backwards compatibility, allow instanciation without new + if (!(this instanceof Color)) { + return new Color(input); + } + // Initialize + this.init(input); + } + Color.prototype = { - /** - * Add a class name to an element - */ - addClass: function (className) { - var element = this.element, - currentClassName = attr(element, 'class') || ''; + // Collection of parsers. This can be extended from the outside by pushing parsers + // to Highcharts.Colors.prototype.parsers. + parsers: [{ + // RGBA color + regex: /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/, + parse: function (result) { + return [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)]; + } + }, { + // HEX color + regex: /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/, + parse: function (result) { + return [pInt(result[1], 16), pInt(result[2], 16), pInt(result[3], 16), 1]; + } + }, { + // RGB color + regex: /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/, + parse: function (result) { + return [pInt(result[1]), pInt(result[2]), pInt(result[3]), 1]; + } + }], - if (currentClassName.indexOf(className) === -1) { - attr(element, 'class', currentClassName + ' ' + className); - } - return this; - }, - /* hasClass and removeClass are not (yet) needed - hasClass: function (className) { - return attr(this.element, 'class').indexOf(className) !== -1; - }, - removeClass: function (className) { - attr(this.element, 'class', attr(this.element, 'class').replace(className, '')); - return this; - }, - */ + /** + * Parse the input color to rgba array + * @param {String} input + */ + init: function (input) { + var result, + rgba, + i, + parser; - /** - * 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 - */ - symbolAttr: function (hash) { - var wrapper = this; + this.input = input; - each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR', 'anchorX', 'anchorY'], function (key) { - wrapper[key] = pick(hash[key], wrapper[key]); - }); + // Gradients + if (input && input.stops) { + this.stops = map(input.stops, function (stop) { + return new Color(stop[1]); + }); - wrapper.attr({ - d: wrapper.renderer.symbols[wrapper.symbolName]( - wrapper.x, - wrapper.y, - wrapper.width, - wrapper.height, - wrapper - ) - }); - }, + // Solid colors + } else { + i = this.parsers.length; + while (i-- && !rgba) { + parser = this.parsers[i]; + result = parser.regex.exec(input); + if (result) { + rgba = parser.parse(result); + } + } + } + this.rgba = rgba || []; + }, - /** - * Apply a clipping path to this object - * @param {String} id - */ - clip: function (clipRect) { - return this.attr('clip-path', clipRect ? 'url(' + this.renderer.url + '#' + clipRect.id + ')' : NONE); - }, + /** + * Return the color a specified format + * @param {String} format + */ + get: function (format) { + var input = this.input, + rgba = this.rgba, + ret; - /** - * 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 - */ - crisp: function (rect) { + if (this.stops) { + ret = merge(input); + ret.stops = [].concat(ret.stops); + each(this.stops, function (stop, i) { + ret.stops[i] = [ret.stops[i][0], stop.get(format)]; + }); - var wrapper = this, - key, - attribs = {}, - normalizer, - strokeWidth = rect.strokeWidth || wrapper.strokeWidth || 0; + // it's NaN if gradient colors on a column chart + } else if (rgba && !isNaN(rgba[0])) { + if (format === 'rgb' || (!format && rgba[3] === 1)) { + ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')'; + } else if (format === 'a') { + ret = rgba[3]; + } else { + ret = 'rgba(' + rgba.join(',') + ')'; + } + } else { + ret = input; + } + return ret; + }, - normalizer = mathRound(strokeWidth) % 2 / 2; // mathRound because strokeWidth can sometimes have roundoff errors + /** + * Brighten the color + * @param {Number} alpha + */ + brighten: function (alpha) { + var i, + rgba = this.rgba; - // normalize for crisp edges - rect.x = mathFloor(rect.x || wrapper.x || 0) + normalizer; - rect.y = mathFloor(rect.y || wrapper.y || 0) + normalizer; - rect.width = mathFloor((rect.width || wrapper.width || 0) - 2 * normalizer); - rect.height = mathFloor((rect.height || wrapper.height || 0) - 2 * normalizer); - rect.strokeWidth = strokeWidth; + if (this.stops) { + each(this.stops, function (stop) { + stop.brighten(alpha); + }); - for (key in rect) { - if (wrapper[key] !== rect[key]) { // only set attribute if changed - wrapper[key] = attribs[key] = rect[key]; - } - } + } else if (isNumber(alpha) && alpha !== 0) { + for (i = 0; i < 3; i++) { + rgba[i] += pInt(alpha * 255); - return attribs; - }, + if (rgba[i] < 0) { + rgba[i] = 0; + } + if (rgba[i] > 255) { + rgba[i] = 255; + } + } + } + return this; + }, - /** - * Set styles for the element - * @param {Object} styles - */ - css: function (styles) { - var elemWrapper = this, - oldStyles = elemWrapper.styles, - newStyles = {}, - elem = elemWrapper.element, - textWidth, - n, - serializedCss = '', - hyphenate, - hasNew = !oldStyles; + /** + * Set the color's opacity to a given alpha value + * @param {Number} alpha + */ + setOpacity: function (alpha) { + this.rgba[3] = alpha; + return this; + } + }; - // convert legacy - if (styles && styles.color) { - styles.fill = styles.color; - } - // Filter out existing styles to increase performance (#2640) - if (oldStyles) { - for (n in styles) { - if (styles[n] !== oldStyles[n]) { - newStyles[n] = styles[n]; - hasNew = true; - } - } - } - if (hasNew) { - textWidth = elemWrapper.textWidth = - (styles && styles.width && elem.nodeName.toLowerCase() === 'text' && pInt(styles.width)) || - elemWrapper.textWidth; // #3501 + /** + * A wrapper object for SVG elements + */ + function SVGElement() {} - // Merge the new styles with the old ones - if (oldStyles) { - styles = extend( - oldStyles, - newStyles - ); - } + SVGElement.prototype = { - // store object - elemWrapper.styles = styles; + // Default base for animation + opacity: 1, + // For labels, these CSS properties are applied to the <text> node directly + textProps: ['direction', 'fontSize', 'fontWeight', 'fontFamily', 'fontStyle', 'color', + 'lineHeight', 'width', 'textDecoration', 'textOverflow', 'textShadow'], - if (textWidth && (useCanVG || (!hasSVG && elemWrapper.renderer.forExport))) { - delete styles.width; - } + /** + * Initialize the SVG renderer + * @param {Object} renderer + * @param {String} nodeName + */ + init: function (renderer, nodeName) { + var wrapper = this; + wrapper.element = nodeName === 'span' ? + createElement(nodeName) : + doc.createElementNS(SVG_NS, nodeName); + wrapper.renderer = renderer; + }, - // serialize and set style attribute - if (isMS && !hasSVG) { - css(elemWrapper.element, styles); - } else { - /*jslint unparam: true*/ - hyphenate = function (a, b) { return '-' + b.toLowerCase(); }; - /*jslint unparam: false*/ - for (n in styles) { - serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + styles[n] + ';'; - } - attr(elem, 'style', serializedCss); // #1881 - } + /** + * Animate a given attribute + * @param {Object} params + * @param {Number} options The same options as in jQuery animation + * @param {Function} complete Function to perform at the end of animation + */ + 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) { + animOptions = merge(animOptions, {}); //#2625 + if (complete) { // allows using a callback with the global animation without overwriting it + animOptions.complete = complete; + } + animate(this, params, animOptions); + } else { + this.attr(params, null, complete); + } + return this; + }, + /** + * Build an SVG gradient out of a common JavaScript configuration object + */ + colorGradient: function (color, prop, elem) { + var renderer = this.renderer, + colorObject, + gradName, + gradAttr, + radAttr, + gradients, + gradientObject, + stops, + stopColor, + stopOpacity, + radialReference, + n, + id, + key = [], + value; - // re-build text - if (textWidth && elemWrapper.added) { - elemWrapper.renderer.buildText(elemWrapper); - } - } + // Apply linear or radial gradients + if (color.linearGradient) { + gradName = 'linearGradient'; + } else if (color.radialGradient) { + gradName = 'radialGradient'; + } - return elemWrapper; - }, + if (gradName) { + gradAttr = color[gradName]; + gradients = renderer.gradients; + stops = color.stops; + radialReference = elem.radialReference; - /** - * Add an event listener - * @param {String} eventType - * @param {Function} handler - */ - on: function (eventType, handler) { - var svgElement = this, - element = svgElement.element; - - // touch - if (hasTouch && eventType === 'click') { - element.ontouchstart = function (e) { - svgElement.touchEventFired = Date.now(); - e.preventDefault(); - handler.call(element, e); - }; - element.onclick = function (e) { - if (userAgent.indexOf('Android') === -1 || Date.now() - (svgElement.touchEventFired || 0) > 1100) { // #2269 - handler.call(element, e); - } - }; - } else { - // simplest possible event model for internal use - element['on' + eventType] = handler; - } - return this; - }, + // Keep < 2.2 kompatibility + if (isArray(gradAttr)) { + color[gradName] = gradAttr = { + x1: gradAttr[0], + y1: gradAttr[1], + x2: gradAttr[2], + y2: gradAttr[3], + gradientUnits: 'userSpaceOnUse' + }; + } - /** - * 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. - */ - setRadialReference: function (coordinates) { - var existingGradient = this.renderer.gradients[this.element.gradient]; + // Correct the radial gradient for the radial reference system + if (gradName === 'radialGradient' && radialReference && !defined(gradAttr.gradientUnits)) { + radAttr = gradAttr; // Save the radial attributes for updating + gradAttr = merge(gradAttr, + renderer.getRadialAttr(radialReference, radAttr), + { gradientUnits: 'userSpaceOnUse' } + ); + } - this.element.radialReference = coordinates; - - // On redrawing objects with an existing gradient, the gradient needs - // to be repositioned (#3801) - if (existingGradient && existingGradient.radAttr) { - existingGradient.animate( - this.renderer.getRadialAttr( - coordinates, - existingGradient.radAttr - ) - ); - } + // Build the unique key to detect whether we need to create a new element (#1282) + for (n in gradAttr) { + if (n !== 'id') { + key.push(n, gradAttr[n]); + } + } + for (n in stops) { + key.push(stops[n]); + } + key = key.join(','); - return this; - }, + // Check if a gradient object with the same config object is created within this renderer + if (gradients[key]) { + id = gradients[key].attr('id'); - /** - * Move an object and its children by x and y values - * @param {Number} x - * @param {Number} y - */ - translate: function (x, y) { - return this.attr({ - translateX: x, - translateY: y - }); - }, + } else { - /** - * Invert a group, rotate and flip - */ - invert: function () { - var wrapper = this; - wrapper.inverted = true; - wrapper.updateTransform(); - return wrapper; - }, + // Set the id and create the element + gradAttr.id = id = PREFIX + idCounter++; + gradients[key] = gradientObject = renderer.createElement(gradName) + .attr(gradAttr) + .add(renderer.defs); - /** - * Private method to update the transform attribute based on internal - * properties - */ - updateTransform: function () { - var wrapper = this, - translateX = wrapper.translateX || 0, - translateY = wrapper.translateY || 0, - scaleX = wrapper.scaleX, - scaleY = wrapper.scaleY, - inverted = wrapper.inverted, - rotation = wrapper.rotation, - element = wrapper.element, - transform; + gradientObject.radAttr = radAttr; - // flipping affects translate as adjustment for flipping around the group's axis - if (inverted) { - translateX += wrapper.attr('width'); - translateY += wrapper.attr('height'); - } + // The gradient needs to keep a list of stops to be able to destroy them + gradientObject.stops = []; + each(stops, function (stop) { + var stopObject; + if (stop[1].indexOf('rgba') === 0) { + colorObject = Color(stop[1]); + stopColor = colorObject.get('rgb'); + stopOpacity = colorObject.get('a'); + } else { + stopColor = stop[1]; + stopOpacity = 1; + } + stopObject = renderer.createElement('stop').attr({ + offset: stop[0], + 'stop-color': stopColor, + 'stop-opacity': stopOpacity + }).add(gradientObject); - // Apply translate. Nearly all transformed elements have translation, so instead - // of checking for translate = 0, do it always (#1767, #1846). - transform = ['translate(' + translateX + ',' + translateY + ')']; + // Add the stop element to the gradient + gradientObject.stops.push(stopObject); + }); + } - // apply rotation - if (inverted) { - transform.push('rotate(90) scale(-1,1)'); - } else if (rotation) { // text rotation - transform.push('rotate(' + rotation + ' ' + (element.getAttribute('x') || 0) + ' ' + (element.getAttribute('y') || 0) + ')'); - - // Delete bBox memo when the rotation changes - //delete wrapper.bBox; - } + // Set the reference to the gradient object + value = 'url(' + renderer.url + '#' + id + ')'; + elem.setAttribute(prop, value); + elem.gradient = key; - // apply scale - if (defined(scaleX) || defined(scaleY)) { - transform.push('scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')'); - } + // Allow the color to be concatenated into tooltips formatters etc. (#2995) + color.toString = function () { + return value; + }; + } + }, - if (transform.length) { - element.setAttribute('transform', transform.join(' ')); - } - }, - /** - * Bring the element to the front - */ - toFront: function () { - var element = this.element; - element.parentNode.appendChild(element); - return this; - }, + /** + * Apply a polyfill to the text-stroke CSS property, by copying the text element + * and apply strokes to the copy. + * + * Contrast checks at http://jsfiddle.net/highcharts/43soe9m1/2/ + * + * docs: update default, document the polyfill and the limitations on hex colors and pixel values, document contrast pseudo-color + */ + applyTextShadow: function (textShadow) { + var elem = this.element, + tspans, + hasContrast = textShadow.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 = forExport || (elem.style.textShadow !== UNDEFINED && !isMS); + // 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)); + } - /** - * 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: function (alignOptions, alignByTranslate, box) { - var align, - vAlign, - x, - y, - attribs = {}, - alignTo, - renderer = this.renderer, - alignedObjects = renderer.alignedObjects; + // 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'; + } - // First call on instanciate - if (alignOptions) { - this.alignOptions = alignOptions; - this.alignByTranslate = alignByTranslate; - if (!box || isString(box)) { // boxes other than renderer handle this internally - this.alignTo = alignTo = box || 'renderer'; - erase(alignedObjects, this); // prevent duplicates, like legendGroup after resize - alignedObjects.push(this); - box = null; // reassign it below - } + /* 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; + } + // */ - // When called on resize, no arguments are supplied - } else { - alignOptions = this.alignOptions; - alignByTranslate = this.alignByTranslate; - alignTo = this.alignTo; - } + // No reason to polyfill, we've got native support + if (supports) { + this.css(styles); // Apply altered textShadow or textRendering workaround + } else { - box = pick(box, renderer[alignTo], renderer); + this.fakeTS = true; // Fake text shadow - // Assign variables - align = alignOptions.align; - vAlign = alignOptions.verticalAlign; - x = (box.x || 0) + (alignOptions.x || 0); // default: left align - y = (box.y || 0) + (alignOptions.y || 0); // default: top align + // In order to get the right y position of the clones, + // copy over the y setter + this.ySetter = this.xSetter; - // Align - if (align === 'right' || align === 'center') { - x += (box.width - (alignOptions.width || 0)) / - { right: 1, center: 2 }[align]; - } - attribs[alignByTranslate ? 'translateX' : 'x'] = mathRound(x); + tspans = [].slice.call(elem.getElementsByTagName('tspan')); + each(textShadow.split(/\s?,\s?/g), function (textShadow) { + var firstChild = elem.firstChild, + color, + strokeWidth; + textShadow = textShadow.split(' '); + color = textShadow[textShadow.length - 1]; - // Vertical align - if (vAlign === 'bottom' || vAlign === 'middle') { - y += (box.height - (alignOptions.height || 0)) / - ({ bottom: 1, middle: 2 }[vAlign] || 1); + // Approximately tune the settings to the text-shadow behaviour + strokeWidth = textShadow[textShadow.length - 2]; - } - attribs[alignByTranslate ? 'translateY' : 'y'] = mathRound(y); + if (strokeWidth) { + each(tspans, function (tspan, y) { + var clone; - // Animate only if already placed - this[this.placed ? 'animate' : 'attr'](attribs); - this.placed = true; - this.alignAttr = attribs; + // 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); + } + } - return this; - }, + // Create the clone and apply shadow properties + clone = tspan.cloneNode(1); + attr(clone, { + 'class': PREFIX + 'text-shadow', + 'fill': color, + 'stroke': color, + 'stroke-opacity': 1 / mathMax(pInt(strokeWidth), 3), + 'stroke-width': strokeWidth, + 'stroke-linejoin': 'round' + }); + elem.insertBefore(clone, firstChild); + }); + } + }); + } + }, - /** - * Get the bounding box (width, height, x and y) for the element - */ - getBBox: function (reload) { - var wrapper = this, - bBox,// = wrapper.bBox, - renderer = wrapper.renderer, - width, - height, - rotation = wrapper.rotation, - element = wrapper.element, - styles = wrapper.styles, - rad = rotation * deg2rad, - textStr = wrapper.textStr, - textShadow, - elemStyle = element.style, - toggleTextShadowShim, - cacheKey; + /** + * Set or get a given attribute + * @param {Object|String} hash + * @param {Mixed|Undefined} val + */ + attr: function (hash, val, complete) { + var key, + value, + element = this.element, + hasSetSymbolSize, + ret = this, + skipAttr; - if (textStr !== UNDEFINED) { + // single key-value pair + if (typeof hash === 'string' && val !== UNDEFINED) { + key = hash; + hash = {}; + hash[key] = val; + } - // Properties that affect bounding box - cacheKey = ['', rotation || 0, styles && styles.fontSize, element.style.width].join(','); + // used as a getter: first argument is a string, second is undefined + if (typeof hash === 'string') { + ret = (this[hash + 'Getter'] || this._defaultGetter).call(this, hash, element); - // 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. - if (textStr === '' || numRegex.test(textStr)) { - cacheKey = 'num:' + textStr.toString().length + cacheKey; + // setter + } else { - // Caching all strings reduces rendering time by 4-5%. - } else { - cacheKey = textStr + cacheKey; - } - } + for (key in hash) { + value = hash[key]; + skipAttr = false; - if (cacheKey && !reload) { - bBox = renderer.cache[cacheKey]; - } - // No cache found - if (!bBox) { - // SVG elements - if (element.namespaceURI === SVG_NS || renderer.forExport) { - try { // Fails in Firefox if the container has display: none. + if (this.symbolName && /^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)/.test(key)) { + if (!hasSetSymbolSize) { + this.symbolAttr(hash); + hasSetSymbolSize = true; + } + skipAttr = true; + } - // 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('.' + PREFIX + 'text-shadow'), function (tspan) { - tspan.style.display = display; - }); - }; + if (this.rotation && (key === 'x' || key === 'y')) { + this.doTransform = true; + } - // Workaround for #3842, Firefox reporting wrong bounding box for shadows - if (isFirefox && elemStyle.textShadow) { - textShadow = elemStyle.textShadow; - elemStyle.textShadow = ''; - } else if (toggleTextShadowShim) { - toggleTextShadowShim(NONE); - } + if (!skipAttr) { + (this[key + 'Setter'] || this._defaultSetter).call(this, value, key, element); + } - bBox = element.getBBox ? - // SVG: use extend because IE9 is not allowed to change width and height in case - // of rotation (below) - extend({}, element.getBBox()) : - // Canvas renderer and legacy IE in export mode - { - width: element.offsetWidth, - height: element.offsetHeight - }; + // Let the shadow follow the main element + if (this.shadows && /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(key)) { + this.updateShadows(key, value); + } + } - // #3842 - if (textShadow) { - elemStyle.textShadow = textShadow; - } else if (toggleTextShadowShim) { - toggleTextShadowShim(''); - } - } catch (e) {} + // Update transform. Do this outside the loop to prevent redundant updating for batch setting + // of attributes. + if (this.doTransform) { + this.updateTransform(); + this.doTransform = false; + } - // If the bBox is not set, the try-catch block above failed. The other condition - // is for Opera that returns a width of -Infinity on hidden elements. - if (!bBox || bBox.width < 0) { - bBox = { width: 0, height: 0 }; - } + } + // In accordance with animate, run a complete callback + if (complete) { + complete(); + } - // VML Renderer or useHTML within SVG - } else { + return ret; + }, - bBox = wrapper.htmlGetBBox(); + updateShadows: function (key, value) { + var shadows = this.shadows, + i = shadows.length; + while (i--) { + shadows[i].setAttribute( + key, + key === 'height' ? + Math.max(value - (shadows[i].cutHeight || 0), 0) : + key === 'd' ? this.d : value + ); + } + }, - } + /** + * Add a class name to an element + */ + addClass: function (className) { + var element = this.element, + currentClassName = attr(element, 'class') || ''; - // True SVG elements as well as HTML elements in modern browsers using the .useHTML option - // need to compensated for rotation - if (renderer.isSVG) { - width = bBox.width; - height = bBox.height; + if (currentClassName.indexOf(className) === -1) { + attr(element, 'class', currentClassName + ' ' + className); + } + return this; + }, + /* hasClass and removeClass are not (yet) needed + hasClass: function (className) { + return attr(this.element, 'class').indexOf(className) !== -1; + }, + removeClass: function (className) { + attr(this.element, 'class', attr(this.element, 'class').replace(className, '')); + return this; + }, + */ - // Workaround for wrong bounding box in IE9 and IE10 (#1101, #1505, #1669, #2568) - if (isMS && styles && styles.fontSize === '11px' && height.toPrecision(3) === '16.9') { - bBox.height = height = 14; - } + /** + * 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 + */ + symbolAttr: function (hash) { + var wrapper = this; - // Adjust for rotated text - if (rotation) { - bBox.width = mathAbs(height * mathSin(rad)) + mathAbs(width * mathCos(rad)); - bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad)); - } - } + each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR', 'anchorX', 'anchorY'], function (key) { + wrapper[key] = pick(hash[key], wrapper[key]); + }); - // Cache it - if (cacheKey) { - renderer.cache[cacheKey] = bBox; - } - } - return bBox; - }, + wrapper.attr({ + d: wrapper.renderer.symbols[wrapper.symbolName]( + wrapper.x, + wrapper.y, + wrapper.width, + wrapper.height, + wrapper + ) + }); + }, - /** - * Show the element - */ - show: function (inherit) { - return this.attr({ visibility: inherit ? 'inherit' : VISIBLE }); - }, + /** + * Apply a clipping path to this object + * @param {String} id + */ + clip: function (clipRect) { + return this.attr('clip-path', clipRect ? 'url(' + this.renderer.url + '#' + clipRect.id + ')' : NONE); + }, - /** - * Hide the element - */ - hide: function () { - return this.attr({ visibility: HIDDEN }); - }, + /** + * 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 + */ + crisp: function (rect) { - fadeOut: function (duration) { - var elemWrapper = this; - elemWrapper.animate({ - opacity: 0 - }, { - duration: duration || 150, - complete: function () { - elemWrapper.attr({ y: -9999 }); // #3088, assuming we're only using this for tooltips - } - }); - }, + var wrapper = this, + key, + attribs = {}, + normalizer, + strokeWidth = rect.strokeWidth || wrapper.strokeWidth || 0; - /** - * 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: function (parent) { + normalizer = mathRound(strokeWidth) % 2 / 2; // mathRound because strokeWidth can sometimes have roundoff errors - var renderer = this.renderer, - element = this.element, - inserted; + // normalize for crisp edges + rect.x = mathFloor(rect.x || wrapper.x || 0) + normalizer; + rect.y = mathFloor(rect.y || wrapper.y || 0) + normalizer; + rect.width = mathFloor((rect.width || wrapper.width || 0) - 2 * normalizer); + rect.height = mathFloor((rect.height || wrapper.height || 0) - 2 * normalizer); + rect.strokeWidth = strokeWidth; - if (parent) { - this.parentGroup = parent; - } + for (key in rect) { + if (wrapper[key] !== rect[key]) { // only set attribute if changed + wrapper[key] = attribs[key] = rect[key]; + } + } - // mark as inverted - this.parentInverted = parent && parent.inverted; + return attribs; + }, - // build formatted text - if (this.textStr !== undefined) { - renderer.buildText(this); - } + /** + * Set styles for the element + * @param {Object} styles + */ + css: function (styles) { + var elemWrapper = this, + oldStyles = elemWrapper.styles, + newStyles = {}, + elem = elemWrapper.element, + textWidth, + n, + serializedCss = '', + hyphenate, + hasNew = !oldStyles; - // Mark as added - this.added = true; + // convert legacy + if (styles && styles.color) { + styles.fill = styles.color; + } - // If we're adding to renderer root, or other elements in the group - // have a z index, we need to handle it - if (!parent || parent.handleZ || this.zIndex) { - inserted = this.zIndexSetter(); - } + // Filter out existing styles to increase performance (#2640) + if (oldStyles) { + for (n in styles) { + if (styles[n] !== oldStyles[n]) { + newStyles[n] = styles[n]; + hasNew = true; + } + } + } + if (hasNew) { + textWidth = elemWrapper.textWidth = + (styles && styles.width && elem.nodeName.toLowerCase() === 'text' && pInt(styles.width)) || + elemWrapper.textWidth; // #3501 - // If zIndex is not handled, append at the end - if (!inserted) { - (parent ? parent.element : renderer.box).appendChild(element); - } + // Merge the new styles with the old ones + if (oldStyles) { + styles = extend( + oldStyles, + newStyles + ); + } - // fire an event for internal hooks - if (this.onAdd) { - this.onAdd(); - } + // store object + elemWrapper.styles = styles; - return this; - }, + if (textWidth && (useCanVG || (!hasSVG && elemWrapper.renderer.forExport))) { + delete styles.width; + } - /** - * 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. - */ - safeRemoveChild: function (element) { - var parentNode = element.parentNode; - if (parentNode) { - parentNode.removeChild(element); - } - }, + // serialize and set style attribute + if (isMS && !hasSVG) { + css(elemWrapper.element, styles); + } else { + hyphenate = function (a, b) { + return '-' + b.toLowerCase(); + }; + for (n in styles) { + serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + styles[n] + ';'; + } + attr(elem, 'style', serializedCss); // #1881 + } - /** - * Destroy the element and element wrapper - */ - destroy: function () { - var wrapper = this, - element = wrapper.element || {}, - shadows = wrapper.shadows, - parentToClean = wrapper.renderer.isSVG && element.nodeName === 'SPAN' && wrapper.parentGroup, - grandParent, - key, - i; - // remove events - element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = element.point = null; - stop(wrapper); // stop running animations + // re-build text + if (textWidth && elemWrapper.added) { + elemWrapper.renderer.buildText(elemWrapper); + } + } - if (wrapper.clipPath) { - wrapper.clipPath = wrapper.clipPath.destroy(); - } + return elemWrapper; + }, - // Destroy stops in case this is a gradient object - if (wrapper.stops) { - for (i = 0; i < wrapper.stops.length; i++) { - wrapper.stops[i] = wrapper.stops[i].destroy(); - } - wrapper.stops = null; - } + /** + * Add an event listener + * @param {String} eventType + * @param {Function} handler + */ + on: function (eventType, handler) { + var svgElement = this, + element = svgElement.element; - // remove element - wrapper.safeRemoveChild(element); + // touch + if (hasTouch && eventType === 'click') { + element.ontouchstart = function (e) { + svgElement.touchEventFired = Date.now(); + e.preventDefault(); + handler.call(element, e); + }; + element.onclick = function (e) { + if (userAgent.indexOf('Android') === -1 || Date.now() - (svgElement.touchEventFired || 0) > 1100) { // #2269 + handler.call(element, e); + } + }; + } else { + // simplest possible event model for internal use + element['on' + eventType] = handler; + } + return this; + }, - // destroy shadows - if (shadows) { - each(shadows, function (shadow) { - wrapper.safeRemoveChild(shadow); - }); - } + /** + * 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. + */ + setRadialReference: function (coordinates) { + var existingGradient = this.renderer.gradients[this.element.gradient]; - // In case of useHTML, clean up empty containers emulating SVG groups (#1960, #2393, #2697). - while (parentToClean && parentToClean.div && parentToClean.div.childNodes.length === 0) { - grandParent = parentToClean.parentGroup; - wrapper.safeRemoveChild(parentToClean.div); - delete parentToClean.div; - parentToClean = grandParent; - } + this.element.radialReference = coordinates; - // remove from alignObjects - if (wrapper.alignTo) { - erase(wrapper.renderer.alignedObjects, wrapper); - } + // On redrawing objects with an existing gradient, the gradient needs + // to be repositioned (#3801) + if (existingGradient && existingGradient.radAttr) { + existingGradient.animate( + this.renderer.getRadialAttr( + coordinates, + existingGradient.radAttr + ) + ); + } - for (key in wrapper) { - delete wrapper[key]; - } + return this; + }, - return null; - }, + /** + * Move an object and its children by x and y values + * @param {Number} x + * @param {Number} y + */ + translate: function (x, y) { + return this.attr({ + translateX: x, + translateY: y + }); + }, - /** - * Add a shadow to the element. Must be done after the element is added to the DOM - * @param {Boolean|Object} shadowOptions - */ - shadow: function (shadowOptions, group, cutOff) { - var shadows = [], - i, - shadow, - element = this.element, - strokeWidth, - shadowWidth, - shadowElementOpacity, + /** + * Invert a group, rotate and flip + */ + invert: function () { + var wrapper = this; + wrapper.inverted = true; + wrapper.updateTransform(); + return wrapper; + }, - // compensate for inverted plot area - transform; + /** + * Private method to update the transform attribute based on internal + * properties + */ + updateTransform: function () { + var wrapper = this, + translateX = wrapper.translateX || 0, + translateY = wrapper.translateY || 0, + scaleX = wrapper.scaleX, + scaleY = wrapper.scaleY, + inverted = wrapper.inverted, + rotation = wrapper.rotation, + element = wrapper.element, + transform; + // flipping affects translate as adjustment for flipping around the group's axis + if (inverted) { + translateX += wrapper.attr('width'); + translateY += wrapper.attr('height'); + } - if (shadowOptions) { - shadowWidth = pick(shadowOptions.width, 3); - shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth; - transform = this.parentInverted ? - '(-1,-1)' : - '(' + pick(shadowOptions.offsetX, 1) + ', ' + pick(shadowOptions.offsetY, 1) + ')'; - for (i = 1; i <= shadowWidth; i++) { - shadow = element.cloneNode(0); - strokeWidth = (shadowWidth * 2) + 1 - (2 * i); - attr(shadow, { - 'isShadow': 'true', - 'stroke': shadowOptions.color || 'black', - 'stroke-opacity': shadowElementOpacity * i, - 'stroke-width': strokeWidth, - 'transform': 'translate' + transform, - 'fill': NONE - }); - if (cutOff) { - attr(shadow, 'height', mathMax(attr(shadow, 'height') - strokeWidth, 0)); - shadow.cutHeight = strokeWidth; - } + // Apply translate. Nearly all transformed elements have translation, so instead + // of checking for translate = 0, do it always (#1767, #1846). + transform = ['translate(' + translateX + ',' + translateY + ')']; - if (group) { - group.element.appendChild(shadow); - } else { - element.parentNode.insertBefore(shadow, element); - } + // apply rotation + if (inverted) { + transform.push('rotate(90) scale(-1,1)'); + } else if (rotation) { // text rotation + transform.push('rotate(' + rotation + ' ' + (element.getAttribute('x') || 0) + ' ' + (element.getAttribute('y') || 0) + ')'); - shadows.push(shadow); - } + // Delete bBox memo when the rotation changes + //delete wrapper.bBox; + } - this.shadows = shadows; - } - return this; + // apply scale + if (defined(scaleX) || defined(scaleY)) { + transform.push('scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')'); + } - }, + if (transform.length) { + element.setAttribute('transform', transform.join(' ')); + } + }, + /** + * Bring the element to the front + */ + toFront: function () { + var element = this.element; + element.parentNode.appendChild(element); + return this; + }, - xGetter: function (key) { - if (this.element.nodeName === 'circle') { - key = { x: 'cx', y: 'cy' }[key] || key; - } - return this._defaultGetter(key); - }, - /** - * Get the current value of an attribute or pseudo attribute, used mainly - * for animation. - */ - _defaultGetter: function (key) { - var ret = pick(this[key], this.element ? this.element.getAttribute(key) : null, 0); + /** + * 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: function (alignOptions, alignByTranslate, box) { + var align, + vAlign, + x, + y, + attribs = {}, + alignTo, + renderer = this.renderer, + alignedObjects = renderer.alignedObjects; - if (/^[\-0-9\.]+$/.test(ret)) { // is numerical - ret = parseFloat(ret); - } - return ret; - }, + // First call on instanciate + if (alignOptions) { + this.alignOptions = alignOptions; + this.alignByTranslate = alignByTranslate; + if (!box || isString(box)) { // boxes other than renderer handle this internally + this.alignTo = alignTo = box || 'renderer'; + erase(alignedObjects, this); // prevent duplicates, like legendGroup after resize + alignedObjects.push(this); + box = null; // reassign it below + } + // When called on resize, no arguments are supplied + } else { + alignOptions = this.alignOptions; + alignByTranslate = this.alignByTranslate; + alignTo = this.alignTo; + } - dSetter: function (value, key, element) { - if (value && value.join) { // join path - value = value.join(' '); - } - if (/(NaN| {2}|^$)/.test(value)) { - value = 'M 0 0'; - } - element.setAttribute(key, value); + box = pick(box, renderer[alignTo], renderer); - this[key] = value; - }, - dashstyleSetter: function (value) { - var i; - value = value && value.toLowerCase(); - if (value) { - value = value - .replace('shortdashdotdot', '3,1,1,1,1,1,') - .replace('shortdashdot', '3,1,1,1') - .replace('shortdot', '1,1,') - .replace('shortdash', '3,1,') - .replace('longdash', '8,3,') - .replace(/dot/g, '1,3,') - .replace('dash', '4,3,') - .replace(/,$/, '') - .split(','); // ending comma + // Assign variables + align = alignOptions.align; + vAlign = alignOptions.verticalAlign; + x = (box.x || 0) + (alignOptions.x || 0); // default: left align + y = (box.y || 0) + (alignOptions.y || 0); // default: top align - i = value.length; - while (i--) { - value[i] = pInt(value[i]) * this['stroke-width']; - } - value = value.join(',') - .replace('NaN', 'none'); // #3226 - this.element.setAttribute('stroke-dasharray', value); - } - }, - alignSetter: function (value) { - this.element.setAttribute('text-anchor', { left: 'start', center: 'middle', right: 'end' }[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(SVG_NS, 'title'); - this.element.appendChild(titleNode); - } - titleNode.appendChild( - doc.createTextNode( - (String(pick(value), '')).replace(/<[^>]*>/g, '') // #3276, #3895 - ) - ); - }, - textSetter: function (value) { - if (value !== this.textStr) { - // Delete bBox memo when the text changes - delete this.bBox; - - this.textStr = value; - if (this.added) { - this.renderer.buildText(this); - } - } - }, - fillSetter: function (value, key, element) { - if (typeof value === 'string') { - element.setAttribute(key, value); - } else if (value) { - this.colorGradient(value, key, element); - } - }, - visibilitySetter: function (value, key, element) { - // IE9-11 doesn't handle visibilty:inherit well, so we remove the attribute instead (#2881, #3909) - if (value === 'inherit') { - element.removeAttribute(key); - } else { - element.setAttribute(key, value); - } - }, - zIndexSetter: function (value, key) { - var renderer = this.renderer, - parentGroup = this.parentGroup, - parentWrapper = parentGroup || renderer, - parentNode = parentWrapper.element || renderer.box, - childNodes, - otherElement, - otherZIndex, - element = this.element, - inserted, - run = this.added, - i; - - if (defined(value)) { - element.setAttribute(key, value); // So we can read it for other elements in the group - value = +value; - if (this[key] === value) { // Only update when needed (#3865) - run = false; - } - this[key] = value; - } + // Align + if (align === 'right' || align === 'center') { + x += (box.width - (alignOptions.width || 0)) / + { right: 1, center: 2 }[align]; + } + attribs[alignByTranslate ? 'translateX' : 'x'] = mathRound(x); - // Insert according to this and other elements' zIndex. Before .add() is called, - // nothing is done. Then on add, or by later calls to zIndexSetter, the node - // is placed on the right place in the DOM. - if (run) { - value = this.zIndex; - if (value && parentGroup) { - parentGroup.handleZ = true; - } - - childNodes = parentNode.childNodes; - for (i = 0; i < childNodes.length && !inserted; i++) { - otherElement = childNodes[i]; - otherZIndex = attr(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)) + // Vertical align + if (vAlign === 'bottom' || vAlign === 'middle') { + y += (box.height - (alignOptions.height || 0)) / + ({ bottom: 1, middle: 2 }[vAlign] || 1); - )) { - parentNode.insertBefore(element, otherElement); - inserted = true; - } - } - if (!inserted) { - parentNode.appendChild(element); - } - } - return inserted; - }, - _defaultSetter: function (value, key, element) { - element.setAttribute(key, value); - } -}; + } + attribs[alignByTranslate ? 'translateY' : 'y'] = mathRound(y); -// Some shared setters and getters -SVGElement.prototype.yGetter = SVGElement.prototype.xGetter; -SVGElement.prototype.translateXSetter = SVGElement.prototype.translateYSetter = - SVGElement.prototype.rotationSetter = SVGElement.prototype.verticalAlignSetter = - SVGElement.prototype.scaleXSetter = SVGElement.prototype.scaleYSetter = function (value, key) { - this[key] = value; - this.doTransform = true; -}; + // Animate only if already placed + this[this.placed ? 'animate' : 'attr'](attribs); + this.placed = true; + this.alignAttr = attribs; -// 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) { - this[key] = value; - // Only apply the stroke attribute if the stroke width is defined and larger than 0 - if (this.stroke && this['stroke-width']) { - this.strokeWidth = this['stroke-width']; - SVGElement.prototype.fillSetter.call(this, this.stroke, 'stroke', element); // use prototype as instance may be overridden - element.setAttribute('stroke-width', this['stroke-width']); - this.hasStroke = true; - } else if (key === 'stroke-width' && value === 0 && this.hasStroke) { - element.removeAttribute('stroke'); - this.hasStroke = false; - } -}; + return this; + }, + /** + * Get the bounding box (width, height, x and y) for the element + */ + getBBox: function (reload, rot) { + var wrapper = this, + bBox,// = wrapper.bBox, + renderer = wrapper.renderer, + width, + height, + rotation, + rad, + element = wrapper.element, + styles = wrapper.styles, + textStr = wrapper.textStr, + textShadow, + elemStyle = element.style, + toggleTextShadowShim, + cache = renderer.cache, + cacheKeys = renderer.cacheKeys, + cacheKey; -/** - * The default SVG renderer - */ -var SVGRenderer = function () { - this.init.apply(this, arguments); -}; -SVGRenderer.prototype = { - Element: SVGElement, + rotation = pick(rot, wrapper.rotation); + rad = rotation * deg2rad; - /** - * Initialize the SVGRenderer - * @param {Object} container - * @param {Number} width - * @param {Number} height - * @param {Boolean} forExport - */ - init: function (container, width, height, style, forExport, allowHTML) { - var renderer = this, - loc = location, - boxWrapper, - element, - desc; + if (textStr !== UNDEFINED) { - boxWrapper = renderer.createElement('svg') - .attr({ - version: '1.1' - }) - .css(this.getStyle(style)); - element = boxWrapper.element; - container.appendChild(element); + // Properties that affect bounding box + cacheKey = ['', rotation || 0, styles && styles.fontSize, element.style.width].join(','); - // For browsers other than IE, add the namespace attribute (#1978) - if (container.innerHTML.indexOf('xmlns') === -1) { - attr(element, 'xmlns', SVG_NS); - } + // 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. + if (textStr === '' || numRegex.test(textStr)) { + cacheKey = 'num:' + textStr.toString().length + cacheKey; - // object properties - renderer.isSVG = true; - renderer.box = element; - renderer.boxWrapper = boxWrapper; - renderer.alignedObjects = []; + // Caching all strings reduces rendering time by 4-5%. + } else { + cacheKey = textStr + cacheKey; + } + } - // Page url used for internal references. #24, #672, #1070 - renderer.url = (isFirefox || isWebKit) && doc.getElementsByTagName('base').length ? - loc.href - .replace(/#.*?$/, '') // remove the hash - .replace(/([\('\)])/g, '\\$1') // escape parantheses and quotes - .replace(/ /g, '%20') : // replace spaces (needed for Safari only) - ''; + if (cacheKey && !reload) { + bBox = cache[cacheKey]; + } - // Add description - desc = this.createElement('desc').add(); - desc.element.appendChild(doc.createTextNode('Created with ' + PRODUCT + ' ' + VERSION)); + // No cache found + if (!bBox) { + // SVG elements + if (element.namespaceURI === SVG_NS || renderer.forExport) { + try { // Fails in Firefox if the container has display: none. - renderer.defs = this.createElement('defs').add(); - renderer.allowHTML = allowHTML; - renderer.forExport = forExport; - renderer.gradients = {}; // Object where gradient SvgElements are stored - renderer.cache = {}; // Cache for numerical bounding boxes + // 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('.' + PREFIX + 'text-shadow'), function (tspan) { + tspan.style.display = display; + }); + }; - renderer.setSize(width, height, false); + // Workaround for #3842, Firefox reporting wrong bounding box for shadows + if (isFirefox && elemStyle.textShadow) { + textShadow = elemStyle.textShadow; + elemStyle.textShadow = ''; + } else if (toggleTextShadowShim) { + toggleTextShadowShim(NONE); + } + bBox = element.getBBox ? + // SVG: use extend because IE9 is not allowed to change width and height in case + // of rotation (below) + extend({}, element.getBBox()) : + // Canvas renderer and legacy IE in export mode + { + width: element.offsetWidth, + height: element.offsetHeight + }; + // #3842 + if (textShadow) { + elemStyle.textShadow = textShadow; + } else if (toggleTextShadowShim) { + toggleTextShadowShim(''); + } + } catch (e) {} - // Issue 110 workaround: - // In Firefox, if a div is positioned by percentage, its pixel position may land - // between pixels. The container itself doesn't display this, but an SVG element - // 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 () { - css(container, { left: 0, top: 0 }); - rect = container.getBoundingClientRect(); - css(container, { - left: (mathCeil(rect.left) - rect.left) + PX, - top: (mathCeil(rect.top) - rect.top) + PX - }); - }; + // If the bBox is not set, the try-catch block above failed. The other condition + // is for Opera that returns a width of -Infinity on hidden elements. + if (!bBox || bBox.width < 0) { + bBox = { width: 0, height: 0 }; + } - // run the fix now - subPixelFix(); - // run it on resize - addEvent(win, 'resize', subPixelFix); - } - }, + // VML Renderer or useHTML within SVG + } else { - getStyle: function (style) { - return (this.style = extend({ - fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif', // default font - fontSize: '12px' - }, style)); - }, + bBox = wrapper.htmlGetBBox(); - /** - * Detect whether the renderer is hidden. This happens when one of the parent elements - * has display: none. #608. - */ - isHidden: function () { - return !this.boxWrapper.getBBox().width; - }, + } - /** - * Destroys the renderer and its allocated members. - */ - destroy: function () { - var renderer = this, - rendererDefs = renderer.defs; - renderer.box = null; - renderer.boxWrapper = renderer.boxWrapper.destroy(); + // True SVG elements as well as HTML elements in modern browsers using the .useHTML option + // need to compensated for rotation + if (renderer.isSVG) { + width = bBox.width; + height = bBox.height; - // Call destroy on all gradient elements - destroyObjectProperties(renderer.gradients || {}); - renderer.gradients = null; + // Workaround for wrong bounding box in IE9 and IE10 (#1101, #1505, #1669, #2568) + if (isMS && styles && styles.fontSize === '11px' && height.toPrecision(3) === '16.9') { + bBox.height = height = 14; + } - // Defs are null in VMLRenderer - // Otherwise, destroy them here. - if (rendererDefs) { - renderer.defs = rendererDefs.destroy(); - } + // Adjust for rotated text + if (rotation) { + bBox.width = mathAbs(height * mathSin(rad)) + mathAbs(width * mathCos(rad)); + bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad)); + } + } - // 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); - } + // Cache it + if (cacheKey) { - renderer.alignedObjects = null; + // Rotate (#4681) + while (cacheKeys.length > 250) { + delete cache[cacheKeys.shift()]; + } - return null; - }, + if (!cache[cacheKey]) { + cacheKeys.push(cacheKey); + } + cache[cacheKey] = bBox; + } + } + return bBox; + }, - /** - * Create a wrapper for an SVG element - * @param {Object} nodeName - */ - createElement: function (nodeName) { - var wrapper = new this.Element(); - wrapper.init(this, nodeName); - return wrapper; - }, + /** + * Show the element + */ + show: function (inherit) { + return this.attr({ visibility: inherit ? 'inherit' : VISIBLE }); + }, - /** - * Dummy function for use in canvas renderer - */ - draw: function () {}, + /** + * Hide the element + */ + hide: function () { + return this.attr({ visibility: HIDDEN }); + }, - /** - * Get converted radial gradient attributes - */ - 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] - }; - }, + fadeOut: function (duration) { + var elemWrapper = this; + elemWrapper.animate({ + opacity: 0 + }, { + duration: duration || 150, + complete: function () { + elemWrapper.attr({ y: -9999 }); // #3088, assuming we're only using this for tooltips + } + }); + }, - /** - * Parse a simple HTML string into SVG tspans - * - * @param {Object} textNode The parent text SVG node - */ - buildText: function (wrapper) { - var textNode = wrapper.element, - renderer = this, - forExport = renderer.forExport, - textStr = pick(wrapper.textStr, '').toString(), - hasMarkup = textStr.indexOf('<') !== -1, - lines, - childNodes = textNode.childNodes, - styleRegex, - hrefRegex, - parentX = attr(textNode, 'x'), - textStyles = wrapper.styles, - width = wrapper.textWidth, - textLineHeight = textStyles && textStyles.lineHeight, - textShadow = textStyles && textStyles.textShadow, - ellipsis = textStyles && textStyles.textOverflow === 'ellipsis', - i = childNodes.length, - tempParent = width && !wrapper.added && this.box, - getLineHeight = function (tspan) { - return textLineHeight ? - pInt(textLineHeight) : - renderer.fontMetrics( - /(px|em)$/.test(tspan && tspan.style.fontSize) ? - tspan.style.fontSize : - ((textStyles && textStyles.fontSize) || renderer.style.fontSize || 12), - tspan - ).h; - }, - unescapeAngleBrackets = function (inputStr) { - return inputStr.replace(/&lt;/g, '<').replace(/&gt;/g, '>'); - }; + /** + * 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: function (parent) { - /// remove old text - while (i--) { - textNode.removeChild(childNodes[i]); - } + var renderer = this.renderer, + element = this.element, + inserted; - // Skip tspans, add text directly to text node. The forceTSpan is a hook - // used in text outline hack. - if (!hasMarkup && !textShadow && !ellipsis && textStr.indexOf(' ') === -1) { - textNode.appendChild(doc.createTextNode(unescapeAngleBrackets(textStr))); - return; + if (parent) { + this.parentGroup = parent; + } - // Complex strings, add more logic - } else { + // mark as inverted + this.parentInverted = parent && parent.inverted; - styleRegex = /<.*style="([^"]+)".*>/; - hrefRegex = /<.*href="(http[^"]+)".*>/; + // build formatted text + if (this.textStr !== undefined) { + renderer.buildText(this); + } - if (tempParent) { - tempParent.appendChild(textNode); // attach it to the DOM to read offset width - } + // Mark as added + this.added = true; - if (hasMarkup) { - lines = textStr - .replace(/<(b|strong)>/g, '<span style="font-weight:bold">') - .replace(/<(i|em)>/g, '<span style="font-style:italic">') - .replace(/<a/g, '<span') - .replace(/<\/(b|strong|i|em|a)>/g, '</span>') - .split(/<br.*?>/g); + // If we're adding to renderer root, or other elements in the group + // have a z index, we need to handle it + if (!parent || parent.handleZ || this.zIndex) { + inserted = this.zIndexSetter(); + } - } else { - lines = [textStr]; - } + // If zIndex is not handled, append at the end + if (!inserted) { + (parent ? parent.element : renderer.box).appendChild(element); + } + // fire an event for internal hooks + if (this.onAdd) { + this.onAdd(); + } - // remove empty line at end - if (lines[lines.length - 1] === '') { - lines.pop(); - } + return this; + }, - - // build the lines - each(lines, function (line, lineNo) { - var spans, spanNo = 0; + /** + * 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. + */ + safeRemoveChild: function (element) { + var parentNode = element.parentNode; + if (parentNode) { + parentNode.removeChild(element); + } + }, - line = line.replace(/<span/g, '|||<span').replace(/<\/span>/g, '</span>|||'); - spans = line.split('|||'); + /** + * Destroy the element and element wrapper + */ + destroy: function () { + var wrapper = this, + element = wrapper.element || {}, + shadows = wrapper.shadows, + parentToClean = wrapper.renderer.isSVG && element.nodeName === 'SPAN' && wrapper.parentGroup, + grandParent, + key, + i; - each(spans, function (span) { - if (span !== '' || spans.length === 1) { - var attributes = {}, - tspan = doc.createElementNS(SVG_NS, 'tspan'), - spanStyle; // #390 - if (styleRegex.test(span)) { - spanStyle = span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2'); - attr(tspan, 'style', spanStyle); - } - if (hrefRegex.test(span) && !forExport) { // Not for export - #1529 - attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"'); - css(tspan, { cursor: 'pointer' }); - } + // remove events + element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = element.point = null; + stop(wrapper); // stop running animations - span = unescapeAngleBrackets(span.replace(/<(.|\n)*?>/g, '') || ' '); + if (wrapper.clipPath) { + wrapper.clipPath = wrapper.clipPath.destroy(); + } - // Nested tags aren't supported, and cause crash in Safari (#1596) - if (span !== ' ') { + // Destroy stops in case this is a gradient object + if (wrapper.stops) { + for (i = 0; i < wrapper.stops.length; i++) { + wrapper.stops[i] = wrapper.stops[i].destroy(); + } + wrapper.stops = null; + } - // add the text node - tspan.appendChild(doc.createTextNode(span)); + // remove element + wrapper.safeRemoveChild(element); - if (!spanNo) { // first span in a line, align it to the left - if (lineNo && parentX !== null) { - attributes.x = parentX; - } - } else { - attributes.dx = 0; // #16 - } + // destroy shadows + if (shadows) { + each(shadows, function (shadow) { + wrapper.safeRemoveChild(shadow); + }); + } - // add attributes - attr(tspan, attributes); + // In case of useHTML, clean up empty containers emulating SVG groups (#1960, #2393, #2697). + while (parentToClean && parentToClean.div && parentToClean.div.childNodes.length === 0) { + grandParent = parentToClean.parentGroup; + wrapper.safeRemoveChild(parentToClean.div); + delete parentToClean.div; + parentToClean = grandParent; + } - // Append it - textNode.appendChild(tspan); + // remove from alignObjects + if (wrapper.alignTo) { + erase(wrapper.renderer.alignedObjects, wrapper); + } - // first span on subsequent line, add the line height - if (!spanNo && lineNo) { + for (key in wrapper) { + delete wrapper[key]; + } - // allow getting the right offset height in exporting in IE - if (!hasSVG && forExport) { - css(tspan, { display: 'block' }); - } + return null; + }, - // Set the line height based on the font size of either - // the text element or the tspan element - attr( - tspan, - 'dy', - getLineHeight(tspan) - ); - } + /** + * Add a shadow to the element. Must be done after the element is added to the DOM + * @param {Boolean|Object} shadowOptions + */ + shadow: function (shadowOptions, group, cutOff) { + var shadows = [], + i, + shadow, + element = this.element, + strokeWidth, + shadowWidth, + shadowElementOpacity, - /*if (width) { - renderer.breakText(wrapper, width); - }*/ + // compensate for inverted plot area + transform; - // Check width and apply soft breaks or ellipsis - if (width) { - var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273 - hasWhiteSpace = spans.length > 1 || lineNo || (words.length > 1 && textStyles.whiteSpace !== 'nowrap'), - tooLong, - wasTooLong, - actualWidth, - rest = [], - dy = getLineHeight(tspan), - softLineNo = 1, - rotation = wrapper.rotation, - wordStr = span, // for ellipsis - cursor = wordStr.length, // binary search cursor - bBox; - while ((hasWhiteSpace || ellipsis) && (words.length || rest.length)) { - wrapper.rotation = 0; // discard rotation when computing box - bBox = wrapper.getBBox(true); - actualWidth = bBox.width; + if (shadowOptions) { + shadowWidth = pick(shadowOptions.width, 3); + shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth; + transform = this.parentInverted ? + '(-1,-1)' : + '(' + pick(shadowOptions.offsetX, 1) + ', ' + pick(shadowOptions.offsetY, 1) + ')'; + for (i = 1; i <= shadowWidth; i++) { + shadow = element.cloneNode(0); + strokeWidth = (shadowWidth * 2) + 1 - (2 * i); + attr(shadow, { + 'isShadow': 'true', + 'stroke': shadowOptions.color || 'black', + 'stroke-opacity': shadowElementOpacity * i, + 'stroke-width': strokeWidth, + 'transform': 'translate' + transform, + 'fill': NONE + }); + if (cutOff) { + attr(shadow, 'height', mathMax(attr(shadow, 'height') - strokeWidth, 0)); + shadow.cutHeight = strokeWidth; + } - // Old IE cannot measure the actualWidth for SVG elements (#2314) - if (!hasSVG && renderer.forExport) { - actualWidth = renderer.measureSpanWidth(tspan.firstChild.data, wrapper.styles); - } + if (group) { + group.element.appendChild(shadow); + } else { + element.parentNode.insertBefore(shadow, element); + } - tooLong = actualWidth > width; + shadows.push(shadow); + } - // For ellipsis, do a binary search for the correct string length - if (wasTooLong === undefined) { - wasTooLong = tooLong; // First time - } - if (ellipsis && wasTooLong) { - cursor /= 2; + this.shadows = shadows; + } + return this; - if (wordStr === '' || (!tooLong && cursor < 0.5)) { - words = []; // All ok, break out - } else { - if (tooLong) { - wasTooLong = true; - } - wordStr = span.substring(0, wordStr.length + (tooLong ? -1 : 1) * mathCeil(cursor)); - words = [wordStr + (width > 3 ? '\u2026' : '')]; - tspan.removeChild(tspan.firstChild); - } + }, - // Looping down, this is the first word sequence that is not too long, - // so we can move on to build the next line. - } else if (!tooLong || words.length === 1) { - words = rest; - rest = []; - - if (words.length) { - softLineNo++; - - tspan = doc.createElementNS(SVG_NS, 'tspan'); - attr(tspan, { - dy: dy, - x: parentX - }); - if (spanStyle) { // #390 - attr(tspan, 'style', spanStyle); - } - textNode.appendChild(tspan); - } - if (actualWidth > width) { // a single word is pressing it out - width = actualWidth; - } - } else { // append to existing line tspan - tspan.removeChild(tspan.firstChild); - rest.unshift(words.pop()); - } - if (words.length) { - tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-'))); - } - } - if (wasTooLong) { - wrapper.attr('title', wrapper.textStr); - } - wrapper.rotation = rotation; - } + xGetter: function (key) { + if (this.element.nodeName === 'circle') { + key = { x: 'cx', y: 'cy' }[key] || key; + } + return this._defaultGetter(key); + }, - spanNo++; - } - } - }); - }); - if (tempParent) { - tempParent.removeChild(textNode); // attach it to the DOM to read offset width - } + /** + * Get the current value of an attribute or pseudo attribute, used mainly + * for animation. + */ + _defaultGetter: function (key) { + var ret = pick(this[key], this.element ? this.element.getAttribute(key) : null, 0); - // Apply the text shadow - if (textShadow && wrapper.applyTextShadow) { - wrapper.applyTextShadow(textShadow); - } - } - }, + if (/^[\-0-9\.]+$/.test(ret)) { // is numerical + ret = parseFloat(ret); + } + return ret; + }, - - /* - breakText: function (wrapper, width) { - var bBox = wrapper.getBBox(), - node = wrapper.element, - textLength = node.textContent.length, - pos = mathRound(width * textLength / bBox.width), // try this position first, based on average character width - increment = 0, - finalPos; + dSetter: function (value, key, element) { + if (value && value.join) { // join path + value = value.join(' '); + } + if (/(NaN| {2}|^$)/.test(value)) { + value = 'M 0 0'; + } + element.setAttribute(key, value); - if (bBox.width > width) { - while (finalPos === undefined) { - textLength = node.getSubStringLength(0, pos); + this[key] = value; + }, + dashstyleSetter: function (value) { + var i; + value = value && value.toLowerCase(); + if (value) { + value = value + .replace('shortdashdotdot', '3,1,1,1,1,1,') + .replace('shortdashdot', '3,1,1,1') + .replace('shortdot', '1,1,') + .replace('shortdash', '3,1,') + .replace('longdash', '8,3,') + .replace(/dot/g, '1,3,') + .replace('dash', '4,3,') + .replace(/,$/, '') + .split(','); // ending comma - if (textLength <= width) { - if (increment === -1) { - finalPos = pos; - } else { - increment = 1; - } - } else { - if (increment === 1) { - finalPos = pos - 1; - } else { - increment = -1; - } - } - pos += increment; - } - } - console.log(finalPos, node.getSubStringLength(0, finalPos)) - }, - */ + i = value.length; + while (i--) { + value[i] = pInt(value[i]) * this['stroke-width']; + } + value = value.join(',') + .replace('NaN', 'none'); // #3226 + this.element.setAttribute('stroke-dasharray', value); + } + }, + alignSetter: function (value) { + this.element.setAttribute('text-anchor', { left: 'start', center: 'middle', right: 'end' }[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(SVG_NS, 'title'); + this.element.appendChild(titleNode); + } + titleNode.appendChild( + doc.createTextNode( + (String(pick(value), '')).replace(/<[^>]*>/g, '') // #3276, #3895 + ) + ); + }, + textSetter: function (value) { + if (value !== this.textStr) { + // Delete bBox memo when the text changes + delete this.bBox; - /** - * Returns white for dark colors and black for bright colors - */ - getContrast: function (color) { - color = Color(color).rgba; - return color[0] + color[1] + color[2] > 384 ? '#000000' : '#FFFFFF'; - }, + this.textStr = value; + if (this.added) { + this.renderer.buildText(this); + } + } + }, + fillSetter: function (value, key, element) { + if (typeof value === 'string') { + element.setAttribute(key, value); + } else if (value) { + this.colorGradient(value, key, element); + } + }, + visibilitySetter: function (value, key, element) { + // IE9-11 doesn't handle visibilty:inherit well, so we remove the attribute instead (#2881, #3909) + if (value === 'inherit') { + element.removeAttribute(key); + } else { + element.setAttribute(key, value); + } + }, + zIndexSetter: function (value, key) { + var renderer = this.renderer, + parentGroup = this.parentGroup, + parentWrapper = parentGroup || renderer, + parentNode = parentWrapper.element || renderer.box, + childNodes, + otherElement, + otherZIndex, + element = this.element, + inserted, + run = this.added, + i; - /** - * 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 - */ - 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, - stateOptions, - stateStyle, - normalStyle, - hoverStyle, - pressedStyle, - disabledStyle, - verticalGradient = { x1: 0, y1: 0, x2: 0, y2: 1 }; + if (defined(value)) { + element.setAttribute(key, value); // So we can read it for other elements in the group + value = +value; + if (this[key] === value) { // Only update when needed (#3865) + run = false; + } + this[key] = value; + } - // Normal state - prepare the attributes - normalState = merge({ - 'stroke-width': 1, - stroke: '#CCCCCC', - fill: { - linearGradient: verticalGradient, - stops: [ - [0, '#FEFEFE'], - [1, '#F6F6F6'] - ] - }, - r: 2, - padding: 5, - style: { - color: 'black' - } - }, normalState); - normalStyle = normalState.style; - delete normalState.style; + // Insert according to this and other elements' zIndex. Before .add() is called, + // nothing is done. Then on add, or by later calls to zIndexSetter, the node + // is placed on the right place in the DOM. + if (run) { + value = this.zIndex; - // Hover state - hoverState = merge(normalState, { - stroke: '#68A', - fill: { - linearGradient: verticalGradient, - stops: [ - [0, '#FFF'], - [1, '#ACF'] - ] - } - }, hoverState); - hoverStyle = hoverState.style; - delete hoverState.style; + if (value && parentGroup) { + parentGroup.handleZ = true; + } - // Pressed state - pressedState = merge(normalState, { - stroke: '#68A', - fill: { - linearGradient: verticalGradient, - stops: [ - [0, '#9BD'], - [1, '#CDF'] - ] - } - }, pressedState); - pressedStyle = pressedState.style; - delete pressedState.style; + childNodes = parentNode.childNodes; + for (i = 0; i < childNodes.length && !inserted; i++) { + otherElement = childNodes[i]; + otherZIndex = attr(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)) - // Disabled state - disabledState = merge(normalState, { - style: { - color: '#CCC' - } - }, disabledState); - disabledStyle = disabledState.style; - delete disabledState.style; + )) { + parentNode.insertBefore(element, otherElement); + inserted = true; + } + } + if (!inserted) { + parentNode.appendChild(element); + } + } + return inserted; + }, + _defaultSetter: function (value, key, element) { + element.setAttribute(key, value); + } + }; - // Add the events. IE9 and IE10 need mouseover and mouseout to funciton (#667). - addEvent(label.element, isMS ? 'mouseover' : 'mouseenter', function () { - if (curState !== 3) { - label.attr(hoverState) - .css(hoverStyle); - } - }); - addEvent(label.element, isMS ? 'mouseout' : 'mouseleave', function () { - if (curState !== 3) { - stateOptions = [normalState, hoverState, pressedState][curState]; - stateStyle = [normalStyle, hoverStyle, pressedStyle][curState]; - label.attr(stateOptions) - .css(stateStyle); - } - }); + // Some shared setters and getters + SVGElement.prototype.yGetter = SVGElement.prototype.xGetter; + SVGElement.prototype.translateXSetter = SVGElement.prototype.translateYSetter = + SVGElement.prototype.rotationSetter = SVGElement.prototype.verticalAlignSetter = + SVGElement.prototype.scaleXSetter = SVGElement.prototype.scaleYSetter = function (value, key) { + this[key] = value; + this.doTransform = true; + }; - label.setState = function (state) { - label.state = curState = state; - if (!state) { - label.attr(normalState) - .css(normalStyle); - } else if (state === 2) { - label.attr(pressedState) - .css(pressedStyle); - } else if (state === 3) { - label.attr(disabledState) - .css(disabledStyle); - } - }; + // 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) { + this[key] = value; + // Only apply the stroke attribute if the stroke width is defined and larger than 0 + if (this.stroke && this['stroke-width']) { + this.strokeWidth = this['stroke-width']; + SVGElement.prototype.fillSetter.call(this, this.stroke, 'stroke', element); // use prototype as instance may be overridden + element.setAttribute('stroke-width', this['stroke-width']); + this.hasStroke = true; + } else if (key === 'stroke-width' && value === 0 && this.hasStroke) { + element.removeAttribute('stroke'); + this.hasStroke = false; + } + }; - return label - .on('click', function (e) { - if (curState !== 3) { - callback.call(label, e); - } - }) - .attr(normalState) - .css(extend({ cursor: 'default' }, normalStyle)); - }, - /** - * Make a straight line crisper by not spilling out to neighbour pixels - * @param {Array} points - * @param {Number} width - */ - 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] = mathRound(points[1]) - (width % 2 / 2); - } - if (points[2] === points[5]) { - points[2] = points[5] = mathRound(points[2]) + (width % 2 / 2); - } - return points; - }, + /** + * The default SVG renderer + */ + var SVGRenderer = function () { + this.init.apply(this, arguments); + }; + SVGRenderer.prototype = { + Element: SVGElement, + /** + * Initialize the SVGRenderer + * @param {Object} container + * @param {Number} width + * @param {Number} height + * @param {Boolean} forExport + */ + init: function (container, width, height, style, forExport, allowHTML) { + var renderer = this, + loc = location, + boxWrapper, + element, + desc; - /** - * Draw a path - * @param {Array} path An SVG path in array form - */ - path: function (path) { - var attr = { - fill: NONE - }; - if (isArray(path)) { - attr.d = path; - } else if (isObject(path)) { // attributes - extend(attr, path); - } - return this.createElement('path').attr(attr); - }, + boxWrapper = renderer.createElement('svg') + .attr({ + version: '1.1' + }) + .css(this.getStyle(style)); + element = boxWrapper.element; + container.appendChild(element); - /** - * Draw and return an SVG circle - * @param {Number} x The x position - * @param {Number} y The y position - * @param {Number} r The radius - */ - circle: function (x, y, r) { - var attr = isObject(x) ? - x : - { - x: x, - y: y, - r: r - }, - wrapper = this.createElement('circle'); + // For browsers other than IE, add the namespace attribute (#1978) + if (container.innerHTML.indexOf('xmlns') === -1) { + attr(element, 'xmlns', SVG_NS); + } - wrapper.xSetter = function (value) { - this.element.setAttribute('cx', value); - }; - wrapper.ySetter = function (value) { - this.element.setAttribute('cy', value); - }; - return wrapper.attr(attr); - }, + // object properties + renderer.isSVG = true; + renderer.box = element; + renderer.boxWrapper = boxWrapper; + renderer.alignedObjects = []; - /** - * 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 - */ - arc: function (x, y, r, innerR, start, end) { - var arc; + // Page url used for internal references. #24, #672, #1070 + renderer.url = (isFirefox || isWebKit) && doc.getElementsByTagName('base').length ? + loc.href + .replace(/#.*?$/, '') // remove the hash + .replace(/([\('\)])/g, '\\$1') // escape parantheses and quotes + .replace(/ /g, '%20') : // replace spaces (needed for Safari only) + ''; - if (isObject(x)) { - y = x.y; - r = x.r; - innerR = x.innerR; - start = x.start; - end = x.end; - x = x.x; - } + // Add description + desc = this.createElement('desc').add(); + desc.element.appendChild(doc.createTextNode('Created with ' + PRODUCT + ' ' + VERSION)); - // Arcs are defined as symbols for the ability to set - // attributes in attr and animate - arc = this.symbol('arc', x || 0, y || 0, r || 0, r || 0, { - innerR: innerR || 0, - start: start || 0, - end: end || 0 - }); - 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 - */ - rect: function (x, y, width, height, r, strokeWidth) { + renderer.defs = this.createElement('defs').add(); + renderer.allowHTML = allowHTML; + renderer.forExport = forExport; + renderer.gradients = {}; // Object where gradient SvgElements are stored + renderer.cache = {}; // Cache for numerical bounding boxes + renderer.cacheKeys = []; - r = isObject(x) ? x.r : r; + renderer.setSize(width, height, false); - var wrapper = this.createElement('rect'), - attribs = isObject(x) ? x : x === UNDEFINED ? {} : { - x: x, - y: y, - width: mathMax(width, 0), - height: mathMax(height, 0) - }; - if (strokeWidth !== UNDEFINED) { - attribs.strokeWidth = strokeWidth; - attribs = wrapper.crisp(attribs); - } - if (r) { - attribs.r = r; - } + // Issue 110 workaround: + // In Firefox, if a div is positioned by percentage, its pixel position may land + // between pixels. The container itself doesn't display this, but an SVG element + // 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 () { + css(container, { left: 0, top: 0 }); + rect = container.getBoundingClientRect(); + css(container, { + left: (mathCeil(rect.left) - rect.left) + PX, + top: (mathCeil(rect.top) - rect.top) + PX + }); + }; - wrapper.rSetter = function (value) { - attr(this.element, { - rx: value, - ry: value - }); - }; - - return wrapper.attr(attribs); - }, + // run the fix now + subPixelFix(); - /** - * Resize the box and re-align all aligned elements - * @param {Object} width - * @param {Object} height - * @param {Boolean} animate - * - */ - setSize: function (width, height, animate) { - var renderer = this, - alignedObjects = renderer.alignedObjects, - i = alignedObjects.length; + // run it on resize + addEvent(win, 'resize', subPixelFix); + } + }, - renderer.width = width; - renderer.height = height; + getStyle: function (style) { + this.style = extend({ + fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif', // default font + fontSize: '12px' + }, style); + return this.style; + }, - renderer.boxWrapper[pick(animate, true) ? 'animate' : 'attr']({ - width: width, - height: height - }); + /** + * Detect whether the renderer is hidden. This happens when one of the parent elements + * has display: none. #608. + */ + isHidden: function () { + return !this.boxWrapper.getBBox().width; + }, - while (i--) { - alignedObjects[i].align(); - } - }, + /** + * Destroys the renderer and its allocated members. + */ + destroy: function () { + var renderer = this, + rendererDefs = renderer.defs; + renderer.box = null; + renderer.boxWrapper = renderer.boxWrapper.destroy(); - /** - * 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. - */ - g: function (name) { - var elem = this.createElement('g'); - return defined(name) ? elem.attr({ 'class': PREFIX + name }) : elem; - }, + // Call destroy on all gradient elements + destroyObjectProperties(renderer.gradients || {}); + renderer.gradients = null; - /** - * Display an image - * @param {String} src - * @param {Number} x - * @param {Number} y - * @param {Number} width - * @param {Number} height - */ - image: function (src, x, y, width, height) { - var attribs = { - preserveAspectRatio: NONE - }, - elemWrapper; + // Defs are null in VMLRenderer + // Otherwise, destroy them here. + if (rendererDefs) { + renderer.defs = rendererDefs.destroy(); + } - // optional properties - if (arguments.length > 1) { - extend(attribs, { - x: x, - y: y, - width: width, - height: height - }); - } + // 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); + } - elemWrapper = this.createElement('image').attr(attribs); + renderer.alignedObjects = null; - // set the href in the xlink namespace - if (elemWrapper.element.setAttributeNS) { - elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink', - 'href', src); - } else { - // could be exporting in IE - // using href throws "not supported" in ie7 and under, requries regex shim to fix later - elemWrapper.element.setAttribute('hc-svg-href', src); - } - return elemWrapper; - }, + return null; + }, - /** - * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object. - * - * @param {Object} symbol - * @param {Object} x - * @param {Object} y - * @param {Object} radius - * @param {Object} options - */ - symbol: function (symbol, x, y, width, height, options) { + /** + * Create a wrapper for an SVG element + * @param {Object} nodeName + */ + createElement: function (nodeName) { + var wrapper = new this.Element(); + wrapper.init(this, nodeName); + return wrapper; + }, - var obj, + /** + * Dummy function for use in canvas renderer + */ + draw: function () {}, - // get the symbol definition function - symbolFn = this.symbols[symbol], + /** + * Get converted radial gradient attributes + */ + 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] + }; + }, - // check if there's a path defined for this symbol - path = symbolFn && symbolFn( - mathRound(x), - mathRound(y), - width, - height, - options - ), + /** + * Parse a simple HTML string into SVG tspans + * + * @param {Object} textNode The parent text SVG node + */ + buildText: function (wrapper) { + var textNode = wrapper.element, + renderer = this, + forExport = renderer.forExport, + textStr = pick(wrapper.textStr, '').toString(), + hasMarkup = textStr.indexOf('<') !== -1, + lines, + childNodes = textNode.childNodes, + styleRegex, + hrefRegex, + parentX = attr(textNode, 'x'), + textStyles = wrapper.styles, + width = wrapper.textWidth, + textLineHeight = textStyles && textStyles.lineHeight, + textShadow = textStyles && textStyles.textShadow, + ellipsis = textStyles && textStyles.textOverflow === 'ellipsis', + i = childNodes.length, + tempParent = width && !wrapper.added && this.box, + getLineHeight = function (tspan) { + return textLineHeight ? + pInt(textLineHeight) : + renderer.fontMetrics( + /(px|em)$/.test(tspan && tspan.style.fontSize) ? + tspan.style.fontSize : + ((textStyles && textStyles.fontSize) || renderer.style.fontSize || 12), + tspan + ).h; + }, + unescapeAngleBrackets = function (inputStr) { + return inputStr.replace(/&lt;/g, '<').replace(/&gt;/g, '>'); + }; - imageElement, - imageRegex = /^url\((.*?)\)$/, - imageSrc, - imageSize, - centerImage; + /// remove old text + while (i--) { + textNode.removeChild(childNodes[i]); + } - if (path) { + // Skip tspans, add text directly to text node. The forceTSpan is a hook + // used in text outline hack. + if (!hasMarkup && !textShadow && !ellipsis && textStr.indexOf(' ') === -1) { + textNode.appendChild(doc.createTextNode(unescapeAngleBrackets(textStr))); - obj = this.path(path); - // expando properties for use in animate and attr - extend(obj, { - symbolName: symbol, - x: x, - y: y, - width: width, - height: height - }); - if (options) { - extend(obj, options); - } + // Complex strings, add more logic + } else { + styleRegex = /<.*style="([^"]+)".*>/; + hrefRegex = /<.*href="(http[^"]+)".*>/; - // image symbols - } else if (imageRegex.test(symbol)) { + if (tempParent) { + tempParent.appendChild(textNode); // attach it to the DOM to read offset width + } - // On image load, set the size and position - centerImage = function (img, size) { - if (img.element) { // it may be destroyed in the meantime (#1390) - img.attr({ - width: size[0], - height: size[1] - }); + if (hasMarkup) { + lines = textStr + .replace(/<(b|strong)>/g, '<span style="font-weight:bold">') + .replace(/<(i|em)>/g, '<span style="font-style:italic">') + .replace(/<a/g, '<span') + .replace(/<\/(b|strong|i|em|a)>/g, '</span>') + .split(/<br.*?>/g); - if (!img.alignByTranslate) { // #185 - img.translate( - mathRound((width - size[0]) / 2), // #1378 - mathRound((height - size[1]) / 2) - ); - } - } - }; + } else { + lines = [textStr]; + } - imageSrc = symbol.match(imageRegex)[1]; - imageSize = symbolSizes[imageSrc] || (options && options.width && options.height && [options.width, options.height]); - // Ireate the image synchronously, add attribs async - obj = this.image(imageSrc) - .attr({ - x: x, - y: y - }); - obj.isImg = true; + // remove empty line at end + if (lines[lines.length - 1] === '') { + lines.pop(); + } - if (imageSize) { - centerImage(obj, imageSize); - } else { - // Initialize image to be 0 size so export will still function if there's no cached sizes. - obj.attr({ width: 0, height: 0 }); - // Create a dummy JavaScript image to get the width and height. Due to a bug in IE < 8, - // the created element must be assigned to a variable in order to load (#292). - imageElement = createElement('img', { - onload: function () { + // build the lines + each(lines, function (line, lineNo) { + var spans, spanNo = 0; - // Special case for SVGs on IE11, the width is not accessible until the image is - // part of the DOM (#2854). - if (this.width === 0) { - css(this, { - position: ABSOLUTE, - top: '-999em' - }); - document.body.appendChild(this); - } + line = line.replace(/<span/g, '|||<span').replace(/<\/span>/g, '</span>|||'); + spans = line.split('|||'); - // Center the image - centerImage(obj, symbolSizes[imageSrc] = [this.width, this.height]); + each(spans, function (span) { + if (span !== '' || spans.length === 1) { + var attributes = {}, + tspan = doc.createElementNS(SVG_NS, 'tspan'), + spanStyle; // #390 + if (styleRegex.test(span)) { + spanStyle = span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2'); + attr(tspan, 'style', spanStyle); + } + if (hrefRegex.test(span) && !forExport) { // Not for export - #1529 + attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"'); + css(tspan, { cursor: 'pointer' }); + } - // Clean up after #2854 workaround. - if (this.parentNode) { - this.parentNode.removeChild(this); - } - }, - src: imageSrc - }); - } - } + span = unescapeAngleBrackets(span.replace(/<(.|\n)*?>/g, '') || ' '); - return obj; - }, + // Nested tags aren't supported, and cause crash in Safari (#1596) + if (span !== ' ') { - /** - * An extendable collection of functions for defining symbol paths. - */ - symbols: { - 'circle': function (x, y, w, h) { - var cpw = 0.166 * w; - return [ - M, x + w / 2, y, - 'C', x + w + cpw, y, x + w + cpw, y + h, x + w / 2, y + h, - 'C', x - cpw, y + h, x - cpw, y, x + w / 2, y, - 'Z' - ]; - }, + // add the text node + tspan.appendChild(doc.createTextNode(span)); - 'square': function (x, y, w, h) { - return [ - M, x, y, - L, x + w, y, - x + w, y + h, - x, y + h, - 'Z' - ]; - }, + if (!spanNo) { // first span in a line, align it to the left + if (lineNo && parentX !== null) { + attributes.x = parentX; + } + } else { + attributes.dx = 0; // #16 + } - 'triangle': function (x, y, w, h) { - return [ - M, x + w / 2, y, - L, x + w, y + h, - x, y + h, - 'Z' - ]; - }, + // add attributes + attr(tspan, attributes); - 'triangle-down': function (x, y, w, h) { - return [ - M, x, y, - L, x + w, y, - x + w / 2, y + h, - 'Z' - ]; - }, - 'diamond': function (x, y, w, h) { - return [ - M, x + w / 2, y, - L, x + w, y + h / 2, - x + w / 2, y + h, - x, y + h / 2, - 'Z' - ]; - }, - 'arc': function (x, y, w, h, options) { - var start = options.start, - radius = options.r || w || h, - end = options.end - 0.001, // to prevent cos and sin of start and end from becoming equal on 360 arcs (related: #1561) - innerRadius = options.innerR, - open = options.open, - cosStart = mathCos(start), - sinStart = mathSin(start), - cosEnd = mathCos(end), - sinEnd = mathSin(end), - longArc = options.end - start < mathPI ? 0 : 1; + // Append it + textNode.appendChild(tspan); - return [ - M, - x + radius * cosStart, - y + radius * sinStart, - 'A', // arcTo - radius, // x radius - radius, // y radius - 0, // slanting - longArc, // long or short arc - 1, // clockwise - x + radius * cosEnd, - y + radius * sinEnd, - open ? M : L, - x + innerRadius * cosEnd, - y + innerRadius * sinEnd, - 'A', // arcTo - innerRadius, // x radius - innerRadius, // y radius - 0, // slanting - longArc, // long or short arc - 0, // clockwise - x + innerRadius * cosStart, - y + innerRadius * sinStart, + // first span on subsequent line, add the line height + if (!spanNo && lineNo) { - open ? '' : 'Z' // close - ]; - }, + // allow getting the right offset height in exporting in IE + if (!hasSVG && forExport) { + css(tspan, { display: 'block' }); + } - /** - * Callout shape used for default tooltips, also used for rounded rectangles in VML - */ - callout: function (x, y, w, h, options) { - var arrowLength = 6, - halfDistance = 6, - r = mathMin((options && options.r) || 0, w, h), - safeDistance = r + halfDistance, - anchorX = options && options.anchorX, - anchorY = options && options.anchorY, - path; + // Set the line height based on the font size of either + // the text element or the tspan element + attr( + tspan, + 'dy', + getLineHeight(tspan) + ); + } - path = [ - 'M', x + r, y, - 'L', x + w - r, y, // top side - 'C', x + w, y, x + w, y, x + w, y + r, // top-right corner - '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 - ]; - - 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 - ); - } 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, - x + r, y + h - ); - } else if (anchorY && anchorY < 0 && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace top - path.splice(3, 3, - 'L', anchorX - halfDistance, y, - anchorX, y - arrowLength, - anchorX + halfDistance, y, - w - r, y - ); - } - return path; - } - }, + /*if (width) { + renderer.breakText(wrapper, width); + }*/ - /** - * Define a clipping rectangle - * @param {String} id - * @param {Number} x - * @param {Number} y - * @param {Number} width - * @param {Number} height - */ - clipRect: function (x, y, width, height) { - var wrapper, - id = PREFIX + idCounter++, + // Check width and apply soft breaks or ellipsis + if (width) { + var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273 + hasWhiteSpace = spans.length > 1 || lineNo || (words.length > 1 && textStyles.whiteSpace !== 'nowrap'), + tooLong, + wasTooLong, + actualWidth, + rest = [], + dy = getLineHeight(tspan), + softLineNo = 1, + rotation = wrapper.rotation, + wordStr = span, // for ellipsis + cursor = wordStr.length, // binary search cursor + bBox; - clipPath = this.createElement('clipPath').attr({ - id: id - }).add(this.defs); + while ((hasWhiteSpace || ellipsis) && (words.length || rest.length)) { + wrapper.rotation = 0; // discard rotation when computing box + bBox = wrapper.getBBox(true); + actualWidth = bBox.width; - wrapper = this.rect(x, y, width, height, 0).add(clipPath); - wrapper.id = id; - wrapper.clipPath = clipPath; - wrapper.count = 0; + // Old IE cannot measure the actualWidth for SVG elements (#2314) + if (!hasSVG && renderer.forExport) { + actualWidth = renderer.measureSpanWidth(tspan.firstChild.data, wrapper.styles); + } - return wrapper; - }, + tooLong = actualWidth > width; + // For ellipsis, do a binary search for the correct string length + if (wasTooLong === undefined) { + wasTooLong = tooLong; // First time + } + if (ellipsis && wasTooLong) { + cursor /= 2; - + if (wordStr === '' || (!tooLong && cursor < 0.5)) { + words = []; // All ok, break out + } else { + if (tooLong) { + wasTooLong = true; + } + wordStr = span.substring(0, wordStr.length + (tooLong ? -1 : 1) * mathCeil(cursor)); + words = [wordStr + (width > 3 ? '\u2026' : '')]; + tspan.removeChild(tspan.firstChild); + } + // Looping down, this is the first word sequence that is not too long, + // so we can move on to build the next line. + } else if (!tooLong || words.length === 1) { + words = rest; + rest = []; - /** - * Add text to the SVG object - * @param {String} str - * @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) { + if (words.length) { + softLineNo++; - // declare variables - var renderer = this, - fakeSVG = useCanVG || (!hasSVG && renderer.forExport), - wrapper, - attr = {}; + tspan = doc.createElementNS(SVG_NS, 'tspan'); + attr(tspan, { + dy: dy, + x: parentX + }); + if (spanStyle) { // #390 + attr(tspan, 'style', spanStyle); + } + textNode.appendChild(tspan); + } + if (actualWidth > width) { // a single word is pressing it out + width = actualWidth; + } + } else { // append to existing line tspan + tspan.removeChild(tspan.firstChild); + rest.unshift(words.pop()); + } + if (words.length) { + tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-'))); + } + } + if (wasTooLong) { + wrapper.attr('title', wrapper.textStr); + } + wrapper.rotation = rotation; + } - if (useHTML && (renderer.allowHTML || !renderer.forExport)) { - return renderer.html(str, x, y); - } + spanNo++; + } + } + }); + }); + if (tempParent) { + tempParent.removeChild(textNode); // attach it to the DOM to read offset width + } - attr.x = Math.round(x || 0); // X is always needed for line-wrap logic - if (y) { - attr.y = Math.round(y); - } - if (str || str === 0) { - attr.text = str; - } + // Apply the text shadow + if (textShadow && wrapper.applyTextShadow) { + wrapper.applyTextShadow(textShadow); + } + } + }, - wrapper = renderer.createElement('text') - .attr(attr); - // Prevent wrapping from creating false offsetWidths in export in legacy IE (#1079, #1063) - if (fakeSVG) { - wrapper.css({ - position: ABSOLUTE - }); - } - if (!useHTML) { - wrapper.xSetter = function (value, key, element) { - var tspans = element.getElementsByTagName('tspan'), - tspan, - parentVal = element.getAttribute(key), - i; - for (i = 0; i < tspans.length; i++) { - tspan = tspans[i]; - // If the x values are equal, the tspan represents a linebreak - if (tspan.getAttribute(key) === parentVal) { - tspan.setAttribute(key, value); - } - } - element.setAttribute(key, value); - }; - } - - return wrapper; - }, + /* + breakText: function (wrapper, width) { + var bBox = wrapper.getBBox(), + node = wrapper.element, + textLength = node.textContent.length, + pos = mathRound(width * textLength / bBox.width), // try this position first, based on average character width + increment = 0, + finalPos; - /** - * Utility to return the baseline offset and total line height from the font size - */ - fontMetrics: function (fontSize, elem) { - var lineHeight, - baseline, - style; + if (bBox.width > width) { + while (finalPos === undefined) { + textLength = node.getSubStringLength(0, pos); - fontSize = fontSize || this.style.fontSize; - if (!fontSize && elem && win.getComputedStyle) { - elem = elem.element || elem; // SVGElement - style = win.getComputedStyle(elem, ""); - fontSize = style && style.fontSize; // #4309, the style doesn't exist inside a hidden iframe in Firefox - } - fontSize = /px/.test(fontSize) ? pInt(fontSize) : /em/.test(fontSize) ? parseFloat(fontSize) * 12 : 12; + if (textLength <= width) { + if (increment === -1) { + finalPos = pos; + } else { + increment = 1; + } + } else { + if (increment === 1) { + finalPos = pos - 1; + } else { + increment = -1; + } + } + pos += increment; + } + } + console.log(finalPos, node.getSubStringLength(0, finalPos)) + }, + */ - // 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 : mathRound(fontSize * 1.2); - baseline = mathRound(lineHeight * 0.8); + /** + * Returns white for dark colors and black for bright colors + */ + getContrast: function (color) { + color = Color(color).rgba; + return color[0] + color[1] + color[2] > 384 ? '#000000' : '#FFFFFF'; + }, - return { - h: lineHeight, - b: baseline, - f: fontSize - }; - }, + /** + * 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 + */ + 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, + stateOptions, + stateStyle, + normalStyle, + hoverStyle, + pressedStyle, + disabledStyle, + verticalGradient = { x1: 0, y1: 0, x2: 0, y2: 1 }; - /** - * Correct X and Y positioning of a label for rotation (#1764) - */ - rotCorr: function (baseline, rotation, alterY) { - var y = baseline; - if (rotation && alterY) { - y = mathMax(y * mathCos(rotation * deg2rad), 4); - } - return { - x: (-baseline / 3) * mathSin(rotation * deg2rad), - y: y - }; - }, + // Normal state - prepare the attributes + normalState = merge({ + 'stroke-width': 1, + stroke: '#CCCCCC', + fill: { + linearGradient: verticalGradient, + stops: [ + [0, '#FEFEFE'], + [1, '#F6F6F6'] + ] + }, + r: 2, + padding: 5, + style: { + color: 'black' + } + }, normalState); + normalStyle = normalState.style; + delete normalState.style; - /** - * Add a label, a text item that can hold a colored or gradient background - * as well as a border and shadow. - * @param {string} str - * @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 - * coordinates it should be pinned to - * @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) { + // Hover state + hoverState = merge(normalState, { + stroke: '#68A', + fill: { + linearGradient: verticalGradient, + stops: [ + [0, '#FFF'], + [1, '#ACF'] + ] + } + }, hoverState); + hoverStyle = hoverState.style; + delete hoverState.style; - var renderer = this, - wrapper = renderer.g(className), - text = renderer.text('', 0, 0, useHTML) - .attr({ - zIndex: 1 - }), - //.add(wrapper), - box, - bBox, - alignFactor = 0, - padding = 3, - paddingLeft = 0, - width, - height, - wrapperX, - wrapperY, - crispAdjust = 0, - deferredAttr = {}, - baselineOffset, - needsBox; + // Pressed state + pressedState = merge(normalState, { + stroke: '#68A', + fill: { + linearGradient: verticalGradient, + stops: [ + [0, '#9BD'], + [1, '#CDF'] + ] + } + }, pressedState); + pressedStyle = pressedState.style; + delete pressedState.style; - /** - * This function runs after the label is added to the DOM (when the bounding box is - * available), and after the text of the label is updated to detect the new bounding - * box and reflect it in the border box. - */ - function updateBoxSize() { - var boxX, - boxY, - style = text.element.style; + // Disabled state + disabledState = merge(normalState, { + style: { + color: '#CCC' + } + }, disabledState); + disabledStyle = disabledState.style; + delete disabledState.style; - bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) && defined(text.textStr) && - text.getBBox(); //#3295 && 3514 box failure when string equals 0 - wrapper.width = (width || bBox.width || 0) + 2 * padding + paddingLeft; - wrapper.height = (height || bBox.height || 0) + 2 * padding; + // Add the events. IE9 and IE10 need mouseover and mouseout to funciton (#667). + addEvent(label.element, isMS ? 'mouseover' : 'mouseenter', function () { + if (curState !== 3) { + label.attr(hoverState) + .css(hoverStyle); + } + }); + addEvent(label.element, isMS ? 'mouseout' : 'mouseleave', function () { + if (curState !== 3) { + stateOptions = [normalState, hoverState, pressedState][curState]; + stateStyle = [normalStyle, hoverStyle, pressedStyle][curState]; + label.attr(stateOptions) + .css(stateStyle); + } + }); - // update the label-scoped y offset - baselineOffset = padding + renderer.fontMetrics(style && style.fontSize, text).b; + label.setState = function (state) { + label.state = curState = state; + if (!state) { + label.attr(normalState) + .css(normalStyle); + } else if (state === 2) { + label.attr(pressedState) + .css(pressedStyle); + } else if (state === 3) { + label.attr(disabledState) + .css(disabledStyle); + } + }; - - if (needsBox) { + return label + .on('click', function (e) { + if (curState !== 3) { + callback.call(label, e); + } + }) + .attr(normalState) + .css(extend({ cursor: 'default' }, normalStyle)); + }, - // create the border box if it is not already present - if (!box) { - boxX = mathRound(-alignFactor * padding) + crispAdjust; - boxY = (baseline ? -baselineOffset : 0) + crispAdjust; + /** + * Make a straight line crisper by not spilling out to neighbour pixels + * @param {Array} points + * @param {Number} width + */ + 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] = mathRound(points[1]) - (width % 2 / 2); + } + if (points[2] === points[5]) { + points[2] = points[5] = mathRound(points[2]) + (width % 2 / 2); + } + return points; + }, - wrapper.box = box = shape ? - renderer.symbol(shape, boxX, boxY, wrapper.width, wrapper.height, deferredAttr) : - renderer.rect(boxX, boxY, wrapper.width, wrapper.height, 0, deferredAttr[STROKE_WIDTH]); - if (!box.isImg) { // #4324, fill "none" causes it to be ignored by mouse events in IE - box.attr('fill', NONE); - } - box.add(wrapper); - } + /** + * Draw a path + * @param {Array} path An SVG path in array form + */ + path: function (path) { + var attr = { + fill: NONE + }; + if (isArray(path)) { + attr.d = path; + } else if (isObject(path)) { // attributes + extend(attr, path); + } + return this.createElement('path').attr(attr); + }, - // apply the box attributes - if (!box.isImg) { // #1630 - box.attr(extend({ - width: mathRound(wrapper.width), - height: mathRound(wrapper.height) - }, deferredAttr)); - } - deferredAttr = null; - } - } + /** + * Draw and return an SVG circle + * @param {Number} x The x position + * @param {Number} y The y position + * @param {Number} r The radius + */ + circle: function (x, y, r) { + var attr = isObject(x) ? x : { x: x, y: y, r: r }, + wrapper = this.createElement('circle'); - /** - * This function runs after setting text or padding, but only if padding is changed - */ - function updateTextPadding() { - var styles = wrapper.styles, - textAlign = styles && styles.textAlign, - x = paddingLeft + padding * (1 - alignFactor), - y; + wrapper.xSetter = function (value) { + this.element.setAttribute('cx', value); + }; + wrapper.ySetter = function (value) { + this.element.setAttribute('cy', value); + }; + return wrapper.attr(attr); + }, - // determin y based on the baseline - y = baseline ? 0 : baselineOffset; + /** + * 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 + */ + arc: function (x, y, r, innerR, start, end) { + var arc; - // compensate for alignment - if (defined(width) && bBox && (textAlign === 'center' || textAlign === 'right')) { - x += { center: 0.5, right: 1 }[textAlign] * (width - bBox.width); - } + if (isObject(x)) { + y = x.y; + r = x.r; + innerR = x.innerR; + start = x.start; + end = x.end; + x = x.x; + } - // update if anything changed - if (x !== text.x || y !== text.y) { - text.attr('x', x); - if (y !== UNDEFINED) { - text.attr('y', y); - } - } + // Arcs are defined as symbols for the ability to set + // attributes in attr and animate + arc = this.symbol('arc', x || 0, y || 0, r || 0, r || 0, { + innerR: innerR || 0, + start: start || 0, + end: end || 0 + }); + arc.r = r; // #959 + return arc; + }, - // record current values - text.x = x; - text.y = y; - } + /** + * 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 + */ + rect: function (x, y, width, height, r, strokeWidth) { - /** - * Set a box attribute, or defer it if the box is not yet created - * @param {Object} key - * @param {Object} value - */ - function boxAttr(key, value) { - if (box) { - box.attr(key, value); - } else { - deferredAttr[key] = value; - } - } + r = isObject(x) ? x.r : r; - /** - * After the text element is added, get the desired size of the border box - * and add it before the text in the DOM. - */ - wrapper.onAdd = function () { - text.add(wrapper); - wrapper.attr({ - text: (str || str === 0) ? str : '', // alignment is available now // #3295: 0 not rendered if given as a value - x: x, - y: y - }); + var wrapper = this.createElement('rect'), + attribs = isObject(x) ? x : x === UNDEFINED ? {} : { + x: x, + y: y, + width: mathMax(width, 0), + height: mathMax(height, 0) + }; - if (box && defined(anchorX)) { - wrapper.attr({ - anchorX: anchorX, - anchorY: anchorY - }); - } - }; + if (strokeWidth !== UNDEFINED) { + attribs.strokeWidth = strokeWidth; + attribs = wrapper.crisp(attribs); + } - /* - * Add specific attribute setters. - */ + if (r) { + attribs.r = r; + } - // only change local variables - wrapper.widthSetter = function (value) { - width = value; - }; - wrapper.heightSetter = function (value) { - height = value; - }; - wrapper.paddingSetter = function (value) { - if (defined(value) && value !== padding) { - padding = wrapper.padding = value; - updateTextPadding(); - } - }; - wrapper.paddingLeftSetter = function (value) { - if (defined(value) && value !== paddingLeft) { - paddingLeft = value; - updateTextPadding(); - } - }; + wrapper.rSetter = function (value) { + attr(this.element, { + rx: value, + ry: value + }); + }; + return wrapper.attr(attribs); + }, - // change local variable and prevent setting attribute on the group - wrapper.alignSetter = function (value) { - alignFactor = { left: 0, center: 0.5, right: 1 }[value]; - }; + /** + * Resize the box and re-align all aligned elements + * @param {Object} width + * @param {Object} height + * @param {Boolean} animate + * + */ + setSize: function (width, height, animate) { + var renderer = this, + alignedObjects = renderer.alignedObjects, + i = alignedObjects.length; - // apply these to the box and the text alike - wrapper.textSetter = function (value) { - if (value !== UNDEFINED) { - text.textSetter(value); - } - updateBoxSize(); - updateTextPadding(); - }; + renderer.width = width; + renderer.height = height; - // apply these to the box but not to the text - wrapper['stroke-widthSetter'] = function (value, key) { - if (value) { - needsBox = true; - } - crispAdjust = value % 2 / 2; - boxAttr(key, value); - }; - wrapper.strokeSetter = wrapper.fillSetter = wrapper.rSetter = function (value, key) { - if (key === 'fill' && value) { - needsBox = true; - } - boxAttr(key, value); - }; - wrapper.anchorXSetter = function (value, key) { - anchorX = value; - boxAttr(key, mathRound(value) - crispAdjust - wrapperX); - }; - wrapper.anchorYSetter = function (value, key) { - anchorY = value; - boxAttr(key, value - wrapperY); - }; + renderer.boxWrapper[pick(animate, true) ? 'animate' : 'attr']({ + width: width, + height: height + }); - // rename attributes - wrapper.xSetter = function (value) { - wrapper.x = value; // for animation getter - if (alignFactor) { - value -= alignFactor * ((width || bBox.width) + padding); - } - wrapperX = mathRound(value); - wrapper.attr('translateX', wrapperX); - }; - wrapper.ySetter = function (value) { - wrapperY = wrapper.y = mathRound(value); - wrapper.attr('translateY', wrapperY); - }; + while (i--) { + alignedObjects[i].align(); + } + }, - // 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 - */ - css: function (styles) { - if (styles) { - var textStyles = {}; - styles = merge(styles); // create a copy to avoid altering the original object (#537) - each(wrapper.textProps, function (prop) { - if (styles[prop] !== UNDEFINED) { - textStyles[prop] = styles[prop]; - delete styles[prop]; - } - }); - text.css(textStyles); - } - return baseCss.call(wrapper, styles); - }, - /** - * Return the bounding box of the box, not the group - */ - getBBox: function () { - return { - width: bBox.width + 2 * padding, - height: bBox.height + 2 * padding, - x: bBox.x - padding, - y: bBox.y - padding - }; - }, - /** - * Apply the shadow to the box - */ - shadow: function (b) { - if (box) { - box.shadow(b); - } - return wrapper; - }, - /** - * Destroy and release memory. - */ - destroy: function () { + /** + * 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. + */ + g: function (name) { + var elem = this.createElement('g'); + return defined(name) ? elem.attr({ 'class': PREFIX + name }) : elem; + }, - // Added by button implementation - removeEvent(wrapper.element, 'mouseenter'); - removeEvent(wrapper.element, 'mouseleave'); + /** + * Display an image + * @param {String} src + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + image: function (src, x, y, width, height) { + var attribs = { + preserveAspectRatio: NONE + }, + elemWrapper; - if (text) { - text = text.destroy(); - } - if (box) { - box = box.destroy(); - } - // Call base implementation to destroy the rest - SVGElement.prototype.destroy.call(wrapper); + // optional properties + if (arguments.length > 1) { + extend(attribs, { + x: x, + y: y, + width: width, + height: height + }); + } - // Release local pointers (#1298) - wrapper = renderer = updateBoxSize = updateTextPadding = boxAttr = null; - } - }); - } -}; // end SVGRenderer + elemWrapper = this.createElement('image').attr(attribs); + // set the href in the xlink namespace + if (elemWrapper.element.setAttributeNS) { + elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink', + 'href', src); + } else { + // could be exporting in IE + // using href throws "not supported" in ie7 and under, requries regex shim to fix later + elemWrapper.element.setAttribute('hc-svg-href', src); + } + return elemWrapper; + }, -// general renderer -Renderer = SVGRenderer; -// extend SvgElement for useHTML option -extend(SVGElement.prototype, { - /** - * Apply CSS to HTML elements. This is used in text within SVG rendering and - * by the VML renderer - */ - htmlCss: function (styles) { - var wrapper = this, - element = wrapper.element, - textWidth = styles && element.tagName === 'SPAN' && styles.width; + /** + * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object. + * + * @param {Object} symbol + * @param {Object} x + * @param {Object} y + * @param {Object} radius + * @param {Object} options + */ + symbol: function (symbol, x, y, width, height, options) { - if (textWidth) { - delete styles.width; - wrapper.textWidth = textWidth; - wrapper.updateTransform(); - } - if (styles && styles.textOverflow === 'ellipsis') { - styles.whiteSpace = 'nowrap'; - styles.overflow = 'hidden'; - } - wrapper.styles = extend(wrapper.styles, styles); - css(wrapper.element, styles); + var obj, - return wrapper; - }, + // get the symbol definition function + symbolFn = this.symbols[symbol], - /** - * VML and useHTML method for calculating the bounding box based on offsets - * @param {Boolean} refresh Whether to force a fresh value from the DOM or to - * use the cached value - * - * @return {Object} A hash containing values for x, y, width and height - */ + // check if there's a path defined for this symbol + path = symbolFn && symbolFn( + mathRound(x), + mathRound(y), + width, + height, + options + ), - htmlGetBBox: function () { - var wrapper = this, - element = wrapper.element; + imageRegex = /^url\((.*?)\)$/, + imageSrc, + imageSize, + centerImage; - // faking getBBox in exported SVG in legacy IE - // faking getBBox in exported SVG in legacy IE (is this a duplicate of the fix for #1079?) - if (element.nodeName === 'text') { - element.style.position = ABSOLUTE; - } + if (path) { - return { - x: element.offsetLeft, - y: element.offsetTop, - width: element.offsetWidth, - height: element.offsetHeight - }; - }, + obj = this.path(path); + // expando properties for use in animate and attr + extend(obj, { + symbolName: symbol, + x: x, + y: y, + width: width, + height: height + }); + if (options) { + extend(obj, options); + } - /** - * VML override private method to update elements based on internal - * properties based on SVG transform - */ - htmlUpdateTransform: function () { - // aligning non added elements is expensive - if (!this.added) { - this.alignOnAdd = true; - return; - } - var wrapper = this, - renderer = wrapper.renderer, - elem = wrapper.element, - translateX = wrapper.translateX || 0, - translateY = wrapper.translateY || 0, - x = wrapper.x || 0, - y = wrapper.y || 0, - align = wrapper.textAlign || 'left', - alignCorrection = { left: 0, center: 0.5, right: 1 }[align], - shadows = wrapper.shadows, - styles = wrapper.styles; + // image symbols + } else if (imageRegex.test(symbol)) { - // apply translate - css(elem, { - marginLeft: translateX, - marginTop: translateY - }); - if (shadows) { // used in labels/tooltip - each(shadows, function (shadow) { - css(shadow, { - marginLeft: translateX + 1, - marginTop: translateY + 1 - }); - }); - } + // On image load, set the size and position + centerImage = function (img, size) { + if (img.element) { // it may be destroyed in the meantime (#1390) + img.attr({ + width: size[0], + height: size[1] + }); - // apply inversion - if (wrapper.inverted) { // wrapper is a group - each(elem.childNodes, function (child) { - renderer.invertChild(child, elem); - }); - } + if (!img.alignByTranslate) { // #185 + img.translate( + mathRound((width - size[0]) / 2), // #1378 + mathRound((height - size[1]) / 2) + ); + } + } + }; - if (elem.tagName === 'SPAN') { + imageSrc = symbol.match(imageRegex)[1]; + imageSize = symbolSizes[imageSrc] || (options && options.width && options.height && [options.width, options.height]); - var width, - rotation = wrapper.rotation, - baseline, - textWidth = pInt(wrapper.textWidth), - currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth, wrapper.textAlign].join(','); + // Ireate the image synchronously, add attribs async + obj = this.image(imageSrc) + .attr({ + x: x, + y: y + }); + obj.isImg = true; - if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed + if (imageSize) { + centerImage(obj, imageSize); + } else { + // Initialize image to be 0 size so export will still function if there's no cached sizes. + obj.attr({ width: 0, height: 0 }); + // Create a dummy JavaScript image to get the width and height. Due to a bug in IE < 8, + // the created element must be assigned to a variable in order to load (#292). + createElement('img', { + onload: function () { - baseline = renderer.fontMetrics(elem.style.fontSize).b; + // Special case for SVGs on IE11, the width is not accessible until the image is + // part of the DOM (#2854). + if (this.width === 0) { + css(this, { + position: ABSOLUTE, + top: '-999em' + }); + document.body.appendChild(this); + } - // Renderer specific handling of span rotation - if (defined(rotation)) { - wrapper.setSpanRotation(rotation, alignCorrection, baseline); - } + // Center the image + centerImage(obj, symbolSizes[imageSrc] = [this.width, this.height]); - width = pick(wrapper.elemWidth, elem.offsetWidth); + // Clean up after #2854 workaround. + if (this.parentNode) { + this.parentNode.removeChild(this); + } + }, + src: imageSrc + }); + } + } - // Update textWidth - if (width > textWidth && /[ \-]/.test(elem.textContent || elem.innerText)) { // #983, #1254 - css(elem, { - width: textWidth + PX, - display: 'block', - whiteSpace: (styles && styles.whiteSpace) || 'normal' // #3331 - }); - width = textWidth; - } + return obj; + }, - wrapper.getSpanCorrection(width, baseline, alignCorrection, rotation, align); - } + /** + * An extendable collection of functions for defining symbol paths. + */ + symbols: { + 'circle': function (x, y, w, h) { + var cpw = 0.166 * w; + return [ + M, x + w / 2, y, + 'C', x + w + cpw, y, x + w + cpw, y + h, x + w / 2, y + h, + 'C', x - cpw, y + h, x - cpw, y, x + w / 2, y, + 'Z' + ]; + }, - // apply position with correction - css(elem, { - left: (x + (wrapper.xCorr || 0)) + PX, - top: (y + (wrapper.yCorr || 0)) + PX - }); + 'square': function (x, y, w, h) { + return [ + M, x, y, + L, x + w, y, + x + w, y + h, + x, y + h, + 'Z' + ]; + }, - // force reflow in webkit to apply the left and top on useHTML element (#1249) - if (isWebKit) { - baseline = elem.offsetHeight; // assigned to baseline for JSLint purpose - } + 'triangle': function (x, y, w, h) { + return [ + M, x + w / 2, y, + L, x + w, y + h, + x, y + h, + 'Z' + ]; + }, - // record current text transform - wrapper.cTT = currentTextTransform; - } - }, + 'triangle-down': function (x, y, w, h) { + return [ + M, x, y, + L, x + w, y, + x + w / 2, y + h, + 'Z' + ]; + }, + 'diamond': function (x, y, w, h) { + return [ + M, x + w / 2, y, + L, x + w, y + h / 2, + x + w / 2, y + h, + x, y + h / 2, + 'Z' + ]; + }, + 'arc': function (x, y, w, h, options) { + var start = options.start, + radius = options.r || w || h, + end = options.end - 0.001, // to prevent cos and sin of start and end from becoming equal on 360 arcs (related: #1561) + innerRadius = options.innerR, + open = options.open, + cosStart = mathCos(start), + sinStart = mathSin(start), + cosEnd = mathCos(end), + sinEnd = mathSin(end), + longArc = options.end - start < mathPI ? 0 : 1; - /** - * Set the rotation of an individual HTML span - */ - setSpanRotation: function (rotation, alignCorrection, baseline) { - var rotationStyle = {}, - cssTransformKey = isMS ? '-ms-transform' : isWebKit ? '-webkit-transform' : isFirefox ? 'MozTransform' : isOpera ? '-o-transform' : ''; + return [ + M, + x + radius * cosStart, + y + radius * sinStart, + 'A', // arcTo + radius, // x radius + radius, // y radius + 0, // slanting + longArc, // long or short arc + 1, // clockwise + x + radius * cosEnd, + y + radius * sinEnd, + open ? M : L, + x + innerRadius * cosEnd, + y + innerRadius * sinEnd, + 'A', // arcTo + innerRadius, // x radius + innerRadius, // y radius + 0, // slanting + longArc, // long or short arc + 0, // clockwise + x + innerRadius * cosStart, + y + innerRadius * sinStart, - rotationStyle[cssTransformKey] = rotationStyle.transform = 'rotate(' + rotation + 'deg)'; - rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] = rotationStyle.transformOrigin = (alignCorrection * 100) + '% ' + baseline + 'px'; - css(this.element, rotationStyle); - }, + open ? '' : 'Z' // close + ]; + }, - /** - * Get the correction in X and Y positioning as the element is rotated. - */ - getSpanCorrection: function (width, baseline, alignCorrection) { - this.xCorr = -width * alignCorrection; - this.yCorr = -baseline; - } -}); + /** + * Callout shape used for default tooltips, also used for rounded rectangles in VML + */ + callout: function (x, y, w, h, options) { + var arrowLength = 6, + halfDistance = 6, + r = mathMin((options && options.r) || 0, w, h), + safeDistance = r + halfDistance, + anchorX = options && options.anchorX, + anchorY = options && options.anchorY, + path; -// Extend SvgRenderer for useHTML option. -extend(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 - * @param {Number} x - * @param {Number} y - */ - html: function (str, x, y) { - var wrapper = this.createElement('span'), - element = wrapper.element, - renderer = wrapper.renderer; + path = [ + 'M', x + r, y, + 'L', x + w - r, y, // top side + 'C', x + w, y, x + w, y, x + w, y + r, // top-right corner + '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 + ]; - // Text setter - wrapper.textSetter = function (value) { - if (value !== element.innerHTML) { - delete this.bBox; - } - element.innerHTML = this.textStr = value; - wrapper.htmlUpdateTransform(); - }; + 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 + ); + } 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, + x + r, y + h + ); + } else if (anchorY && anchorY < 0 && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace top + path.splice(3, 3, + 'L', anchorX - halfDistance, y, + anchorX, y - arrowLength, + anchorX + halfDistance, y, + w - r, y + ); + } + return path; + } + }, - // Various setters which rely on update transform - wrapper.xSetter = wrapper.ySetter = wrapper.alignSetter = wrapper.rotationSetter = function (value, key) { - if (key === 'align') { - key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML. - } - wrapper[key] = value; - wrapper.htmlUpdateTransform(); - }; + /** + * Define a clipping rectangle + * @param {String} id + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + clipRect: function (x, y, width, height) { + var wrapper, + id = PREFIX + idCounter++, - // Set the default attributes - wrapper.attr({ - text: str, - x: mathRound(x), - y: mathRound(y) - }) - .css({ - position: ABSOLUTE, - fontFamily: this.style.fontFamily, - fontSize: this.style.fontSize - }); + clipPath = this.createElement('clipPath').attr({ + id: id + }).add(this.defs); - // Keep the whiteSpace style outside the wrapper.styles collection - element.style.whiteSpace = 'nowrap'; + wrapper = this.rect(x, y, width, height, 0).add(clipPath); + wrapper.id = id; + wrapper.clipPath = clipPath; + wrapper.count = 0; - // Use the HTML specific .css method - wrapper.css = wrapper.htmlCss; + return wrapper; + }, - // This is specific for HTML within SVG - if (renderer.isSVG) { - wrapper.add = function (svgGroupWrapper) { - var htmlGroup, - container = renderer.box.parentNode, - parentGroup, - parents = []; - this.parentGroup = svgGroupWrapper; - // Create a mock group to hold the HTML elements - if (svgGroupWrapper) { - htmlGroup = svgGroupWrapper.div; - if (!htmlGroup) { - // Read the parent chain into an array and read from top down - parentGroup = svgGroupWrapper; - while (parentGroup) { + /** + * Add text to the SVG object + * @param {String} str + * @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) { - parents.push(parentGroup); + // declare variables + var renderer = this, + fakeSVG = useCanVG || (!hasSVG && renderer.forExport), + wrapper, + attr = {}; - // Move up to the next parent group - parentGroup = parentGroup.parentGroup; - } + if (useHTML && (renderer.allowHTML || !renderer.forExport)) { + return renderer.html(str, x, y); + } - // Ensure dynamically updating position when any parent is translated - each(parents.reverse(), function (parentGroup) { - var htmlGroupStyle, - cls = attr(parentGroup.element, 'class'); + attr.x = Math.round(x || 0); // X is always needed for line-wrap logic + if (y) { + attr.y = Math.round(y); + } + if (str || str === 0) { + attr.text = str; + } - if (cls) { - cls = { className: cls }; - } // else null + wrapper = renderer.createElement('text') + .attr(attr); - // Create a HTML div and append it to the parent div to emulate - // the SVG group structure - htmlGroup = parentGroup.div = parentGroup.div || createElement(DIV, cls, { - position: ABSOLUTE, - left: (parentGroup.translateX || 0) + PX, - top: (parentGroup.translateY || 0) + PX - }, htmlGroup || container); // the top group is appended to container + // Prevent wrapping from creating false offsetWidths in export in legacy IE (#1079, #1063) + if (fakeSVG) { + wrapper.css({ + position: ABSOLUTE + }); + } - // Shortcut - htmlGroupStyle = htmlGroup.style; + if (!useHTML) { + wrapper.xSetter = function (value, key, element) { + var tspans = element.getElementsByTagName('tspan'), + tspan, + parentVal = element.getAttribute(key), + i; + for (i = 0; i < tspans.length; i++) { + tspan = tspans[i]; + // If the x values are equal, the tspan represents a linebreak + if (tspan.getAttribute(key) === parentVal) { + tspan.setAttribute(key, value); + } + } + element.setAttribute(key, value); + }; + } - // Set listeners to update the HTML div's position whenever the SVG group - // position is changed - extend(parentGroup, { - translateXSetter: function (value, key) { - htmlGroupStyle.left = value + PX; - parentGroup[key] = value; - parentGroup.doTransform = true; - }, - translateYSetter: function (value, key) { - htmlGroupStyle.top = value + PX; - parentGroup[key] = value; - parentGroup.doTransform = true; - } - }); + return wrapper; + }, - // These properties are set as attributes on the SVG group, and as - // identical CSS properties on the div. (#3542) - each(['opacity', 'visibility'], function (prop) { - wrap(parentGroup, prop + 'Setter', function (proceed, value, key, elem) { - proceed.call(this, value, key, elem); - htmlGroupStyle[key] = value; - }); - }); - }); + /** + * Utility to return the baseline offset and total line height from the font size + */ + fontMetrics: function (fontSize, elem) { + var lineHeight, + baseline, + style; - } - } else { - htmlGroup = container; - } + fontSize = fontSize || this.style.fontSize; + if (!fontSize && elem && win.getComputedStyle) { + elem = elem.element || elem; // SVGElement + style = win.getComputedStyle(elem, ''); + fontSize = style && style.fontSize; // #4309, the style doesn't exist inside a hidden iframe in Firefox + } + fontSize = /px/.test(fontSize) ? pInt(fontSize) : /em/.test(fontSize) ? parseFloat(fontSize) * 12 : 12; - htmlGroup.appendChild(element); + // 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 : mathRound(fontSize * 1.2); + baseline = mathRound(lineHeight * 0.8); - // Shared with VML: - wrapper.added = true; - if (wrapper.alignOnAdd) { - wrapper.htmlUpdateTransform(); - } + return { + h: lineHeight, + b: baseline, + f: fontSize + }; + }, - return wrapper; - }; - } - return wrapper; - } -}); + /** + * Correct X and Y positioning of a label for rotation (#1764) + */ + rotCorr: function (baseline, rotation, alterY) { + var y = baseline; + if (rotation && alterY) { + y = mathMax(y * mathCos(rotation * deg2rad), 4); + } + return { + x: (-baseline / 3) * mathSin(rotation * deg2rad), + y: y + }; + }, -/* **************************************************************************** - * * - * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE * - * * - * For applications and websites that don't need IE support, like platform * - * targeted mobile apps and web apps, this code can be removed. * - * * - *****************************************************************************/ + /** + * Add a label, a text item that can hold a colored or gradient background + * as well as a border and shadow. + * @param {string} str + * @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 + * coordinates it should be pinned to + * @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) { -/** - * @constructor - */ -var VMLRenderer, VMLElement; -if (!hasSVG && !useCanVG) { + var renderer = this, + wrapper = renderer.g(className), + text = renderer.text('', 0, 0, useHTML) + .attr({ + zIndex: 1 + }), + //.add(wrapper), + box, + bBox, + alignFactor = 0, + padding = 3, + paddingLeft = 0, + width, + height, + wrapperX, + wrapperY, + crispAdjust = 0, + deferredAttr = {}, + baselineOffset, + needsBox, + updateBoxSize, + updateTextPadding, + boxAttr; -/** - * The VML element wrapper. - */ -VMLElement = { + /** + * This function runs after the label is added to the DOM (when the bounding box is + * available), and after the text of the label is updated to detect the new bounding + * box and reflect it in the border box. + */ + updateBoxSize = function () { + var boxX, + boxY, + style = text.element.style; - /** - * Initialize a new VML element wrapper. It builds the markup as a string - * to minimize DOM traffic. - * @param {Object} renderer - * @param {Object} nodeName - */ - init: function (renderer, nodeName) { - var wrapper = this, - markup = ['<', nodeName, ' filled="f" stroked="f"'], - style = ['position: ', ABSOLUTE, ';'], - isDiv = nodeName === DIV; + bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) && defined(text.textStr) && + text.getBBox(); //#3295 && 3514 box failure when string equals 0 + wrapper.width = (width || bBox.width || 0) + 2 * padding + paddingLeft; + wrapper.height = (height || bBox.height || 0) + 2 * padding; - // divs and shapes need size - if (nodeName === 'shape' || isDiv) { - style.push('left:0;top:0;width:1px;height:1px;'); - } - style.push('visibility: ', isDiv ? HIDDEN : VISIBLE); + // update the label-scoped y offset + baselineOffset = padding + renderer.fontMetrics(style && style.fontSize, text).b; - markup.push(' style="', style.join(''), '"/>'); - // create element with default attributes and style - if (nodeName) { - markup = isDiv || nodeName === 'span' || nodeName === 'img' ? - markup.join('') - : renderer.prepVML(markup); - wrapper.element = createElement(markup); - } + if (needsBox) { - wrapper.renderer = renderer; - }, + if (!box) { + // create the border box if it is not already present + boxX = crispAdjust; + boxY = (baseline ? -baselineOffset : 0) + crispAdjust; - /** - * Add the node to the given parent - * @param {Object} parent - */ - add: function (parent) { - var wrapper = this, - renderer = wrapper.renderer, - element = wrapper.element, - box = renderer.box, - inverted = parent && parent.inverted, + wrapper.box = box = shape ? + renderer.symbol(shape, boxX, boxY, wrapper.width, wrapper.height, deferredAttr) : + renderer.rect(boxX, boxY, wrapper.width, wrapper.height, 0, deferredAttr[STROKE_WIDTH]); - // get the parent node - parentNode = parent ? - parent.element || parent : - box; + if (!box.isImg) { // #4324, fill "none" causes it to be ignored by mouse events in IE + box.attr('fill', NONE); + } + box.add(wrapper); + } + // apply the box attributes + if (!box.isImg) { // #1630 + box.attr(extend({ + width: mathRound(wrapper.width), + height: mathRound(wrapper.height) + }, deferredAttr)); + } + deferredAttr = null; + } + }; - // if the parent group is inverted, apply inversion on all children - if (inverted) { // only on groups - renderer.invertChild(element, parentNode); - } + /** + * This function runs after setting text or padding, but only if padding is changed + */ + updateTextPadding = function () { + var styles = wrapper.styles, + textAlign = styles && styles.textAlign, + x = paddingLeft + padding, + y; - // append it - parentNode.appendChild(element); + // determin y based on the baseline + y = baseline ? 0 : baselineOffset; - // align text after adding to be able to read offset - wrapper.added = true; - if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) { - wrapper.updateTransform(); - } + // compensate for alignment + if (defined(width) && bBox && (textAlign === 'center' || textAlign === 'right')) { + x += { center: 0.5, right: 1 }[textAlign] * (width - bBox.width); + } - // fire an event for internal hooks - if (wrapper.onAdd) { - wrapper.onAdd(); - } + // update if anything changed + if (x !== text.x || y !== text.y) { + text.attr('x', x); + if (y !== UNDEFINED) { + text.attr('y', y); + } + } - return wrapper; - }, + // record current values + text.x = x; + text.y = y; + }; - /** - * VML always uses htmlUpdateTransform - */ - updateTransform: SVGElement.prototype.htmlUpdateTransform, + /** + * Set a box attribute, or defer it if the box is not yet created + * @param {Object} key + * @param {Object} value + */ + boxAttr = function (key, value) { + if (box) { + box.attr(key, value); + } else { + deferredAttr[key] = value; + } + }; - /** - * Set the rotation of a span with oldIE's filter - */ - setSpanRotation: function () { - // Adjust for alignment and rotation. Rotation of useHTML content is not yet implemented - // but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+ - // has support for CSS3 transform. The getBBox method also needs to be updated - // to compensate for the rotation, like it currently does for SVG. - // Test case: http://jsfiddle.net/highcharts/Ybt44/ + /** + * After the text element is added, get the desired size of the border box + * and add it before the text in the DOM. + */ + wrapper.onAdd = function () { + text.add(wrapper); + wrapper.attr({ + text: (str || str === 0) ? str : '', // alignment is available now // #3295: 0 not rendered if given as a value + x: x, + y: y + }); - var rotation = this.rotation, - costheta = mathCos(rotation * deg2rad), - sintheta = mathSin(rotation * deg2rad); - - css(this.element, { - filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta, - ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta, - ', sizingMethod=\'auto expand\')'].join('') : NONE - }); - }, + if (box && defined(anchorX)) { + wrapper.attr({ + anchorX: anchorX, + anchorY: anchorY + }); + } + }; - /** - * Get the positioning correction for the span after rotating. - */ - getSpanCorrection: function (width, baseline, alignCorrection, rotation, align) { + /* + * Add specific attribute setters. + */ - var costheta = rotation ? mathCos(rotation * deg2rad) : 1, - sintheta = rotation ? mathSin(rotation * deg2rad) : 0, - height = pick(this.elemHeight, this.element.offsetHeight), - quad, - nonLeft = align && align !== 'left'; + // only change local variables + wrapper.widthSetter = function (value) { + width = value; + }; + wrapper.heightSetter = function (value) { + height = value; + }; + wrapper.paddingSetter = function (value) { + if (defined(value) && value !== padding) { + padding = wrapper.padding = value; + updateTextPadding(); + } + }; + wrapper.paddingLeftSetter = function (value) { + if (defined(value) && value !== paddingLeft) { + paddingLeft = value; + updateTextPadding(); + } + }; - // correct x and y - this.xCorr = costheta < 0 && -width; - this.yCorr = sintheta < 0 && -height; - // correct for baseline and corners spilling out after rotation - quad = costheta * sintheta < 0; - this.xCorr += sintheta * baseline * (quad ? 1 - alignCorrection : alignCorrection); - this.yCorr -= costheta * baseline * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1); - // correct for the length/height of the text - if (nonLeft) { - this.xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1); - if (rotation) { - this.yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1); - } - css(this.element, { - textAlign: align - }); - } - }, + // change local variable and prevent setting attribute on the group + wrapper.alignSetter = function (value) { + value = { left: 0, center: 0.5, right: 1 }[value]; + if (value !== alignFactor) { + alignFactor = value; + if (bBox) { // Bounding box exists, means we're dynamically changing + wrapper.attr({ x: x }); + } + } + }; - /** - * Converts a subset of an SVG path definition to its VML counterpart. Takes an array - * as the parameter and returns a string. - */ - pathToVML: function (value) { - // convert paths - var i = value.length, - path = []; + // apply these to the box and the text alike + wrapper.textSetter = function (value) { + if (value !== UNDEFINED) { + text.textSetter(value); + } + updateBoxSize(); + updateTextPadding(); + }; - while (i--) { + // apply these to the box but not to the text + wrapper['stroke-widthSetter'] = function (value, key) { + if (value) { + needsBox = true; + } + crispAdjust = value % 2 / 2; + boxAttr(key, value); + }; + wrapper.strokeSetter = wrapper.fillSetter = wrapper.rSetter = function (value, key) { + if (key === 'fill' && value) { + needsBox = true; + } + boxAttr(key, value); + }; + wrapper.anchorXSetter = function (value, key) { + anchorX = value; + boxAttr(key, mathRound(value) - crispAdjust - wrapperX); + }; + wrapper.anchorYSetter = function (value, key) { + anchorY = value; + boxAttr(key, value - wrapperY); + }; - // Multiply by 10 to allow subpixel precision. - // Substracting half a pixel seems to make the coordinates - // align with SVG, but this hasn't been tested thoroughly - if (isNumber(value[i])) { - path[i] = mathRound(value[i] * 10) - 5; - } else if (value[i] === 'Z') { // close the path - path[i] = 'x'; - } else { - path[i] = value[i]; + // rename attributes + wrapper.xSetter = function (value) { + wrapper.x = value; // for animation getter + if (alignFactor) { + value -= alignFactor * ((width || bBox.width) + 2 * padding); + } + wrapperX = mathRound(value); + wrapper.attr('translateX', wrapperX); + }; + wrapper.ySetter = function (value) { + wrapperY = wrapper.y = mathRound(value); + wrapper.attr('translateY', wrapperY); + }; - // When the start X and end X coordinates of an arc are too close, - // they are rounded to the same value above. In this case, substract or - // add 1 from the end X and Y positions. #186, #760, #1371, #1410. - if (value.isArc && (value[i] === 'wa' || value[i] === 'at')) { - // Start and end X - if (path[i + 5] === path[i + 7]) { - path[i + 7] += value[i + 7] > value[i + 5] ? 1 : -1; - } - // Start and end Y - if (path[i + 6] === path[i + 8]) { - path[i + 8] += value[i + 8] > value[i + 6] ? 1 : -1; - } - } - } - } + // 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 + */ + css: function (styles) { + if (styles) { + var textStyles = {}; + styles = merge(styles); // create a copy to avoid altering the original object (#537) + each(wrapper.textProps, function (prop) { + if (styles[prop] !== UNDEFINED) { + textStyles[prop] = styles[prop]; + delete styles[prop]; + } + }); + text.css(textStyles); + } + return baseCss.call(wrapper, styles); + }, + /** + * Return the bounding box of the box, not the group + */ + getBBox: function () { + return { + width: bBox.width + 2 * padding, + height: bBox.height + 2 * padding, + x: bBox.x - padding, + y: bBox.y - padding + }; + }, + /** + * Apply the shadow to the box + */ + shadow: function (b) { + if (box) { + box.shadow(b); + } + return wrapper; + }, + /** + * Destroy and release memory. + */ + destroy: function () { - - // Loop up again to handle path shortcuts (#2132) - /*while (i++ < path.length) { - if (path[i] === 'H') { // horizontal line to - path[i] = 'L'; - path.splice(i + 2, 0, path[i - 1]); - } else if (path[i] === 'V') { // vertical line to - path[i] = 'L'; - path.splice(i + 1, 0, path[i - 2]); - } - }*/ - return path.join(' ') || 'x'; - }, + // Added by button implementation + removeEvent(wrapper.element, 'mouseenter'); + removeEvent(wrapper.element, 'mouseleave'); - /** - * Set the element's clipping to a predefined rectangle - * - * @param {String} id The id of the clip rectangle - */ - clip: function (clipRect) { - var wrapper = this, - clipMembers, - cssRet; + if (text) { + text = text.destroy(); + } + if (box) { + box = box.destroy(); + } + // Call base implementation to destroy the rest + SVGElement.prototype.destroy.call(wrapper); - if (clipRect) { - clipMembers = clipRect.members; - erase(clipMembers, wrapper); // Ensure unique list of elements (#1258) - clipMembers.push(wrapper); - wrapper.destroyClip = function () { - erase(clipMembers, wrapper); - }; - cssRet = clipRect.getCSS(wrapper); + // Release local pointers (#1298) + wrapper = renderer = updateBoxSize = updateTextPadding = boxAttr = null; + } + }); + } + }; // end SVGRenderer - } else { - if (wrapper.destroyClip) { - wrapper.destroyClip(); - } - cssRet = { clip: docMode8 ? 'inherit' : 'rect(auto)' }; // #1214 - } - return wrapper.css(cssRet); + // general renderer + Renderer = SVGRenderer; + // extend SvgElement for useHTML option + extend(SVGElement.prototype, { + /** + * Apply CSS to HTML elements. This is used in text within SVG rendering and + * by the VML renderer + */ + htmlCss: function (styles) { + var wrapper = this, + element = wrapper.element, + textWidth = styles && element.tagName === 'SPAN' && styles.width; - }, + if (textWidth) { + delete styles.width; + wrapper.textWidth = textWidth; + wrapper.updateTransform(); + } + if (styles && styles.textOverflow === 'ellipsis') { + styles.whiteSpace = 'nowrap'; + styles.overflow = 'hidden'; + } + wrapper.styles = extend(wrapper.styles, styles); + css(wrapper.element, styles); - /** - * Set styles for the element - * @param {Object} styles - */ - css: SVGElement.prototype.htmlCss, + return wrapper; + }, - /** - * 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. - */ - safeRemoveChild: function (element) { - // discardElement will detach the node from its parent before attaching it - // to the garbage bin. Therefore it is important that the node is attached and have parent. - if (element.parentNode) { - discardElement(element); - } - }, + /** + * VML and useHTML method for calculating the bounding box based on offsets + * @param {Boolean} refresh Whether to force a fresh value from the DOM or to + * use the cached value + * + * @return {Object} A hash containing values for x, y, width and height + */ - /** - * Extend element.destroy by removing it from the clip members array - */ - destroy: function () { - if (this.destroyClip) { - this.destroyClip(); - } + htmlGetBBox: function () { + var wrapper = this, + element = wrapper.element; - return SVGElement.prototype.destroy.apply(this); - }, + // faking getBBox in exported SVG in legacy IE + // faking getBBox in exported SVG in legacy IE (is this a duplicate of the fix for #1079?) + if (element.nodeName === 'text') { + element.style.position = ABSOLUTE; + } - /** - * Add an event listener. VML override for normalizing event parameters. - * @param {String} eventType - * @param {Function} handler - */ - on: function (eventType, handler) { - // simplest possible event model for internal use - this.element['on' + eventType] = function () { - var evt = win.event; - evt.target = evt.srcElement; - handler(evt); - }; - return this; - }, + return { + x: element.offsetLeft, + y: element.offsetTop, + width: element.offsetWidth, + height: element.offsetHeight + }; + }, - /** - * In stacked columns, cut off the shadows so that they don't overlap - */ - cutOffPath: function (path, length) { + /** + * VML override private method to update elements based on internal + * properties based on SVG transform + */ + htmlUpdateTransform: function () { + // aligning non added elements is expensive + if (!this.added) { + this.alignOnAdd = true; + return; + } - var len; + var wrapper = this, + renderer = wrapper.renderer, + elem = wrapper.element, + translateX = wrapper.translateX || 0, + translateY = wrapper.translateY || 0, + x = wrapper.x || 0, + y = wrapper.y || 0, + align = wrapper.textAlign || 'left', + alignCorrection = { left: 0, center: 0.5, right: 1 }[align], + shadows = wrapper.shadows, + styles = wrapper.styles; - path = path.split(/[ ,]/); - len = path.length; + // apply translate + css(elem, { + marginLeft: translateX, + marginTop: translateY + }); + if (shadows) { // used in labels/tooltip + each(shadows, function (shadow) { + css(shadow, { + marginLeft: translateX + 1, + marginTop: translateY + 1 + }); + }); + } - if (len === 9 || len === 11) { - path[len - 4] = path[len - 2] = pInt(path[len - 2]) - 10 * length; - } - return path.join(' '); - }, + // apply inversion + if (wrapper.inverted) { // wrapper is a group + each(elem.childNodes, function (child) { + renderer.invertChild(child, elem); + }); + } - /** - * Apply a drop shadow by copying elements and giving them different strokes - * @param {Boolean|Object} shadowOptions - */ - shadow: function (shadowOptions, group, cutOff) { - var shadows = [], - i, - element = this.element, - renderer = this.renderer, - shadow, - elemStyle = element.style, - markup, - path = element.path, - strokeWidth, - modifiedPath, - shadowWidth, - shadowElementOpacity; + if (elem.tagName === 'SPAN') { - // some times empty paths are not strings - if (path && typeof path.value !== 'string') { - path = 'x'; - } - modifiedPath = path; + var width, + rotation = wrapper.rotation, + baseline, + textWidth = pInt(wrapper.textWidth), + currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth, wrapper.textAlign].join(','); - if (shadowOptions) { - shadowWidth = pick(shadowOptions.width, 3); - shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth; - for (i = 1; i <= 3; i++) { + if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed - strokeWidth = (shadowWidth * 2) + 1 - (2 * i); - // Cut off shadows for stacked column items - if (cutOff) { - modifiedPath = this.cutOffPath(path.value, strokeWidth + 0.5); - } + baseline = renderer.fontMetrics(elem.style.fontSize).b; - markup = ['<shape isShadow="true" strokeweight="', strokeWidth, - '" filled="false" path="', modifiedPath, - '" coordsize="10 10" style="', element.style.cssText, '" />']; + // Renderer specific handling of span rotation + if (defined(rotation)) { + wrapper.setSpanRotation(rotation, alignCorrection, baseline); + } - shadow = createElement(renderer.prepVML(markup), - null, { - left: pInt(elemStyle.left) + pick(shadowOptions.offsetX, 1), - top: pInt(elemStyle.top) + pick(shadowOptions.offsetY, 1) - } - ); - if (cutOff) { - shadow.cutOff = strokeWidth + 1; - } + width = pick(wrapper.elemWidth, elem.offsetWidth); - // apply the opacity - markup = ['<stroke color="', shadowOptions.color || 'black', '" opacity="', shadowElementOpacity * i, '"/>']; - createElement(renderer.prepVML(markup), null, null, shadow); + // Update textWidth + if (width > textWidth && /[ \-]/.test(elem.textContent || elem.innerText)) { // #983, #1254 + css(elem, { + width: textWidth + PX, + display: 'block', + whiteSpace: (styles && styles.whiteSpace) || 'normal' // #3331 + }); + width = textWidth; + } + wrapper.getSpanCorrection(width, baseline, alignCorrection, rotation, align); + } - // insert it - if (group) { - group.element.appendChild(shadow); - } else { - element.parentNode.insertBefore(shadow, element); - } + // apply position with correction + css(elem, { + left: (x + (wrapper.xCorr || 0)) + PX, + top: (y + (wrapper.yCorr || 0)) + PX + }); - // record it - shadows.push(shadow); + // force reflow in webkit to apply the left and top on useHTML element (#1249) + if (isWebKit) { + baseline = elem.offsetHeight; // assigned to baseline for lint purpose + } - } + // record current text transform + wrapper.cTT = currentTextTransform; + } + }, - this.shadows = shadows; - } - return this; - }, - updateShadows: noop, // Used in SVG only + /** + * Set the rotation of an individual HTML span + */ + setSpanRotation: function (rotation, alignCorrection, baseline) { + var rotationStyle = {}, + cssTransformKey = isMS ? '-ms-transform' : isWebKit ? '-webkit-transform' : isFirefox ? 'MozTransform' : isOpera ? '-o-transform' : ''; - setAttr: function (key, value) { - if (docMode8) { // IE8 setAttribute bug - this.element[key] = value; - } else { - this.element.setAttribute(key, value); - } - }, - classSetter: function (value) { - // IE8 Standards mode has problems retrieving the className unless set like this - this.element.className = value; - }, - dashstyleSetter: function (value, key, element) { - 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 */ - }, - dSetter: function (value, key, element) { - var i, - shadows = this.shadows; - value = value || []; - this.d = value.join && value.join(' '); // used in getter for animation + rotationStyle[cssTransformKey] = rotationStyle.transform = 'rotate(' + rotation + 'deg)'; + rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] = rotationStyle.transformOrigin = (alignCorrection * 100) + '% ' + baseline + 'px'; + css(this.element, rotationStyle); + }, - element.path = value = this.pathToVML(value); + /** + * Get the correction in X and Y positioning as the element is rotated. + */ + getSpanCorrection: function (width, baseline, alignCorrection) { + this.xCorr = -width * alignCorrection; + this.yCorr = -baseline; + } + }); - // update shadows - if (shadows) { - i = shadows.length; - while (i--) { - shadows[i].path = shadows[i].cutOff ? this.cutOffPath(value, shadows[i].cutOff) : value; - } - } - this.setAttr(key, value); - }, - fillSetter: function (value, key, element) { - var nodeName = element.nodeName; - if (nodeName === 'SPAN') { // text color - element.style.color = value; - } else if (nodeName !== 'IMG') { // #1336 - element.filled = value !== NONE; - this.setAttr('fillcolor', this.renderer.color(value, element, key, this)); - } - }, - opacitySetter: noop, // Don't bother - animation is too slow and filters introduce artifacts - rotationSetter: function (value, key, element) { - var style = element.style; - this[key] = style[key] = value; // style is for #1873 + // Extend SvgRenderer for useHTML option. + extend(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 + * @param {Number} x + * @param {Number} y + */ + html: function (str, x, y) { + var wrapper = this.createElement('span'), + element = wrapper.element, + renderer = wrapper.renderer; - // Correction for the 1x1 size of the shape container. Used in gauge needles. - style.left = -mathRound(mathSin(value * deg2rad) + 1) + PX; - style.top = mathRound(mathCos(value * deg2rad)) + PX; - }, - strokeSetter: function (value, key, element) { - this.setAttr('strokecolor', this.renderer.color(value, element, key)); - }, - 'stroke-widthSetter': function (value, key, element) { - element.stroked = !!value; // VML "stroked" attribute - this[key] = value; // used in getter, issue #113 - if (isNumber(value)) { - value += PX; - } - this.setAttr('strokeweight', value); - }, - titleSetter: function (value, key) { - this.setAttr(key, value); - }, - visibilitySetter: function (value, key, element) { + // Text setter + wrapper.textSetter = function (value) { + if (value !== element.innerHTML) { + delete this.bBox; + } + element.innerHTML = this.textStr = value; + wrapper.htmlUpdateTransform(); + }; - // Handle inherited visibility - if (value === 'inherit') { - value = VISIBLE; - } - - // Let the shadow follow the main element - if (this.shadows) { - each(this.shadows, function (shadow) { - shadow.style[key] = value; - }); - } + // Various setters which rely on update transform + wrapper.xSetter = wrapper.ySetter = wrapper.alignSetter = wrapper.rotationSetter = function (value, key) { + if (key === 'align') { + key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML. + } + wrapper[key] = value; + wrapper.htmlUpdateTransform(); + }; - // Instead of toggling the visibility CSS property, move the div out of the viewport. - // This works around #61 and #586 - if (element.nodeName === 'DIV') { - value = value === HIDDEN ? '-999em' : 0; + // Set the default attributes + wrapper + .attr({ + text: str, + x: mathRound(x), + y: mathRound(y) + }) + .css({ + position: ABSOLUTE, + fontFamily: this.style.fontFamily, + fontSize: this.style.fontSize + }); - // In order to redraw, IE7 needs the div to be visible when tucked away - // outside the viewport. So the visibility is actually opposite of - // the expected value. This applies to the tooltip only. - if (!docMode8) { - element.style[key] = value ? VISIBLE : HIDDEN; - } - key = 'top'; - } - element.style[key] = value; - }, - xSetter: function (value, key, element) { - this[key] = value; // used in getter + // Keep the whiteSpace style outside the wrapper.styles collection + element.style.whiteSpace = 'nowrap'; - if (key === 'x') { - key = 'left'; - } else if (key === 'y') { - key = 'top'; - }/* else { - value = mathMax(0, value); // don't set width or height below zero (#311) - }*/ + // Use the HTML specific .css method + wrapper.css = wrapper.htmlCss; - // clipping rectangle special - if (this.updateClipping) { - this[key] = value; // the key is now 'left' or 'top' for 'x' and 'y' - this.updateClipping(); - } else { - // normal - element.style[key] = value; - } - }, - zIndexSetter: function (value, key, element) { - element.style[key] = value; - } -}; -Highcharts.VMLElement = VMLElement = extendClass(SVGElement, VMLElement); + // This is specific for HTML within SVG + if (renderer.isSVG) { + wrapper.add = function (svgGroupWrapper) { -// Some shared setters -VMLElement.prototype.ySetter = - VMLElement.prototype.widthSetter = - VMLElement.prototype.heightSetter = - VMLElement.prototype.xSetter; + var htmlGroup, + container = renderer.box.parentNode, + parentGroup, + parents = []; + this.parentGroup = svgGroupWrapper; -/** - * The VML renderer - */ -var VMLRendererExtension = { // inherit SVGRenderer + // Create a mock group to hold the HTML elements + if (svgGroupWrapper) { + htmlGroup = svgGroupWrapper.div; + if (!htmlGroup) { - Element: VMLElement, - isIE8: userAgent.indexOf('MSIE 8.0') > -1, + // Read the parent chain into an array and read from top down + parentGroup = svgGroupWrapper; + while (parentGroup) { + parents.push(parentGroup); - /** - * Initialize the VMLRenderer - * @param {Object} container - * @param {Number} width - * @param {Number} height - */ - init: function (container, width, height, style) { - var renderer = this, - boxWrapper, - box, - css; + // Move up to the next parent group + parentGroup = parentGroup.parentGroup; + } - renderer.alignedObjects = []; + // Ensure dynamically updating position when any parent is translated + each(parents.reverse(), function (parentGroup) { + var htmlGroupStyle, + cls = attr(parentGroup.element, 'class'); - boxWrapper = renderer.createElement(DIV) - .css(extend(this.getStyle(style), { position: RELATIVE})); - box = boxWrapper.element; - container.appendChild(boxWrapper.element); + if (cls) { + cls = { className: cls }; + } // else null + // Create a HTML div and append it to the parent div to emulate + // the SVG group structure + htmlGroup = parentGroup.div = parentGroup.div || createElement(DIV, cls, { + position: ABSOLUTE, + left: (parentGroup.translateX || 0) + PX, + top: (parentGroup.translateY || 0) + PX + }, htmlGroup || container); // the top group is appended to container - // generate the containing box - renderer.isVML = true; - renderer.box = box; - renderer.boxWrapper = boxWrapper; - renderer.cache = {}; + // Shortcut + htmlGroupStyle = htmlGroup.style; + // Set listeners to update the HTML div's position whenever the SVG group + // position is changed + extend(parentGroup, { + translateXSetter: function (value, key) { + htmlGroupStyle.left = value + PX; + parentGroup[key] = value; + parentGroup.doTransform = true; + }, + translateYSetter: function (value, key) { + htmlGroupStyle.top = value + PX; + parentGroup[key] = value; + parentGroup.doTransform = true; + } + }); - renderer.setSize(width, height, false); + // These properties are set as attributes on the SVG group, and as + // identical CSS properties on the div. (#3542) + each(['opacity', 'visibility'], function (prop) { + wrap(parentGroup, prop + 'Setter', function (proceed, value, key, elem) { + proceed.call(this, value, key, elem); + htmlGroupStyle[key] = value; + }); + }); + }); - // The only way to make IE6 and IE7 print is to use a global namespace. However, - // with IE8 the only way to make the dynamic shapes visible in screen and print mode - // seems to be to add the xmlns attribute and the behaviour style inline. - if (!doc.namespaces.hcv) { + } + } else { + htmlGroup = container; + } - doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml'); + htmlGroup.appendChild(element); - // Setup default CSS (#2153, #2368, #2384) - css = 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke' + - '{ behavior:url(#default#VML); display: inline-block; } '; - try { - doc.createStyleSheet().cssText = css; - } catch (e) { - doc.styleSheets[0].cssText += css; - } + // Shared with VML: + wrapper.added = true; + if (wrapper.alignOnAdd) { + wrapper.htmlUpdateTransform(); + } - } - }, + return wrapper; + }; + } + return wrapper; + } + }); - /** - * Detect whether the renderer is hidden. This happens when one of the parent elements - * has display: none - */ - isHidden: function () { - return !this.box.offsetWidth; - }, + /* **************************************************************************** + * * + * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE * + * * + * For applications and websites that don't need IE support, like platform * + * targeted mobile apps and web apps, this code can be removed. * + * * + *****************************************************************************/ - /** - * Define a clipping rectangle. In VML it is accomplished by storing the values - * for setting the CSS style to all associated members. - * - * @param {Number} x - * @param {Number} y - * @param {Number} width - * @param {Number} height - */ - clipRect: function (x, y, width, height) { + /** + * @constructor + */ + var VMLRenderer, VMLElement; + if (!hasSVG && !useCanVG) { - // create a dummy element - var clipRect = this.createElement(), - isObj = isObject(x); + /** + * The VML element wrapper. + */ + VMLElement = { - // mimic a rectangle with its style object for automatic updating in attr - return extend(clipRect, { - members: [], - count: 0, - left: (isObj ? x.x : x) + 1, - top: (isObj ? x.y : y) + 1, - width: (isObj ? x.width : width) - 1, - height: (isObj ? x.height : height) - 1, - getCSS: function (wrapper) { - var element = wrapper.element, - nodeName = element.nodeName, - isShape = nodeName === 'shape', - inverted = wrapper.inverted, - rect = this, - top = rect.top - (isShape ? element.offsetTop : 0), - left = rect.left, - right = left + rect.width, - bottom = top + rect.height, - ret = { - clip: 'rect(' + - mathRound(inverted ? left : top) + 'px,' + - mathRound(inverted ? bottom : right) + 'px,' + - mathRound(inverted ? right : bottom) + 'px,' + - mathRound(inverted ? top : left) + 'px)' - }; + /** + * Initialize a new VML element wrapper. It builds the markup as a string + * to minimize DOM traffic. + * @param {Object} renderer + * @param {Object} nodeName + */ + init: function (renderer, nodeName) { + var wrapper = this, + markup = ['<', nodeName, ' filled="f" stroked="f"'], + style = ['position: ', ABSOLUTE, ';'], + isDiv = nodeName === DIV; - // issue 74 workaround - if (!inverted && docMode8 && nodeName === 'DIV') { - extend(ret, { - width: right + PX, - height: bottom + PX - }); - } - return ret; - }, + // divs and shapes need size + if (nodeName === 'shape' || isDiv) { + style.push('left:0;top:0;width:1px;height:1px;'); + } + style.push('visibility: ', isDiv ? HIDDEN : VISIBLE); - // 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.css(clipRect.getCSS(member)); - } - }); - } - }); + markup.push(' style="', style.join(''), '"/>'); - }, + // create element with default attributes and style + if (nodeName) { + markup = isDiv || nodeName === 'span' || nodeName === 'img' ? + markup.join('') : + renderer.prepVML(markup); + wrapper.element = createElement(markup); + } + wrapper.renderer = renderer; + }, - /** - * Take a color and return it if it's a string, make it a gradient if it's a - * gradient configuration object, and apply opacity. - * - * @param {Object} color The color or config object - */ - color: function (color, elem, prop, wrapper) { - var renderer = this, - colorObject, - regexRgba = /^rgba/, - markup, - fillType, - ret = NONE; + /** + * Add the node to the given parent + * @param {Object} parent + */ + add: function (parent) { + var wrapper = this, + renderer = wrapper.renderer, + element = wrapper.element, + box = renderer.box, + inverted = parent && parent.inverted, - // Check for linear or radial gradient - if (color && color.linearGradient) { - fillType = 'gradient'; - } else if (color && color.radialGradient) { - fillType = 'pattern'; - } + // get the parent node + parentNode = parent ? + parent.element || parent : + box; + if (parent) { + this.parentGroup = parent; + } - if (fillType) { + // if the parent group is inverted, apply inversion on all children + if (inverted) { // only on groups + renderer.invertChild(element, parentNode); + } - var stopColor, - stopOpacity, - gradient = color.linearGradient || color.radialGradient, - x1, - y1, - x2, - y2, - opacity1, - opacity2, - color1, - color2, - fillAttr = '', - stops = color.stops, - firstStop, - 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" />']; - createElement(renderer.prepVML(markup), null, null, elem); - }; + // append it + parentNode.appendChild(element); - // Extend from 0 to 1 - firstStop = stops[0]; - lastStop = stops[stops.length - 1]; - if (firstStop[0] > 0) { - stops.unshift([ - 0, - firstStop[1] - ]); - } - if (lastStop[0] < 1) { - stops.push([ - 1, - lastStop[1] - ]); - } + // align text after adding to be able to read offset + wrapper.added = true; + if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) { + wrapper.updateTransform(); + } - // Compute the stops - each(stops, function (stop, i) { - if (regexRgba.test(stop[1])) { - colorObject = Color(stop[1]); - stopColor = colorObject.get('rgb'); - stopOpacity = colorObject.get('a'); - } else { - stopColor = stop[1]; - stopOpacity = 1; - } + // fire an event for internal hooks + if (wrapper.onAdd) { + wrapper.onAdd(); + } - // Build the color attribute - colors.push((stop[0] * 100) + '% ' + stopColor); + return wrapper; + }, - // Only start and end opacities are allowed, so we use the first and the last - if (!i) { - opacity1 = stopOpacity; - color2 = stopColor; - } else { - opacity2 = stopOpacity; - color1 = stopColor; - } - }); + /** + * VML always uses htmlUpdateTransform + */ + updateTransform: SVGElement.prototype.htmlUpdateTransform, - // Apply the gradient to fills only. - if (prop === 'fill') { + /** + * Set the rotation of a span with oldIE's filter + */ + setSpanRotation: function () { + // Adjust for alignment and rotation. Rotation of useHTML content is not yet implemented + // but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+ + // has support for CSS3 transform. The getBBox method also needs to be updated + // to compensate for the rotation, like it currently does for SVG. + // Test case: http://jsfiddle.net/highcharts/Ybt44/ - // Handle linear gradient angle - if (fillType === 'gradient') { - x1 = gradient.x1 || gradient[0] || 0; - y1 = gradient.y1 || gradient[1] || 0; - x2 = gradient.x2 || gradient[2] || 0; - y2 = gradient.y2 || gradient[3] || 0; - fillAttr = 'angle="' + (90 - math.atan( - (y2 - y1) / // y vector - (x2 - x1) // x vector - ) * 180 / mathPI) + '"'; + var rotation = this.rotation, + costheta = mathCos(rotation * deg2rad), + sintheta = mathSin(rotation * deg2rad); - addFillNode(); + css(this.element, { + filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta, + ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta, + ', sizingMethod=\'auto expand\')'].join('') : NONE + }); + }, - // Radial (circular) gradient - } else { + /** + * Get the positioning correction for the span after rotating. + */ + getSpanCorrection: function (width, baseline, alignCorrection, rotation, align) { - var r = gradient.r, - sizex = r * 2, - sizey = r * 2, - cx = gradient.cx, - cy = gradient.cy, - radialReference = elem.radialReference, - bBox, - applyRadialGradient = function () { - if (radialReference) { - bBox = wrapper.getBBox(); - cx += (radialReference[0] - bBox.x) / bBox.width - 0.5; - cy += (radialReference[1] - bBox.y) / bBox.height - 0.5; - sizex *= radialReference[2] / bBox.width; - sizey *= radialReference[2] / bBox.height; - } - fillAttr = 'src="' + defaultOptions.global.VMLRadialGradientURL + '" ' + - 'size="' + sizex + ',' + sizey + '" ' + - 'origin="0.5,0.5" ' + - 'position="' + cx + ',' + cy + '" ' + - 'color2="' + color2 + '" '; + var costheta = rotation ? mathCos(rotation * deg2rad) : 1, + sintheta = rotation ? mathSin(rotation * deg2rad) : 0, + height = pick(this.elemHeight, this.element.offsetHeight), + quad, + nonLeft = align && align !== 'left'; - addFillNode(); - }; + // correct x and y + this.xCorr = costheta < 0 && -width; + this.yCorr = sintheta < 0 && -height; - // Apply radial gradient - if (wrapper.added) { - applyRadialGradient(); - } else { - // We need to know the bounding box to get the size and position right - wrapper.onAdd = applyRadialGradient; - } + // correct for baseline and corners spilling out after rotation + quad = costheta * sintheta < 0; + this.xCorr += sintheta * baseline * (quad ? 1 - alignCorrection : alignCorrection); + this.yCorr -= costheta * baseline * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1); + // correct for the length/height of the text + if (nonLeft) { + this.xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1); + if (rotation) { + this.yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1); + } + css(this.element, { + textAlign: align + }); + } + }, - // The fill element's color attribute is broken in IE8 standards mode, so we - // need to set the parent shape's fillcolor attribute instead. - ret = color1; - } + /** + * Converts a subset of an SVG path definition to its VML counterpart. Takes an array + * as the parameter and returns a string. + */ + pathToVML: function (value) { + // convert paths + var i = value.length, + path = []; - // Gradients are not supported for VML stroke, return the first color. #722. - } else { - ret = stopColor; - } + while (i--) { - // if the color is an rgba color, split it and add a fill node - // to hold the opacity component - } else if (regexRgba.test(color) && elem.tagName !== 'IMG') { + // Multiply by 10 to allow subpixel precision. + // Substracting half a pixel seems to make the coordinates + // align with SVG, but this hasn't been tested thoroughly + if (isNumber(value[i])) { + path[i] = mathRound(value[i] * 10) - 5; + } else if (value[i] === 'Z') { // close the path + path[i] = 'x'; + } else { + path[i] = value[i]; - colorObject = Color(color); + // When the start X and end X coordinates of an arc are too close, + // they are rounded to the same value above. In this case, substract or + // add 1 from the end X and Y positions. #186, #760, #1371, #1410. + if (value.isArc && (value[i] === 'wa' || value[i] === 'at')) { + // Start and end X + if (path[i + 5] === path[i + 7]) { + path[i + 7] += value[i + 7] > value[i + 5] ? 1 : -1; + } + // Start and end Y + if (path[i + 6] === path[i + 8]) { + path[i + 8] += value[i + 8] > value[i + 6] ? 1 : -1; + } + } + } + } - markup = ['<', prop, ' opacity="', colorObject.get('a'), '"/>']; - createElement(this.prepVML(markup), null, null, elem); - ret = colorObject.get('rgb'); + // Loop up again to handle path shortcuts (#2132) + /*while (i++ < path.length) { + if (path[i] === 'H') { // horizontal line to + path[i] = 'L'; + path.splice(i + 2, 0, path[i - 1]); + } else if (path[i] === 'V') { // vertical line to + path[i] = 'L'; + path.splice(i + 1, 0, path[i - 2]); + } + }*/ + return path.join(' ') || 'x'; + }, + /** + * Set the element's clipping to a predefined rectangle + * + * @param {String} id The id of the clip rectangle + */ + clip: function (clipRect) { + var wrapper = this, + clipMembers, + cssRet; - } else { - var propNodes = elem.getElementsByTagName(prop); // 'stroke' or 'fill' node - if (propNodes.length) { - propNodes[0].opacity = 1; - propNodes[0].type = 'solid'; - } - ret = color; - } + if (clipRect) { + clipMembers = clipRect.members; + erase(clipMembers, wrapper); // Ensure unique list of elements (#1258) + clipMembers.push(wrapper); + wrapper.destroyClip = function () { + erase(clipMembers, wrapper); + }; + cssRet = clipRect.getCSS(wrapper); - return ret; - }, + } else { + if (wrapper.destroyClip) { + wrapper.destroyClip(); + } + cssRet = { clip: docMode8 ? 'inherit' : 'rect(auto)' }; // #1214 + } - /** - * Take a VML string and prepare it for either IE8 or IE6/IE7. - * @param {Array} markup A string array of the VML markup to prepare - */ - prepVML: function (markup) { - var vmlStyle = 'display:inline-block;behavior:url(#default#VML);', - isIE8 = this.isIE8; + return wrapper.css(cssRet); - markup = markup.join(''); + }, - if (isIE8) { // add xmlns and style inline - markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />'); - if (markup.indexOf('style="') === -1) { - markup = markup.replace('/>', ' style="' + vmlStyle + '" />'); - } else { - markup = markup.replace('style="', 'style="' + vmlStyle); - } + /** + * Set styles for the element + * @param {Object} styles + */ + css: SVGElement.prototype.htmlCss, - } else { // add namespace - markup = markup.replace('<', '<hcv:'); - } + /** + * 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. + */ + safeRemoveChild: function (element) { + // discardElement will detach the node from its parent before attaching it + // to the garbage bin. Therefore it is important that the node is attached and have parent. + if (element.parentNode) { + discardElement(element); + } + }, - return markup; - }, + /** + * Extend element.destroy by removing it from the clip members array + */ + destroy: function () { + if (this.destroyClip) { + this.destroyClip(); + } - /** - * Create rotated and aligned text - * @param {String} str - * @param {Number} x - * @param {Number} y - */ - text: SVGRenderer.prototype.html, + return SVGElement.prototype.destroy.apply(this); + }, - /** - * Create and return a path element - * @param {Array} path - */ - path: function (path) { - var attr = { - // subpixel precision down to 0.1 (width and height = 1px) - coordsize: '10 10' - }; - if (isArray(path)) { - attr.d = path; - } else if (isObject(path)) { // attributes - extend(attr, path); - } - // create the shape - return this.createElement('shape').attr(attr); - }, + /** + * Add an event listener. VML override for normalizing event parameters. + * @param {String} eventType + * @param {Function} handler + */ + on: function (eventType, handler) { + // simplest possible event model for internal use + this.element['on' + eventType] = function () { + var evt = win.event; + evt.target = evt.srcElement; + handler(evt); + }; + return this; + }, - /** - * Create and return a circle element. In VML circles are implemented as - * shapes, which is faster than v:oval - * @param {Number} x - * @param {Number} y - * @param {Number} r - */ - circle: function (x, y, r) { - var circle = this.symbol('circle'); - if (isObject(x)) { - r = x.r; - y = x.y; - x = x.x; - } - circle.isCircle = true; // Causes x and y to mean center (#1682) - circle.r = r; - return circle.attr({ x: x, y: y }); - }, + /** + * In stacked columns, cut off the shadows so that they don't overlap + */ + cutOffPath: function (path, length) { - /** - * Create a group using an outer div and an inner v:group to allow rotating - * and flipping. A simple v:group would have problems with positioning - * child HTML elements and CSS clip. - * - * @param {String} name The name of the group - */ - g: function (name) { - var wrapper, - attribs; + var len; - // set the class name - if (name) { - attribs = { 'className': PREFIX + name, 'class': PREFIX + name }; - } + path = path.split(/[ ,]/); + len = path.length; - // the div to hold HTML and clipping - wrapper = this.createElement(DIV).attr(attribs); + if (len === 9 || len === 11) { + path[len - 4] = path[len - 2] = pInt(path[len - 2]) - 10 * length; + } + return path.join(' '); + }, - return wrapper; - }, + /** + * Apply a drop shadow by copying elements and giving them different strokes + * @param {Boolean|Object} shadowOptions + */ + shadow: function (shadowOptions, group, cutOff) { + var shadows = [], + i, + element = this.element, + renderer = this.renderer, + shadow, + elemStyle = element.style, + markup, + path = element.path, + strokeWidth, + modifiedPath, + shadowWidth, + shadowElementOpacity; - /** - * VML override to create a regular HTML image - * @param {String} src - * @param {Number} x - * @param {Number} y - * @param {Number} width - * @param {Number} height - */ - image: function (src, x, y, width, height) { - var obj = this.createElement('img') - .attr({ src: src }); + // some times empty paths are not strings + if (path && typeof path.value !== 'string') { + path = 'x'; + } + modifiedPath = path; - if (arguments.length > 1) { - obj.attr({ - x: x, - y: y, - width: width, - height: height - }); - } - return obj; - }, + if (shadowOptions) { + shadowWidth = pick(shadowOptions.width, 3); + shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth; + for (i = 1; i <= 3; i++) { - /** - * 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); - }, + strokeWidth = (shadowWidth * 2) + 1 - (2 * i); - /** - * In the VML renderer, each child of an inverted div (group) is inverted - * @param {Object} element - * @param {Object} parentNode - */ - invertChild: function (element, parentNode) { - var ren = this, - parentStyle = parentNode.style, - imgStyle = element.tagName === 'IMG' && element.style; // #1111 + // Cut off shadows for stacked column items + if (cutOff) { + modifiedPath = this.cutOffPath(path.value, strokeWidth + 0.5); + } - css(element, { - flip: 'x', - left: pInt(parentStyle.width) - (imgStyle ? pInt(imgStyle.top) : 1), - top: pInt(parentStyle.height) - (imgStyle ? pInt(imgStyle.left) : 1), - rotation: -90 - }); + markup = ['<shape isShadow="true" strokeweight="', strokeWidth, + '" filled="false" path="', modifiedPath, + '" coordsize="10 10" style="', element.style.cssText, '" />']; - // 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); - }); - }, + shadow = createElement(renderer.prepVML(markup), + null, { + left: pInt(elemStyle.left) + pick(shadowOptions.offsetX, 1), + top: pInt(elemStyle.top) + pick(shadowOptions.offsetY, 1) + } + ); + if (cutOff) { + shadow.cutOff = strokeWidth + 1; + } - /** - * Symbol definitions that override the parent SVG renderer's symbols - * - */ - symbols: { - // VML specific arc function - arc: function (x, y, w, h, options) { - var start = options.start, - end = options.end, - radius = options.r || w || h, - innerRadius = options.innerR, - cosStart = mathCos(start), - sinStart = mathSin(start), - cosEnd = mathCos(end), - sinEnd = mathSin(end), - ret; + // apply the opacity + markup = ['<stroke color="', shadowOptions.color || 'black', '" opacity="', shadowElementOpacity * i, '"/>']; + createElement(renderer.prepVML(markup), null, null, shadow); - if (end - start === 0) { // no angle, don't show it. - return ['x']; - } - ret = [ - 'wa', // clockwise arc to - x - radius, // left - y - radius, // top - x + radius, // right - y + radius, // bottom - x + radius * cosStart, // start x - y + radius * sinStart, // start y - x + radius * cosEnd, // end x - y + radius * sinEnd // end y - ]; + // insert it + if (group) { + group.element.appendChild(shadow); + } else { + element.parentNode.insertBefore(shadow, element); + } - if (options.open && !innerRadius) { - ret.push( - 'e', - M, - x,// - innerRadius, - y// - innerRadius - ); - } + // record it + shadows.push(shadow); - ret.push( - 'at', // anti clockwise arc to - x - innerRadius, // left - y - innerRadius, // top - x + innerRadius, // right - y + innerRadius, // bottom - x + innerRadius * cosEnd, // start x - y + innerRadius * sinEnd, // start y - x + innerRadius * cosStart, // end x - y + innerRadius * sinStart, // end y - 'x', // finish path - 'e' // close - ); + } - ret.isArc = true; - return ret; + this.shadows = shadows; + } + return this; + }, + updateShadows: noop, // Used in SVG only - }, - // Add circle symbol path. This performs significantly faster than v:oval. - circle: function (x, y, w, h, wrapper) { + setAttr: function (key, value) { + if (docMode8) { // IE8 setAttribute bug + this.element[key] = value; + } else { + this.element.setAttribute(key, value); + } + }, + classSetter: function (value) { + // IE8 Standards mode has problems retrieving the className unless set like this + this.element.className = value; + }, + dashstyleSetter: function (value, key, element) { + 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 */ + }, + dSetter: function (value, key, element) { + var i, + shadows = this.shadows; + value = value || []; + this.d = value.join && value.join(' '); // used in getter for animation - if (wrapper) { - w = h = 2 * wrapper.r; - } + element.path = value = this.pathToVML(value); - // Center correction, #1682 - if (wrapper && wrapper.isCircle) { - x -= w / 2; - y -= h / 2; - } + // update shadows + if (shadows) { + i = shadows.length; + while (i--) { + shadows[i].path = shadows[i].cutOff ? this.cutOffPath(value, shadows[i].cutOff) : value; + } + } + this.setAttr(key, value); + }, + fillSetter: function (value, key, element) { + var nodeName = element.nodeName; + if (nodeName === 'SPAN') { // text color + element.style.color = value; + } else if (nodeName !== 'IMG') { // #1336 + element.filled = value !== NONE; + this.setAttr('fillcolor', this.renderer.color(value, element, key, this)); + } + }, + opacitySetter: noop, // Don't bother - animation is too slow and filters introduce artifacts + rotationSetter: function (value, key, element) { + var style = element.style; + this[key] = style[key] = value; // style is for #1873 - // Return the path - return [ - 'wa', // clockwisearcto - x, // left - y, // top - x + w, // right - y + h, // bottom - x + w, // start x - y + h / 2, // start y - x + w, // end x - y + h / 2, // end y - //'x', // finish path - 'e' // close - ]; - }, - /** - * Add rectangle symbol path which eases rotation and omits arcsize problems - * compared to the built-in VML roundrect shape. When borders are not rounded, - * use the simpler square path, else use the callout path without the arrow. - */ - rect: function (x, y, w, h, options) { - return SVGRenderer.prototype.symbols[ - !defined(options) || !options.r ? 'square' : 'callout' - ].call(0, x, y, w, h, options); - } - } -}; -Highcharts.VMLRenderer = VMLRenderer = function () { - this.init.apply(this, arguments); -}; -VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension); + // Correction for the 1x1 size of the shape container. Used in gauge needles. + style.left = -mathRound(mathSin(value * deg2rad) + 1) + PX; + style.top = mathRound(mathCos(value * deg2rad)) + PX; + }, + strokeSetter: function (value, key, element) { + this.setAttr('strokecolor', this.renderer.color(value, element, key)); + }, + 'stroke-widthSetter': function (value, key, element) { + element.stroked = !!value; // VML "stroked" attribute + this[key] = value; // used in getter, issue #113 + if (isNumber(value)) { + value += PX; + } + this.setAttr('strokeweight', value); + }, + titleSetter: function (value, key) { + this.setAttr(key, value); + }, + visibilitySetter: function (value, key, element) { - // general renderer - Renderer = VMLRenderer; -} + // Handle inherited visibility + if (value === 'inherit') { + value = VISIBLE; + } -// This method is used with exporting in old IE, when emulating SVG (see #2314) -SVGRenderer.prototype.measureSpanWidth = function (text, styles) { - var measuringSpan = doc.createElement('span'), - offsetWidth, - textNode = doc.createTextNode(text); + // Let the shadow follow the main element + if (this.shadows) { + each(this.shadows, function (shadow) { + shadow.style[key] = value; + }); + } - measuringSpan.appendChild(textNode); - css(measuringSpan, styles); - this.box.appendChild(measuringSpan); - offsetWidth = measuringSpan.offsetWidth; - discardElement(measuringSpan); // #2463 - return offsetWidth; -}; + // Instead of toggling the visibility CSS property, move the div out of the viewport. + // This works around #61 and #586 + if (element.nodeName === 'DIV') { + value = value === HIDDEN ? '-999em' : 0; + // In order to redraw, IE7 needs the div to be visible when tucked away + // outside the viewport. So the visibility is actually opposite of + // the expected value. This applies to the tooltip only. + if (!docMode8) { + element.style[key] = value ? VISIBLE : HIDDEN; + } + key = 'top'; + } + element.style[key] = value; + }, + xSetter: function (value, key, element) { + this[key] = value; // used in getter -/* **************************************************************************** - * * - * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE * - * * - *****************************************************************************/ -/* **************************************************************************** - * * - * START OF ANDROID < 3 SPECIFIC CODE. THIS CAN BE REMOVED IF YOU'RE NOT * - * TARGETING THAT SYSTEM. * - * * - *****************************************************************************/ -var CanVGRenderer, - CanVGController; + if (key === 'x') { + key = 'left'; + } else if (key === 'y') { + key = 'top'; + }/* else { + value = mathMax(0, value); // don't set width or height below zero (#311) + }*/ -if (useCanVG) { - /** - * The CanVGRenderer is empty from start to keep the source footprint small. - * When requested, the CanVGController downloads the rest of the source packaged - * together with the canvg library. - */ - Highcharts.CanVGRenderer = CanVGRenderer = function () { - // Override the global SVG namespace to fake SVG/HTML that accepts CSS - SVG_NS = 'http://www.w3.org/1999/xhtml'; - }; + // clipping rectangle special + if (this.updateClipping) { + this[key] = value; // the key is now 'left' or 'top' for 'x' and 'y' + this.updateClipping(); + } else { + // normal + element.style[key] = value; + } + }, + zIndexSetter: function (value, key, element) { + element.style[key] = value; + } + }; + Highcharts.VMLElement = VMLElement = extendClass(SVGElement, VMLElement); - /** - * Start with an empty symbols object. This is needed when exporting is used (exporting.src.js will add a few symbols), but - * the implementation from SvgRenderer will not be merged in until first render. - */ - CanVGRenderer.prototype.symbols = {}; + // Some shared setters + VMLElement.prototype.ySetter = + VMLElement.prototype.widthSetter = + VMLElement.prototype.heightSetter = + VMLElement.prototype.xSetter; - /** - * Handles on demand download of canvg rendering support. - */ - CanVGController = (function () { - // List of renderering calls - var deferredRenderCalls = []; - /** - * When downloaded, we are ready to draw deferred charts. - */ - function drawDeferred() { - var callLength = deferredRenderCalls.length, - callIndex; + /** + * The VML renderer + */ + var VMLRendererExtension = { // inherit SVGRenderer - // Draw all pending render calls - for (callIndex = 0; callIndex < callLength; callIndex++) { - deferredRenderCalls[callIndex](); - } - // Clear the list - deferredRenderCalls = []; - } + Element: VMLElement, + isIE8: userAgent.indexOf('MSIE 8.0') > -1, - return { - push: function (func, scriptLocation) { - // Only get the script once - if (deferredRenderCalls.length === 0) { - getScript(scriptLocation, drawDeferred); - } - // Register render call - deferredRenderCalls.push(func); - } - }; - }()); - Renderer = CanVGRenderer; -} // end CanVGRenderer + /** + * Initialize the VMLRenderer + * @param {Object} container + * @param {Number} width + * @param {Number} height + */ + init: function (container, width, height, style) { + var renderer = this, + boxWrapper, + box, + css; -/* **************************************************************************** - * * - * END OF ANDROID < 3 SPECIFIC CODE * - * * - *****************************************************************************/ + renderer.alignedObjects = []; -/** - * The Tick class - */ -function Tick(axis, pos, type, noLabel) { - this.axis = axis; - this.pos = pos; - this.type = type || ''; - this.isNew = true; + boxWrapper = renderer.createElement(DIV) + .css(extend(this.getStyle(style), { position: 'relative' })); + box = boxWrapper.element; + container.appendChild(boxWrapper.element); - if (!type && !noLabel) { - this.addLabel(); - } -} -Tick.prototype = { - /** - * Write the tick label - */ - addLabel: function () { - var tick = this, - axis = tick.axis, - options = axis.options, - chart = axis.chart, - categories = axis.categories, - names = axis.names, - pos = tick.pos, - labelOptions = options.labels, - str, - tickPositions = axis.tickPositions, - isFirst = pos === tickPositions[0], - isLast = pos === tickPositions[tickPositions.length - 1], - value = categories ? - pick(categories[pos], names[pos], pos) : - pos, - label = tick.label, - tickPositionInfo = tickPositions.info, - dateTimeLabelFormat; + // generate the containing box + renderer.isVML = true; + renderer.box = box; + renderer.boxWrapper = boxWrapper; + renderer.gradients = {}; + renderer.cache = {}; // Cache for numerical bounding boxes + renderer.cacheKeys = []; - // 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]; - } - // set properties for access in render method - tick.isFirst = isFirst; - tick.isLast = isLast; - // get the string - str = axis.labelFormatter.call({ - axis: axis, - chart: chart, - isFirst: isFirst, - isLast: isLast, - dateTimeLabelFormat: dateTimeLabelFormat, - value: axis.isLog ? correctFloat(lin2log(value)) : value - }); + renderer.setSize(width, height, false); - // prepare CSS - //css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) + PX }; - - // first call - if (!defined(label)) { + // The only way to make IE6 and IE7 print is to use a global namespace. However, + // with IE8 the only way to make the dynamic shapes visible in screen and print mode + // seems to be to add the xmlns attribute and the behaviour style inline. + if (!doc.namespaces.hcv) { - tick.label = label = - defined(str) && labelOptions.enabled ? - chart.renderer.text( - str, - 0, - 0, - labelOptions.useHTML - ) - //.attr(attr) - // without position absolute, IE export sometimes is wrong - .css(merge(labelOptions.style)) - .add(axis.labelGroup) : - null; - tick.labelLength = label && label.getBBox().width; // Un-rotated length - tick.rotation = 0; // Base value to detect change for new calls to getBBox + doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml'); - // update - } else if (label) { - label.attr({ text: str }); - } - }, + // Setup default CSS (#2153, #2368, #2384) + css = 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke' + + '{ behavior:url(#default#VML); display: inline-block; } '; + try { + doc.createStyleSheet().cssText = css; + } catch (e) { + doc.styleSheets[0].cssText += css; + } - /** - * Get the offset height or width of the label - */ - getLabelSize: function () { - return this.label ? - this.label.getBBox()[this.axis.horiz ? 'height' : 'width'] : - 0; - }, + } + }, - /** - * Handle the label overflow by adjusting the labels to the left and right edge, or - * hide them if they collide into the neighbour label. - */ - handleOverflow: function (xy) { - var axis = this.axis, - pxPos = xy.x, - chartWidth = axis.chart.chartWidth, - spacing = axis.chart.spacing, - leftBound = pick(axis.labelLeft, mathMin(axis.pos, spacing[3])), - rightBound = pick(axis.labelRight, mathMax(axis.pos + axis.len, chartWidth - spacing[1])), - label = this.label, - rotation = this.rotation, - factor = { left: 0, center: 0.5, right: 1 }[axis.labelAlign], - labelWidth = label.getBBox().width, - slotWidth = axis.slotWidth, - xCorrection = factor, - goRight = 1, - leftPos, - rightPos, - textWidth, - css = {}; - // Check if the label overshoots the chart spacing box. If it does, move it. - // If it now overshoots the slotWidth, add ellipsis. - if (!rotation) { - leftPos = pxPos - factor * labelWidth; - rightPos = pxPos + (1 - factor) * labelWidth; + /** + * Detect whether the renderer is hidden. This happens when one of the parent elements + * has display: none + */ + isHidden: function () { + return !this.box.offsetWidth; + }, - if (leftPos < leftBound) { - slotWidth = xy.x + slotWidth * (1 - factor) - leftBound; - } else if (rightPos > rightBound) { - slotWidth = rightBound - xy.x + slotWidth * factor; - goRight = -1; - } + /** + * Define a clipping rectangle. In VML it is accomplished by storing the values + * for setting the CSS style to all associated members. + * + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + clipRect: function (x, y, width, height) { - slotWidth = mathMin(axis.slotWidth, slotWidth); // #4177 - if (slotWidth < axis.slotWidth && axis.labelAlign === 'center') { - xy.x += goRight * (axis.slotWidth - slotWidth - xCorrection * (axis.slotWidth - mathMin(labelWidth, slotWidth))); - } - // 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 > slotWidth || (axis.autoRotation && label.styles.width)) { - textWidth = slotWidth; - } + // create a dummy element + var clipRect = this.createElement(), + isObj = isObject(x); - // Add ellipsis to prevent rotated labels to be clipped against the edge of the chart - } else if (rotation < 0 && pxPos - factor * labelWidth < leftBound) { - textWidth = mathRound(pxPos / mathCos(rotation * deg2rad) - leftBound); - } else if (rotation > 0 && pxPos + factor * labelWidth > rightBound) { - textWidth = mathRound((chartWidth - pxPos) / mathCos(rotation * deg2rad)); - } + // mimic a rectangle with its style object for automatic updating in attr + return extend(clipRect, { + members: [], + count: 0, + left: (isObj ? x.x : x) + 1, + top: (isObj ? x.y : y) + 1, + width: (isObj ? x.width : width) - 1, + height: (isObj ? x.height : height) - 1, + getCSS: function (wrapper) { + var element = wrapper.element, + nodeName = element.nodeName, + isShape = nodeName === 'shape', + inverted = wrapper.inverted, + rect = this, + top = rect.top - (isShape ? element.offsetTop : 0), + left = rect.left, + right = left + rect.width, + bottom = top + rect.height, + ret = { + clip: 'rect(' + + mathRound(inverted ? left : top) + 'px,' + + mathRound(inverted ? bottom : right) + 'px,' + + mathRound(inverted ? right : bottom) + 'px,' + + mathRound(inverted ? top : left) + 'px)' + }; - if (textWidth) { - css.width = textWidth; - if (!axis.options.labels.style.textOverflow) { - css.textOverflow = 'ellipsis'; - } - label.css(css); - } - }, + // issue 74 workaround + if (!inverted && docMode8 && nodeName === 'DIV') { + extend(ret, { + width: right + PX, + height: bottom + PX + }); + } + return ret; + }, - /** - * Get the x and y position for ticks and labels - */ - getPosition: function (horiz, pos, tickmarkOffset, old) { - var axis = this.axis, - chart = axis.chart, - cHeight = (old && chart.oldChartHeight) || chart.chartHeight; + // 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.css(clipRect.getCSS(member)); + } + }); + } + }); - 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), + }, - y: horiz ? - cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) : - cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB - }; - }, + /** + * Take a color and return it if it's a string, make it a gradient if it's a + * gradient configuration object, and apply opacity. + * + * @param {Object} color The color or config object + */ + color: function (color, elem, prop, wrapper) { + var renderer = this, + colorObject, + regexRgba = /^rgba/, + markup, + fillType, + ret = NONE; - /** - * Get the x, y position of the tick label - */ - getLabelPosition: function (x, y, label, horiz, labelOptions, tickmarkOffset, index, step) { - var axis = this.axis, - transA = axis.transA, - reversed = axis.reversed, - staggerLines = axis.staggerLines, - rotCorr = axis.tickRotCorr || { x: 0, y: 0 }, - yOffset = pick(labelOptions.y, rotCorr.y + (axis.side === 2 ? 8 : -(label.getBBox().height / 2))), - line; + // Check for linear or radial gradient + if (color && color.linearGradient) { + fillType = 'gradient'; + } else if (color && color.radialGradient) { + fillType = 'pattern'; + } - x = x + labelOptions.x + rotCorr.x - (tickmarkOffset && horiz ? - tickmarkOffset * transA * (reversed ? -1 : 1) : 0); - y = y + yOffset - (tickmarkOffset && !horiz ? - tickmarkOffset * transA * (reversed ? 1 : -1) : 0); - // Correct for staggered labels - if (staggerLines) { - line = (index / (step || 1) % staggerLines); - y += line * (axis.labelOffset / staggerLines); - } + if (fillType) { - return { - x: x, - y: mathRound(y) - }; - }, + var stopColor, + stopOpacity, + gradient = color.linearGradient || color.radialGradient, + x1, + y1, + x2, + y2, + opacity1, + opacity2, + color1, + color2, + fillAttr = '', + stops = color.stops, + firstStop, + 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" />']; + createElement(renderer.prepVML(markup), null, null, elem); + }; - /** - * Extendible method to return the path of the marker - */ - getMarkPath: function (x, y, tickLength, tickWidth, horiz, renderer) { - return renderer.crispLine([ - M, - x, - y, - L, - x + (horiz ? 0 : -tickLength), - y + (horiz ? tickLength : 0) - ], tickWidth); - }, + // Extend from 0 to 1 + firstStop = stops[0]; + lastStop = stops[stops.length - 1]; + if (firstStop[0] > 0) { + stops.unshift([ + 0, + firstStop[1] + ]); + } + if (lastStop[0] < 1) { + stops.push([ + 1, + lastStop[1] + ]); + } - /** - * Put everything in place - * - * @param index {Number} - * @param old {Boolean} Use old coordinates to prepare an animation into new position - */ - render: function (index, old, opacity) { - var tick = this, - axis = tick.axis, - options = axis.options, - chart = axis.chart, - renderer = chart.renderer, - horiz = axis.horiz, - type = tick.type, - label = tick.label, - pos = tick.pos, - labelOptions = options.labels, - gridLine = tick.gridLine, - gridPrefix = type ? type + 'Grid' : 'grid', - tickPrefix = type ? type + 'Tick' : 'tick', - gridLineWidth = options[gridPrefix + 'LineWidth'], - gridLineColor = options[gridPrefix + 'LineColor'], - dashStyle = options[gridPrefix + 'LineDashStyle'], - tickLength = options[tickPrefix + 'Length'], - tickWidth = pick(options[tickPrefix + 'Width'], !type && axis.isXAxis ? 1 : 0), // X axis defaults to 1 - tickColor = options[tickPrefix + 'Color'], - tickPosition = options[tickPrefix + 'Position'], - gridLinePath, - mark = tick.mark, - markPath, - step = /*axis.labelStep || */labelOptions.step, - attribs, - 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 + // Compute the stops + each(stops, function (stop, i) { + if (regexRgba.test(stop[1])) { + colorObject = Color(stop[1]); + stopColor = colorObject.get('rgb'); + stopOpacity = colorObject.get('a'); + } else { + stopColor = stop[1]; + stopOpacity = 1; + } - opacity = pick(opacity, 1); - this.isActive = true; + // Build the color attribute + colors.push((stop[0] * 100) + '% ' + stopColor); - // create the grid line - if (gridLineWidth) { - gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLineWidth * reverseCrisp, old, true); + // Only start and end opacities are allowed, so we use the first and the last + if (!i) { + opacity1 = stopOpacity; + color2 = stopColor; + } else { + opacity2 = stopOpacity; + color1 = stopColor; + } + }); - if (gridLine === UNDEFINED) { - attribs = { - stroke: gridLineColor, - 'stroke-width': gridLineWidth - }; - if (dashStyle) { - attribs.dashstyle = dashStyle; - } - if (!type) { - attribs.zIndex = 1; - } - if (old) { - attribs.opacity = 0; - } - tick.gridLine = gridLine = - gridLineWidth ? - renderer.path(gridLinePath) - .attr(attribs).add(axis.gridGroup) : - null; - } + // Apply the gradient to fills only. + if (prop === 'fill') { - // If the parameter 'old' is set, the current call will be followed - // by another call, therefore do not do any animations this time - if (!old && gridLine && gridLinePath) { - gridLine[tick.isNew ? 'attr' : 'animate']({ - d: gridLinePath, - opacity: opacity - }); - } - } + // Handle linear gradient angle + if (fillType === 'gradient') { + x1 = gradient.x1 || gradient[0] || 0; + y1 = gradient.y1 || gradient[1] || 0; + x2 = gradient.x2 || gradient[2] || 0; + y2 = gradient.y2 || gradient[3] || 0; + fillAttr = 'angle="' + (90 - math.atan( + (y2 - y1) / // y vector + (x2 - x1) // x vector + ) * 180 / mathPI) + '"'; - // create the tick mark - if (tickWidth && tickLength) { + addFillNode(); - // negate the length - if (tickPosition === 'inside') { - tickLength = -tickLength; - } - if (axis.opposite) { - tickLength = -tickLength; - } + // Radial (circular) gradient + } else { - markPath = tick.getMarkPath(x, y, tickLength, tickWidth * reverseCrisp, horiz, renderer); - if (mark) { // updating - mark.animate({ - d: markPath, - opacity: opacity - }); - } else { // first time - tick.mark = renderer.path( - markPath - ).attr({ - stroke: tickColor, - 'stroke-width': tickWidth, - opacity: opacity - }).add(axis.axisGroup); - } - } + var r = gradient.r, + sizex = r * 2, + sizey = r * 2, + cx = gradient.cx, + cy = gradient.cy, + radialReference = elem.radialReference, + bBox, + applyRadialGradient = function () { + if (radialReference) { + bBox = wrapper.getBBox(); + cx += (radialReference[0] - bBox.x) / bBox.width - 0.5; + cy += (radialReference[1] - bBox.y) / bBox.height - 0.5; + sizex *= radialReference[2] / bBox.width; + sizey *= radialReference[2] / bBox.height; + } + fillAttr = 'src="' + defaultOptions.global.VMLRadialGradientURL + '" ' + + 'size="' + sizex + ',' + sizey + '" ' + + 'origin="0.5,0.5" ' + + 'position="' + cx + ',' + cy + '" ' + + 'color2="' + color2 + '" '; - // the label is created on init - now move it into place - if (label && !isNaN(x)) { - label.xy = xy = tick.getLabelPosition(x, y, label, horiz, labelOptions, tickmarkOffset, index, step); + addFillNode(); + }; - // Apply show first and show last. If the tick is both first and last, it is - // a single centered tick, in which case we show the label anyway (#2100). - if ((tick.isFirst && !tick.isLast && !pick(options.showFirstLabel, 1)) || - (tick.isLast && !tick.isFirst && !pick(options.showLastLabel, 1))) { - show = false; + // Apply radial gradient + if (wrapper.added) { + applyRadialGradient(); + } else { + // We need to know the bounding box to get the size and position right + wrapper.onAdd = applyRadialGradient; + } - // Handle label overflow and show or hide accordingly - } else if (horiz && !axis.isRadial && !labelOptions.step && !labelOptions.rotation && !old && opacity !== 0) { - tick.handleOverflow(xy); - } + // The fill element's color attribute is broken in IE8 standards mode, so we + // need to set the parent shape's fillcolor attribute instead. + ret = color1; + } - // apply step - if (step && index % step) { - // show those indices dividable by step - show = false; - } + // Gradients are not supported for VML stroke, return the first color. #722. + } else { + ret = stopColor; + } - // Set the new position, and show or hide - if (show && !isNaN(xy.y)) { - xy.opacity = opacity; - label[tick.isNew ? 'attr' : 'animate'](xy); - tick.isNew = false; - } else { - label.attr('y', -9999); // #1338 - } - } - }, + // if the color is an rgba color, split it and add a fill node + // to hold the opacity component + } else if (regexRgba.test(color) && elem.tagName !== 'IMG') { - /** - * Destructor for the tick prototype - */ - destroy: function () { - destroyObjectProperties(this, this.axis); - } -}; + colorObject = Color(color); -/** - * The object wrapper for plot lines and plot bands - * @param {Object} options - */ -Highcharts.PlotLineOrBand = function (axis, options) { - this.axis = axis; + markup = ['<', prop, ' opacity="', colorObject.get('a'), '"/>']; + createElement(this.prepVML(markup), null, null, elem); - if (options) { - this.options = options; - this.id = options.id; - } -}; + ret = colorObject.get('rgb'); -Highcharts.PlotLineOrBand.prototype = { - - /** - * Render the plot line or plot band. If it is already existing, - * move it. - */ - render: function () { - var plotLine = this, - axis = plotLine.axis, - horiz = axis.horiz, - options = plotLine.options, - optionsLabel = options.label, - label = plotLine.label, - width = options.width, - to = options.to, - from = options.from, - isBand = defined(from) && defined(to), - value = options.value, - dashStyle = options.dashStyle, - svgElem = plotLine.svgElem, - path = [], - addEvent, - eventType, - xs, - ys, - x, - y, - color = options.color, - zIndex = options.zIndex, - events = options.events, - attribs = {}, - renderer = axis.chart.renderer; - // logarithmic conversion - if (axis.isLog) { - from = log2lin(from); - to = log2lin(to); - value = log2lin(value); - } + } else { + var propNodes = elem.getElementsByTagName(prop); // 'stroke' or 'fill' node + if (propNodes.length) { + propNodes[0].opacity = 1; + propNodes[0].type = 'solid'; + } + ret = color; + } - // plot line - if (width) { - path = axis.getPlotLinePath(value, width); - attribs = { - stroke: color, - 'stroke-width': width - }; - if (dashStyle) { - attribs.dashstyle = dashStyle; - } - } else if (isBand) { // plot band + return ret; + }, - path = axis.getPlotBandPath(from, to, options); - if (color) { - attribs.fill = color; - } - if (options.borderWidth) { - attribs.stroke = options.borderColor; - attribs['stroke-width'] = options.borderWidth; - } - } else { - return; - } - // zIndex - if (defined(zIndex)) { - attribs.zIndex = zIndex; - } + /** + * Take a VML string and prepare it for either IE8 or IE6/IE7. + * @param {Array} markup A string array of the VML markup to prepare + */ + prepVML: function (markup) { + var vmlStyle = 'display:inline-block;behavior:url(#default#VML);', + isIE8 = this.isIE8; - // common for lines and bands - if (svgElem) { - if (path) { - svgElem.animate({ - d: path - }, null, svgElem.onGetPath); - } else { - svgElem.hide(); - svgElem.onGetPath = function () { - svgElem.show(); - }; - if (label) { - plotLine.label = label = label.destroy(); - } - } - } else if (path && path.length) { - plotLine.svgElem = svgElem = renderer.path(path) - .attr(attribs).add(); + markup = markup.join(''); - // events - if (events) { - addEvent = function (eventType) { - svgElem.on(eventType, function (e) { - events[eventType].apply(plotLine, [e]); - }); - }; - for (eventType in events) { - addEvent(eventType); - } - } - } + if (isIE8) { // add xmlns and style inline + markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />'); + if (markup.indexOf('style="') === -1) { + markup = markup.replace('/>', ' style="' + vmlStyle + '" />'); + } else { + markup = markup.replace('style="', 'style="' + vmlStyle); + } - // the plot band/line label - if (optionsLabel && defined(optionsLabel.text) && path && path.length && axis.width > 0 && axis.height > 0) { - // apply defaults - optionsLabel = merge({ - align: horiz && isBand && 'center', - x: horiz ? !isBand && 4 : 10, - verticalAlign : !horiz && isBand && 'middle', - y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4, - rotation: horiz && !isBand && 90 - }, optionsLabel); + } else { // add namespace + markup = markup.replace('<', '<hcv:'); + } - // add the SVG element - if (!label) { - attribs = { - align: optionsLabel.textAlign || optionsLabel.align, - rotation: optionsLabel.rotation - }; - if (defined(zIndex)) { - attribs.zIndex = zIndex; - } - plotLine.label = label = renderer.text( - optionsLabel.text, - 0, - 0, - optionsLabel.useHTML - ) - .attr(attribs) - .css(optionsLabel.style) - .add(); - } + return markup; + }, - // get the bounding box and align the label - // #3000 changed to better handle choice between plotband or plotline - xs = [path[1], path[4], (isBand ? path[6] : path[1])]; - ys = [path[2], path[5], (isBand ? path[7] : path[2])]; - x = arrayMin(xs); - y = arrayMin(ys); + /** + * Create rotated and aligned text + * @param {String} str + * @param {Number} x + * @param {Number} y + */ + text: SVGRenderer.prototype.html, - label.align(optionsLabel, false, { - x: x, - y: y, - width: arrayMax(xs) - x, - height: arrayMax(ys) - y - }); - label.show(); + /** + * Create and return a path element + * @param {Array} path + */ + path: function (path) { + var attr = { + // subpixel precision down to 0.1 (width and height = 1px) + coordsize: '10 10' + }; + if (isArray(path)) { + attr.d = path; + } else if (isObject(path)) { // attributes + extend(attr, path); + } + // create the shape + return this.createElement('shape').attr(attr); + }, - } else if (label) { // move out of sight - label.hide(); - } + /** + * Create and return a circle element. In VML circles are implemented as + * shapes, which is faster than v:oval + * @param {Number} x + * @param {Number} y + * @param {Number} r + */ + circle: function (x, y, r) { + var circle = this.symbol('circle'); + if (isObject(x)) { + r = x.r; + y = x.y; + x = x.x; + } + circle.isCircle = true; // Causes x and y to mean center (#1682) + circle.r = r; + return circle.attr({ x: x, y: y }); + }, - // chainable - return plotLine; - }, + /** + * Create a group using an outer div and an inner v:group to allow rotating + * and flipping. A simple v:group would have problems with positioning + * child HTML elements and CSS clip. + * + * @param {String} name The name of the group + */ + g: function (name) { + var wrapper, + attribs; - /** - * Remove the plot line or band - */ - destroy: function () { - // remove it from the lookup - erase(this.axis.plotLinesAndBands, this); - - delete this.axis; - destroyObjectProperties(this); - } -}; + // set the class name + if (name) { + attribs = { 'className': PREFIX + name, 'class': PREFIX + name }; + } -/** - * Object with members for extending the Axis prototype - */ + // the div to hold HTML and clipping + wrapper = this.createElement(DIV).attr(attribs); -AxisPlotLineOrBandExtension = { + return wrapper; + }, - /** - * Create the path for a plot band - */ - getPlotBandPath: function (from, to) { - var toPath = this.getPlotLinePath(to, null, null, true), - path = this.getPlotLinePath(from, null, null, true); + /** + * VML override to create a regular HTML image + * @param {String} src + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + image: function (src, x, y, width, height) { + var obj = this.createElement('img') + .attr({ src: src }); - if (path && toPath && path.toString() !== toPath.toString()) { // #3836 - path.push( - toPath[4], - toPath[5], - toPath[1], - toPath[2] - ); - } else { // outside the axis area - path = null; - } - - return path; - }, + if (arguments.length > 1) { + obj.attr({ + x: x, + y: y, + width: width, + height: height + }); + } + return obj; + }, - addPlotBand: function (options) { - return this.addPlotBandOrLine(options, 'plotBands'); - }, - - addPlotLine: function (options) { - return this.addPlotBandOrLine(options, 'plotLines'); - }, + /** + * 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); + }, - /** - * Add a plot band or plot line after render time - * - * @param options {Object} The plotBand or plotLine configuration object - */ - addPlotBandOrLine: function (options, coll) { - var obj = new Highcharts.PlotLineOrBand(this, options).render(), - userOptions = this.userOptions; + /** + * In the VML renderer, each child of an inverted div (group) is inverted + * @param {Object} element + * @param {Object} parentNode + */ + invertChild: function (element, parentNode) { + var ren = this, + parentStyle = parentNode.style, + imgStyle = element.tagName === 'IMG' && element.style; // #1111 - if (obj) { // #2189 - // Add it to the user options for exporting and Axis.update - if (coll) { - userOptions[coll] = userOptions[coll] || []; - userOptions[coll].push(options); - } - this.plotLinesAndBands.push(obj); - } - - return obj; - }, + css(element, { + flip: 'x', + left: pInt(parentStyle.width) - (imgStyle ? pInt(imgStyle.top) : 1), + top: pInt(parentStyle.height) - (imgStyle ? pInt(imgStyle.left) : 1), + rotation: -90 + }); - /** - * Remove a plot band or plot line from the chart by id - * @param {Object} id - */ - removePlotBandOrLine: function (id) { - var plotLinesAndBands = this.plotLinesAndBands, - options = this.options, - userOptions = this.userOptions, - i = plotLinesAndBands.length; - while (i--) { - if (plotLinesAndBands[i].id === id) { - plotLinesAndBands[i].destroy(); - } - } - each([options.plotLines || [], userOptions.plotLines || [], options.plotBands || [], userOptions.plotBands || []], function (arr) { - i = arr.length; - while (i--) { - if (arr[i].id === id) { - erase(arr, arr[i]); - } - } - }); - } -}; + // 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); + }); + }, -/** - * Create a new axis object - * @param {Object} chart - * @param {Object} options - */ -var Axis = Highcharts.Axis = function () { - this.init.apply(this, arguments); -}; + /** + * Symbol definitions that override the parent SVG renderer's symbols + * + */ + symbols: { + // VML specific arc function + arc: function (x, y, w, h, options) { + var start = options.start, + end = options.end, + radius = options.r || w || h, + innerRadius = options.innerR, + cosStart = mathCos(start), + sinStart = mathSin(start), + cosEnd = mathCos(end), + sinEnd = mathSin(end), + ret; -Axis.prototype = { + if (end - start === 0) { // no angle, don't show it. + return ['x']; + } - /** - * Default options for the X axis - the Y axis has extended defaults - */ - defaultOptions: { - // allowDecimals: null, - // alternateGridColor: null, - // categories: [], - dateTimeLabelFormats: { - millisecond: '%H:%M:%S.%L', - second: '%H:%M:%S', - minute: '%H:%M', - hour: '%H:%M', - day: '%e. %b', - week: '%e. %b', - month: '%b \'%y', - year: '%Y' - }, - endOnTick: false, - gridLineColor: '#D8D8D8', - // gridLineDashStyle: 'solid', - // gridLineWidth: 0, - // reversed: false, + ret = [ + 'wa', // clockwise arc to + x - radius, // left + y - radius, // top + x + radius, // right + y + radius, // bottom + x + radius * cosStart, // start x + y + radius * sinStart, // start y + x + radius * cosEnd, // end x + y + radius * sinEnd // end y + ]; - labels: { - enabled: true, - // rotation: 0, - // align: 'center', - // step: null, - style: { - color: '#606060', - cursor: 'default', - fontSize: '11px' - }, - x: 0, - y: 15 - /*formatter: function () { - return this.value; - },*/ - }, - lineColor: '#C0D0E0', - lineWidth: 1, - //linkedTo: null, - //max: undefined, - //min: undefined, - minPadding: 0.01, - maxPadding: 0.01, - //minRange: null, - minorGridLineColor: '#E0E0E0', - // minorGridLineDashStyle: null, - minorGridLineWidth: 1, - minorTickColor: '#A0A0A0', - //minorTickInterval: null, - minorTickLength: 2, - minorTickPosition: 'outside', // inside or outside - //minorTickWidth: 0, - //opposite: false, - //offset: 0, - //plotBands: [{ - // events: {}, - // zIndex: 1, - // labels: { align, x, verticalAlign, y, style, rotation, textAlign } - //}], - //plotLines: [{ - // events: {} - // dashStyle: {} - // zIndex: - // labels: { align, x, verticalAlign, y, style, rotation, textAlign } - //}], - //reversed: false, - // showFirstLabel: true, - // showLastLabel: true, - startOfWeek: 1, - startOnTick: false, - tickColor: '#C0D0E0', - //tickInterval: null, - tickLength: 10, - tickmarkPlacement: 'between', // on or between - tickPixelInterval: 100, - tickPosition: 'outside', - //tickWidth: 1, - title: { - //text: null, - align: 'middle', // low, middle or high - //margin: 0 for horizontal, 10 for vertical axes, - //rotation: 0, - //side: 'outside', - style: { - color: '#707070' - } - //x: 0, - //y: 0 - }, - type: 'linear' // linear, logarithmic or datetime - //visible: true - }, + if (options.open && !innerRadius) { + ret.push( + 'e', + M, + x,// - innerRadius, + y// - innerRadius + ); + } - /** - * This options set extends the defaultOptions for Y axes - */ - defaultYAxisOptions: { - endOnTick: true, - gridLineWidth: 1, - tickPixelInterval: 72, - showLastLabel: true, - labels: { - x: -8, - y: 3 - }, - lineWidth: 0, - maxPadding: 0.05, - minPadding: 0.05, - startOnTick: true, - //tickWidth: 0, - title: { - rotation: 270, - text: 'Values' - }, - stackLabels: { - enabled: false, - //align: dynamic, - //y: dynamic, - //x: dynamic, - //verticalAlign: dynamic, - //textAlign: dynamic, - //rotation: 0, - formatter: function () { - return Highcharts.numberFormat(this.total, -1); - }, - style: merge(defaultPlotOptions.line.dataLabels.style, { color: '#000000' }) - } - }, + ret.push( + 'at', // anti clockwise arc to + x - innerRadius, // left + y - innerRadius, // top + x + innerRadius, // right + y + innerRadius, // bottom + x + innerRadius * cosEnd, // start x + y + innerRadius * sinEnd, // start y + x + innerRadius * cosStart, // end x + y + innerRadius * sinStart, // end y + 'x', // finish path + 'e' // close + ); - /** - * These options extend the defaultOptions for left axes - */ - defaultLeftAxisOptions: { - labels: { - x: -15, - y: null - }, - title: { - rotation: 270 - } - }, + ret.isArc = true; + return ret; - /** - * These options extend the defaultOptions for right axes - */ - defaultRightAxisOptions: { - labels: { - x: 15, - y: null - }, - title: { - rotation: 90 - } - }, + }, + // Add circle symbol path. This performs significantly faster than v:oval. + circle: function (x, y, w, h, wrapper) { - /** - * These options extend the defaultOptions for bottom axes - */ - defaultBottomAxisOptions: { - labels: { - autoRotation: [-45], - x: 0, - y: null // based on font size - // overflow: undefined, - // staggerLines: null - }, - title: { - rotation: 0 - } - }, - /** - * These options extend the defaultOptions for top axes - */ - defaultTopAxisOptions: { - labels: { - autoRotation: [-45], - x: 0, - y: -15 - // overflow: undefined - // staggerLines: null - }, - title: { - rotation: 0 - } - }, + if (wrapper) { + w = h = 2 * wrapper.r; + } - /** - * Initialize the axis - */ - init: function (chart, userOptions) { + // Center correction, #1682 + if (wrapper && wrapper.isCircle) { + x -= w / 2; + y -= h / 2; + } + // Return the path + return [ + 'wa', // clockwisearcto + x, // left + y, // top + x + w, // right + y + h, // bottom + x + w, // start x + y + h / 2, // start y + x + w, // end x + y + h / 2, // end y + //'x', // finish path + 'e' // close + ]; + }, + /** + * Add rectangle symbol path which eases rotation and omits arcsize problems + * compared to the built-in VML roundrect shape. When borders are not rounded, + * use the simpler square path, else use the callout path without the arrow. + */ + rect: function (x, y, w, h, options) { + return SVGRenderer.prototype.symbols[ + !defined(options) || !options.r ? 'square' : 'callout' + ].call(0, x, y, w, h, options); + } + } + }; + Highcharts.VMLRenderer = VMLRenderer = function () { + this.init.apply(this, arguments); + }; + VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension); - var isXAxis = userOptions.isX, - axis = this; + // general renderer + Renderer = VMLRenderer; + } - axis.chart = chart; + // This method is used with exporting in old IE, when emulating SVG (see #2314) + SVGRenderer.prototype.measureSpanWidth = function (text, styles) { + var measuringSpan = doc.createElement('span'), + offsetWidth, + textNode = doc.createTextNode(text); - // Flag, is the axis horizontal - axis.horiz = chart.inverted ? !isXAxis : isXAxis; + measuringSpan.appendChild(textNode); + css(measuringSpan, styles); + this.box.appendChild(measuringSpan); + offsetWidth = measuringSpan.offsetWidth; + discardElement(measuringSpan); // #2463 + return offsetWidth; + }; - // Flag, isXAxis - axis.isXAxis = isXAxis; - axis.coll = isXAxis ? 'xAxis' : 'yAxis'; - axis.opposite = userOptions.opposite; // needed in setOptions - axis.side = userOptions.side || (axis.horiz ? - (axis.opposite ? 0 : 2) : // top : bottom - (axis.opposite ? 1 : 3)); // right : left + /* **************************************************************************** + * * + * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE * + * * + *****************************************************************************/ + /* **************************************************************************** + * * + * START OF ANDROID < 3 SPECIFIC CODE. THIS CAN BE REMOVED IF YOU'RE NOT * + * TARGETING THAT SYSTEM. * + * * + *****************************************************************************/ + var CanVGRenderer, + CanVGController; - axis.setOptions(userOptions); + if (useCanVG) { + /** + * The CanVGRenderer is empty from start to keep the source footprint small. + * When requested, the CanVGController downloads the rest of the source packaged + * together with the canvg library. + */ + Highcharts.CanVGRenderer = CanVGRenderer = function () { + // Override the global SVG namespace to fake SVG/HTML that accepts CSS + SVG_NS = 'http://www.w3.org/1999/xhtml'; + }; + /** + * Start with an empty symbols object. This is needed when exporting is used (exporting.src.js will add a few symbols), but + * the implementation from SvgRenderer will not be merged in until first render. + */ + CanVGRenderer.prototype.symbols = {}; - var options = this.options, - type = options.type, - isDatetimeAxis = type === 'datetime'; + /** + * Handles on demand download of canvg rendering support. + */ + CanVGController = (function () { + // List of renderering calls + var deferredRenderCalls = []; - axis.labelFormatter = options.labels.formatter || axis.defaultLabelFormatter; // can be overwritten by dynamic format + /** + * When downloaded, we are ready to draw deferred charts. + */ + function drawDeferred() { + var callLength = deferredRenderCalls.length, + callIndex; + // Draw all pending render calls + for (callIndex = 0; callIndex < callLength; callIndex++) { + deferredRenderCalls[callIndex](); + } + // Clear the list + deferredRenderCalls = []; + } - // Flag, stagger lines or not - axis.userOptions = userOptions; + return { + push: function (func, scriptLocation) { + // Only get the script once + if (deferredRenderCalls.length === 0) { + Highcharts.getScript(scriptLocation, drawDeferred); + } + // Register render call + deferredRenderCalls.push(func); + } + }; + }()); - //axis.axisTitleMargin = UNDEFINED,// = options.title.margin, - axis.minPixelPadding = 0; + Renderer = CanVGRenderer; + } // end CanVGRenderer - axis.reversed = options.reversed; - axis.visible = options.visible !== false; - axis.zoomEnabled = options.zoomEnabled !== false; + /* **************************************************************************** + * * + * END OF ANDROID < 3 SPECIFIC CODE * + * * + *****************************************************************************/ - // Initial categories - axis.categories = options.categories || type === 'category'; - axis.names = axis.names || []; // Preserve on update (#3830) + /** + * The Tick class + */ + function Tick(axis, pos, type, noLabel) { + this.axis = axis; + this.pos = pos; + this.type = type || ''; + this.isNew = true; - // Elements - //axis.axisGroup = UNDEFINED; - //axis.gridGroup = UNDEFINED; - //axis.axisTitle = UNDEFINED; - //axis.axisLine = UNDEFINED; + if (!type && !noLabel) { + this.addLabel(); + } + } - // Shorthand types - axis.isLog = type === 'logarithmic'; - axis.isDatetimeAxis = isDatetimeAxis; + Tick.prototype = { + /** + * Write the tick label + */ + addLabel: function () { + var tick = this, + axis = tick.axis, + options = axis.options, + chart = axis.chart, + categories = axis.categories, + names = axis.names, + pos = tick.pos, + labelOptions = options.labels, + str, + tickPositions = axis.tickPositions, + isFirst = pos === tickPositions[0], + isLast = pos === tickPositions[tickPositions.length - 1], + value = categories ? + pick(categories[pos], names[pos], pos) : + pos, + label = tick.label, + tickPositionInfo = tickPositions.info, + dateTimeLabelFormat; - // Flag, if axis is linked to another axis - axis.isLinked = defined(options.linkedTo); - // Linked axis. - //axis.linkedParent = UNDEFINED; + // 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]; + } + // set properties for access in render method + tick.isFirst = isFirst; + tick.isLast = isLast; - // Tick positions - //axis.tickPositions = UNDEFINED; // array containing predefined positions - // Tick intervals - //axis.tickInterval = UNDEFINED; - //axis.minorTickInterval = UNDEFINED; + // get the string + str = axis.labelFormatter.call({ + axis: axis, + chart: chart, + isFirst: isFirst, + isLast: isLast, + dateTimeLabelFormat: dateTimeLabelFormat, + value: axis.isLog ? correctFloat(lin2log(value)) : value + }); - - // Major ticks - axis.ticks = {}; - axis.labelEdge = []; - // Minor ticks - axis.minorTicks = {}; + // prepare CSS + //css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) + PX }; - // List of plotLines/Bands - axis.plotLinesAndBands = []; + // first call + if (!defined(label)) { - // Alternate bands - axis.alternateBands = {}; + tick.label = label = + defined(str) && labelOptions.enabled ? + chart.renderer.text( + str, + 0, + 0, + labelOptions.useHTML + ) + //.attr(attr) + // without position absolute, IE export sometimes is wrong + .css(merge(labelOptions.style)) + .add(axis.labelGroup) : + null; + tick.labelLength = label && label.getBBox().width; // Un-rotated length + tick.rotation = 0; // Base value to detect change for new calls to getBBox - // Axis metrics - //axis.left = UNDEFINED; - //axis.top = UNDEFINED; - //axis.width = UNDEFINED; - //axis.height = UNDEFINED; - //axis.bottom = UNDEFINED; - //axis.right = UNDEFINED; - //axis.transA = UNDEFINED; - //axis.transB = UNDEFINED; - //axis.oldTransA = UNDEFINED; - axis.len = 0; - //axis.oldMin = UNDEFINED; - //axis.oldMax = UNDEFINED; - //axis.oldUserMin = UNDEFINED; - //axis.oldUserMax = UNDEFINED; - //axis.oldAxisLength = UNDEFINED; - axis.minRange = axis.userMinRange = options.minRange || options.maxZoom; - axis.range = options.range; - axis.offset = options.offset || 0; + // update + } else if (label) { + label.attr({ text: str }); + } + }, + /** + * Get the offset height or width of the label + */ + getLabelSize: function () { + return this.label ? + this.label.getBBox()[this.axis.horiz ? 'height' : 'width'] : + 0; + }, - // Dictionary for stacks - axis.stacks = {}; - axis.oldStacks = {}; - axis.stacksTouched = 0; + /** + * Handle the label overflow by adjusting the labels to the left and right edge, or + * hide them if they collide into the neighbour label. + */ + handleOverflow: function (xy) { + var axis = this.axis, + pxPos = xy.x, + chartWidth = axis.chart.chartWidth, + spacing = axis.chart.spacing, + leftBound = pick(axis.labelLeft, mathMin(axis.pos, spacing[3])), + rightBound = pick(axis.labelRight, mathMax(axis.pos + axis.len, chartWidth - spacing[1])), + label = this.label, + rotation = this.rotation, + factor = { left: 0, center: 0.5, right: 1 }[axis.labelAlign], + labelWidth = label.getBBox().width, + slotWidth = axis.slotWidth, + xCorrection = factor, + goRight = 1, + leftPos, + rightPos, + textWidth, + css = {}; - // Min and max in the data - //axis.dataMin = UNDEFINED, - //axis.dataMax = UNDEFINED, + // Check if the label overshoots the chart spacing box. If it does, move it. + // If it now overshoots the slotWidth, add ellipsis. + if (!rotation) { + leftPos = pxPos - factor * labelWidth; + rightPos = pxPos + (1 - factor) * labelWidth; - // The axis range - axis.max = null; - axis.min = null; + if (leftPos < leftBound) { + slotWidth = xy.x + slotWidth * (1 - factor) - leftBound; + } else if (rightPos > rightBound) { + slotWidth = rightBound - xy.x + slotWidth * factor; + goRight = -1; + } - // User set min and max - //axis.userMin = UNDEFINED, - //axis.userMax = UNDEFINED, + slotWidth = mathMin(axis.slotWidth, slotWidth); // #4177 + if (slotWidth < axis.slotWidth && axis.labelAlign === 'center') { + xy.x += goRight * (axis.slotWidth - slotWidth - xCorrection * (axis.slotWidth - mathMin(labelWidth, slotWidth))); + } + // 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 > slotWidth || (axis.autoRotation && label.styles.width)) { + textWidth = slotWidth; + } - // Crosshair options - axis.crosshair = pick(options.crosshair, splat(chart.options.tooltip.crosshairs)[isXAxis ? 0 : 1], false); - // Run Axis + // Add ellipsis to prevent rotated labels to be clipped against the edge of the chart + } else if (rotation < 0 && pxPos - factor * labelWidth < leftBound) { + textWidth = mathRound(pxPos / mathCos(rotation * deg2rad) - leftBound); + } else if (rotation > 0 && pxPos + factor * labelWidth > rightBound) { + textWidth = mathRound((chartWidth - pxPos) / mathCos(rotation * deg2rad)); + } - var eventType, - events = axis.options.events; + if (textWidth) { + css.width = textWidth; + if (!axis.options.labels.style.textOverflow) { + css.textOverflow = 'ellipsis'; + } + label.css(css); + } + }, - // Register - if (inArray(axis, chart.axes) === -1) { // don't add it again on Axis.update() - if (isXAxis && !this.isColorAxis) { // #2713 - chart.axes.splice(chart.xAxis.length, 0, axis); - } else { - chart.axes.push(axis); - } + /** + * Get the x and y position for ticks and labels + */ + getPosition: function (horiz, pos, tickmarkOffset, old) { + var axis = this.axis, + chart = axis.chart, + cHeight = (old && chart.oldChartHeight) || chart.chartHeight; - chart[axis.coll].push(axis); - } + 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.series = axis.series || []; // populated by Series + y: horiz ? + cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) : + cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB + }; - // inverted charts have reversed xAxes as default - if (chart.inverted && isXAxis && axis.reversed === UNDEFINED) { - axis.reversed = true; - } + }, - axis.removePlotBand = axis.removePlotBandOrLine; - axis.removePlotLine = axis.removePlotBandOrLine; + /** + * Get the x, y position of the tick label + */ + getLabelPosition: function (x, y, label, horiz, labelOptions, tickmarkOffset, index, step) { + var axis = this.axis, + transA = axis.transA, + reversed = axis.reversed, + staggerLines = axis.staggerLines, + rotCorr = axis.tickRotCorr || { x: 0, y: 0 }, + yOffset = labelOptions.y, + line; + if (!defined(yOffset)) { + yOffset = axis.side === 2 ? + rotCorr.y + 8 : + // #3140, #3140 + yOffset = mathCos(label.rotation * deg2rad) * (rotCorr.y - label.getBBox(false, 0).height / 2); + } - // register event listeners - for (eventType in events) { - addEvent(axis, eventType, events[eventType]); - } + x = x + labelOptions.x + rotCorr.x - (tickmarkOffset && horiz ? + tickmarkOffset * transA * (reversed ? -1 : 1) : 0); + y = y + yOffset - (tickmarkOffset && !horiz ? + tickmarkOffset * transA * (reversed ? 1 : -1) : 0); - // extend logarithmic axis - if (axis.isLog) { - axis.val2lin = log2lin; - axis.lin2val = lin2log; - } - }, + // Correct for staggered labels + if (staggerLines) { + line = (index / (step || 1) % staggerLines); + if (axis.opposite) { + line = staggerLines - line - 1; + } + y += line * (axis.labelOffset / staggerLines); + } - /** - * Merge and set options - */ - setOptions: function (userOptions) { - this.options = merge( - this.defaultOptions, - this.isXAxis ? {} : this.defaultYAxisOptions, - [this.defaultTopAxisOptions, this.defaultRightAxisOptions, - this.defaultBottomAxisOptions, this.defaultLeftAxisOptions][this.side], - merge( - defaultOptions[this.coll], // if set in setOptions (#1053) - userOptions - ) - ); - }, + return { + x: x, + y: mathRound(y) + }; + }, - /** - * The default label formatter. The context is a special config object for the label. - */ - defaultLabelFormatter: function () { - var axis = this.axis, - value = this.value, - categories = axis.categories, - dateTimeLabelFormat = this.dateTimeLabelFormat, - numericSymbols = defaultOptions.lang.numericSymbols, - i = numericSymbols && numericSymbols.length, - multi, - ret, - formatOption = axis.options.labels.format, + /** + * Extendible method to return the path of the marker + */ + getMarkPath: function (x, y, tickLength, tickWidth, horiz, renderer) { + return renderer.crispLine([ + M, + x, + y, + L, + x + (horiz ? 0 : -tickLength), + y + (horiz ? tickLength : 0) + ], tickWidth); + }, - // make sure the same symbol is added for all labels on a linear axis - numericSymbolDetector = axis.isLog ? value : axis.tickInterval; + /** + * Put everything in place + * + * @param index {Number} + * @param old {Boolean} Use old coordinates to prepare an animation into new position + */ + render: function (index, old, opacity) { + var tick = this, + axis = tick.axis, + options = axis.options, + chart = axis.chart, + renderer = chart.renderer, + horiz = axis.horiz, + type = tick.type, + label = tick.label, + pos = tick.pos, + labelOptions = options.labels, + gridLine = tick.gridLine, + gridPrefix = type ? type + 'Grid' : 'grid', + tickPrefix = type ? type + 'Tick' : 'tick', + gridLineWidth = options[gridPrefix + 'LineWidth'], + gridLineColor = options[gridPrefix + 'LineColor'], + dashStyle = options[gridPrefix + 'LineDashStyle'], + tickLength = options[tickPrefix + 'Length'], + tickWidth = pick(options[tickPrefix + 'Width'], !type && axis.isXAxis ? 1 : 0), // X axis defaults to 1 + tickColor = options[tickPrefix + 'Color'], + tickPosition = options[tickPrefix + 'Position'], + gridLinePath, + mark = tick.mark, + markPath, + step = /*axis.labelStep || */labelOptions.step, + attribs, + 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 - if (formatOption) { - ret = format(formatOption, this); + opacity = pick(opacity, 1); + this.isActive = true; - } else if (categories) { - ret = value; + // create the grid line + if (gridLineWidth) { + gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLineWidth * reverseCrisp, old, true); - } else if (dateTimeLabelFormat) { // datetime axis - ret = dateFormat(dateTimeLabelFormat, value); + if (gridLine === UNDEFINED) { + attribs = { + stroke: gridLineColor, + 'stroke-width': gridLineWidth + }; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } + if (!type) { + attribs.zIndex = 1; + } + if (old) { + attribs.opacity = 0; + } + tick.gridLine = gridLine = + gridLineWidth ? + renderer.path(gridLinePath) + .attr(attribs).add(axis.gridGroup) : + null; + } - } 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); - if (numericSymbolDetector >= multi && (value * 10) % multi === 0 && numericSymbols[i] !== null) { - ret = Highcharts.numberFormat(value / multi, -1) + numericSymbols[i]; - } - } - } + // If the parameter 'old' is set, the current call will be followed + // by another call, therefore do not do any animations this time + if (!old && gridLine && gridLinePath) { + gridLine[tick.isNew ? 'attr' : 'animate']({ + d: gridLinePath, + opacity: opacity + }); + } + } - if (ret === UNDEFINED) { - if (mathAbs(value) >= 10000) { // add thousands separators - ret = Highcharts.numberFormat(value, -1); + // create the tick mark + if (tickWidth && tickLength) { - } else { // small numbers - ret = Highcharts.numberFormat(value, -1, UNDEFINED, ''); // #2466 - } - } + // negate the length + if (tickPosition === 'inside') { + tickLength = -tickLength; + } + if (axis.opposite) { + tickLength = -tickLength; + } - return ret; - }, + markPath = tick.getMarkPath(x, y, tickLength, tickWidth * reverseCrisp, horiz, renderer); + if (mark) { // updating + mark.animate({ + d: markPath, + opacity: opacity + }); + } else { // first time + tick.mark = renderer.path( + markPath + ).attr({ + stroke: tickColor, + 'stroke-width': tickWidth, + opacity: opacity + }).add(axis.axisGroup); + } + } - /** - * Get the minimum and maximum for the series of each axis - */ - getSeriesExtremes: function () { - var axis = this, - chart = axis.chart; + // the label is created on init - now move it into place + if (label && !isNaN(x)) { + label.xy = xy = tick.getLabelPosition(x, y, label, horiz, labelOptions, tickmarkOffset, index, step); - axis.hasVisibleSeries = false; + // Apply show first and show last. If the tick is both first and last, it is + // a single centered tick, in which case we show the label anyway (#2100). + if ((tick.isFirst && !tick.isLast && !pick(options.showFirstLabel, 1)) || + (tick.isLast && !tick.isFirst && !pick(options.showLastLabel, 1))) { + show = false; - // Reset properties in case we're redrawing (#3353) - axis.dataMin = axis.dataMax = axis.threshold = null; - axis.softThreshold = !axis.isXAxis; - - if (axis.buildStacks) { - axis.buildStacks(); - } + // Handle label overflow and show or hide accordingly + } else if (horiz && !axis.isRadial && !labelOptions.step && !labelOptions.rotation && !old && opacity !== 0) { + tick.handleOverflow(xy); + } - // loop through this axis' series - each(axis.series, function (series) { + // apply step + if (step && index % step) { + // show those indices dividable by step + show = false; + } - if (series.visible || !chart.options.chart.ignoreHiddenSeries) { + // Set the new position, and show or hide + if (show && !isNaN(xy.y)) { + xy.opacity = opacity; + label[tick.isNew ? 'attr' : 'animate'](xy); + tick.isNew = false; + } else { + label.attr('y', -9999); // #1338 + } + } + }, - var seriesOptions = series.options, - xData, - threshold = seriesOptions.threshold, - seriesDataMin, - seriesDataMax; + /** + * Destructor for the tick prototype + */ + destroy: function () { + destroyObjectProperties(this, this.axis); + } + }; - axis.hasVisibleSeries = true; + /** + * The object wrapper for plot lines and plot bands + * @param {Object} options + */ + Highcharts.PlotLineOrBand = function (axis, options) { + this.axis = axis; - // Validate threshold in logarithmic axes - if (axis.isLog && threshold <= 0) { - threshold = null; - } + if (options) { + this.options = options; + this.id = options.id; + } + }; - // Get dataMin and dataMax for X axes - if (axis.isXAxis) { - xData = series.xData; - if (xData.length) { - axis.dataMin = mathMin(pick(axis.dataMin, xData[0]), arrayMin(xData)); - axis.dataMax = mathMax(pick(axis.dataMax, xData[0]), arrayMax(xData)); - } + Highcharts.PlotLineOrBand.prototype = { - // Get dataMin and dataMax for Y axes, as well as handle stacking and processed data - } else { + /** + * Render the plot line or plot band. If it is already existing, + * move it. + */ + render: function () { + var plotLine = this, + axis = plotLine.axis, + horiz = axis.horiz, + options = plotLine.options, + optionsLabel = options.label, + label = plotLine.label, + width = options.width, + to = options.to, + from = options.from, + isBand = defined(from) && defined(to), + value = options.value, + dashStyle = options.dashStyle, + svgElem = plotLine.svgElem, + path = [], + addEvent, + eventType, + xs, + ys, + x, + y, + color = options.color, + zIndex = options.zIndex, + events = options.events, + attribs = {}, + renderer = axis.chart.renderer; - // Get this particular series extremes - series.getExtremes(); - seriesDataMax = series.dataMax; - seriesDataMin = series.dataMin; + // logarithmic conversion + if (axis.isLog) { + from = log2lin(from); + to = log2lin(to); + value = log2lin(value); + } - // Get the dataMin and dataMax so far. If percentage is used, the min and max are - // always 0 and 100. If seriesDataMin and seriesDataMax is null, then series - // doesn't have active y data, we continue with nulls - if (defined(seriesDataMin) && defined(seriesDataMax)) { - axis.dataMin = mathMin(pick(axis.dataMin, seriesDataMin), seriesDataMin); - axis.dataMax = mathMax(pick(axis.dataMax, seriesDataMax), seriesDataMax); - } + // plot line + if (width) { + path = axis.getPlotLinePath(value, width); + attribs = { + stroke: color, + 'stroke-width': width + }; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } + } else if (isBand) { // plot band - // Adjust to threshold - if (defined(threshold)) { - axis.threshold = threshold; - } - // If any series has a hard threshold, it takes precedence - if (!seriesOptions.softThreshold || axis.isLog) { - axis.softThreshold = false; - } - } - } - }); - }, + path = axis.getPlotBandPath(from, to, options); + if (color) { + attribs.fill = color; + } + if (options.borderWidth) { + attribs.stroke = options.borderColor; + attribs['stroke-width'] = options.borderWidth; + } + } else { + return; + } + // zIndex + if (defined(zIndex)) { + attribs.zIndex = zIndex; + } - /** - * Translate from axis value to pixel position on the chart, or back - * - */ - translate: function (val, backwards, cvsCoord, old, handleLog, pointPlacement) { - var axis = this.linkedParent || this, // #1417 - sign = 1, - cvsOffset = 0, - localA = old ? axis.oldTransA : axis.transA, - localMin = old ? axis.oldMin : axis.min, - returnValue, - minPixelPadding = axis.minPixelPadding, - doPostTranslate = (axis.doPostTranslate || (axis.isLog && handleLog)) && axis.lin2val; + // common for lines and bands + if (svgElem) { + if (path) { + svgElem.show(); + svgElem.animate({ d: path }); + } else { + svgElem.hide(); + if (label) { + plotLine.label = label = label.destroy(); + } + } + } else if (path && path.length) { + plotLine.svgElem = svgElem = renderer.path(path) + .attr(attribs).add(); - if (!localA) { - localA = axis.transA; - } + // events + if (events) { + addEvent = function (eventType) { + svgElem.on(eventType, function (e) { + events[eventType].apply(plotLine, [e]); + }); + }; + for (eventType in events) { + addEvent(eventType); + } + } + } - // In vertical axes, the canvas coordinates start from 0 at the top like in - // SVG. - if (cvsCoord) { - sign *= -1; // canvas coordinates inverts the value - cvsOffset = axis.len; - } + // the plot band/line label + if (optionsLabel && defined(optionsLabel.text) && path && path.length && + axis.width > 0 && axis.height > 0 && !path.flat) { + // apply defaults + optionsLabel = merge({ + align: horiz && isBand && 'center', + x: horiz ? !isBand && 4 : 10, + verticalAlign: !horiz && isBand && 'middle', + y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4, + rotation: horiz && !isBand && 90 + }, optionsLabel); - // Handle reversed axis - if (axis.reversed) { - sign *= -1; - cvsOffset -= sign * (axis.sector || axis.len); - } + // add the SVG element + if (!label) { + attribs = { + align: optionsLabel.textAlign || optionsLabel.align, + rotation: optionsLabel.rotation + }; + if (defined(zIndex)) { + attribs.zIndex = zIndex; + } + plotLine.label = label = renderer.text( + optionsLabel.text, + 0, + 0, + optionsLabel.useHTML + ) + .attr(attribs) + .css(optionsLabel.style) + .add(); + } - // From pixels to value - if (backwards) { // reverse translation + // get the bounding box and align the label + // #3000 changed to better handle choice between plotband or plotline + xs = [path[1], path[4], (isBand ? path[6] : path[1])]; + ys = [path[2], path[5], (isBand ? path[7] : path[2])]; + x = arrayMin(xs); + y = arrayMin(ys); - val = val * sign + cvsOffset; - val -= minPixelPadding; - returnValue = val / localA + localMin; // from chart pixel to value - if (doPostTranslate) { // log and ordinal axes - returnValue = axis.lin2val(returnValue); - } + label.align(optionsLabel, false, { + x: x, + y: y, + width: arrayMax(xs) - x, + height: arrayMax(ys) - y + }); + label.show(); - // 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); - } + } else if (label) { // move out of sight + label.hide(); + } - return returnValue; - }, + // chainable + return plotLine; + }, - /** - * Utility method to translate an axis value to pixel position. - * @param {Number} value A value in terms of axis units - * @param {Boolean} paneCoordinates Whether to return the pixel coordinate relative to the chart - * or just the axis/pane itself. - */ - toPixels: function (value, paneCoordinates) { - return this.translate(value, false, !this.horiz, null, true) + (paneCoordinates ? 0 : this.pos); - }, + /** + * Remove the plot line or band + */ + destroy: function () { + // remove it from the lookup + erase(this.axis.plotLinesAndBands, this); - /* - * Utility method to translate a pixel position in to an axis value - * @param {Number} pixel The pixel value coordinate - * @param {Boolean} paneCoordiantes Whether the input pixel is relative to the chart or just the - * axis/pane itself. - */ - toValue: function (pixel, paneCoordinates) { - return this.translate(pixel - (paneCoordinates ? 0 : this.pos), true, !this.horiz, null, true); - }, + delete this.axis; + destroyObjectProperties(this); + } + }; - /** - * Create the path for a plot line that goes from the given value on - * this axis, across the plot to the opposite side - * @param {Number} value - * @param {Number} lineWidth Used for calculation crisp line - * @param {Number] old Use old coordinates (for resizing and rescaling) - */ - getPlotLinePath: function (value, lineWidth, old, force, translatedValue) { - var axis = this, - chart = axis.chart, - axisLeft = axis.left, - axisTop = axis.top, - x1, - y1, - x2, - y2, - cHeight = (old && chart.oldChartHeight) || chart.chartHeight, - cWidth = (old && chart.oldChartWidth) || chart.chartWidth, - skip, - transB = axis.transB, - /** - * Check if x is between a and b. If not, either move to a/b or skip, - * depending on the force parameter. - */ - between = function (x, a, b) { - if (x < a || x > b) { - if (force) { - x = mathMin(mathMax(a, x), b); - } else { - skip = true; - } - } - return x; - }; + /** + * Object with members for extending the Axis prototype + */ - translatedValue = pick(translatedValue, axis.translate(value, null, null, old)); - x1 = x2 = mathRound(translatedValue + transB); - y1 = y2 = mathRound(cHeight - translatedValue - transB); + AxisPlotLineOrBandExtension = { - if (isNaN(translatedValue)) { // no min or max - skip = true; + /** + * Create the path for a plot band + */ + getPlotBandPath: function (from, to) { + var toPath = this.getPlotLinePath(to, null, null, true), + path = this.getPlotLinePath(from, null, null, true); - } else if (axis.horiz) { - y1 = axisTop; - y2 = cHeight - axis.bottom; - x1 = x2 = between(x1, axisLeft, axisLeft + axis.width); - } else { - x1 = axisLeft; - x2 = cWidth - axis.right; - y1 = y2 = between(y1, axisTop, axisTop + axis.height); - } - return skip && !force ? - null : - chart.renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 1); - }, + if (path && toPath) { - /** - * Set the tick positions of a linear axis to round values like whole tens or every five. - */ - getLinearTickPositions: function (tickInterval, min, max) { - var pos, - lastPos, - roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval), - roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval), - tickPositions = []; + // Flat paths don't need labels (#3836) + path.flat = path.toString() === toPath.toString(); - // For single points, add a tick regardless of the relative position (#2662) - if (min === max && isNumber(min)) { - return [min]; - } + path.push( + toPath[4], + toPath[5], + toPath[1], + toPath[2] + ); + } else { // outside the axis area + path = null; + } - // Populate the intermediate values - pos = roundedMin; - while (pos <= roundedMax) { + return path; + }, - // Place the tick on the rounded value - tickPositions.push(pos); + addPlotBand: function (options) { + return this.addPlotBandOrLine(options, 'plotBands'); + }, - // Always add the raw tickInterval, not the corrected one. - pos = correctFloat(pos + tickInterval); + addPlotLine: function (options) { + return this.addPlotBandOrLine(options, 'plotLines'); + }, - // If the interval is not big enough in the current min - max range to actually increase - // the loop variable, we need to break out to prevent endless loop. Issue #619 - if (pos === lastPos) { - break; - } + /** + * Add a plot band or plot line after render time + * + * @param options {Object} The plotBand or plotLine configuration object + */ + addPlotBandOrLine: function (options, coll) { + var obj = new Highcharts.PlotLineOrBand(this, options).render(), + userOptions = this.userOptions; - // Record the last value - lastPos = pos; - } - return tickPositions; - }, + if (obj) { // #2189 + // Add it to the user options for exporting and Axis.update + if (coll) { + userOptions[coll] = userOptions[coll] || []; + userOptions[coll].push(options); + } + this.plotLinesAndBands.push(obj); + } - /** - * Return the minor tick positions. For logarithmic axes, reuse the same logic - * as for major ticks. - */ - getMinorTickPositions: function () { - var axis = this, - options = axis.options, - tickPositions = axis.tickPositions, - minorTickInterval = axis.minorTickInterval, - minorTickPositions = [], - pos, - i, - pointRangePadding = axis.pointRangePadding || 0, - min = axis.min - pointRangePadding, // #1498 - max = axis.max + pointRangePadding, // #1498 - range = max - min, - len; + return obj; + }, - // If minor ticks get too dense, they are hard to read, and may cause long running script. So we don't draw them. - if (range && range / minorTickInterval < axis.len / 3) { // #3875 + /** + * Remove a plot band or plot line from the chart by id + * @param {Object} id + */ + removePlotBandOrLine: function (id) { + var plotLinesAndBands = this.plotLinesAndBands, + options = this.options, + userOptions = this.userOptions, + i = plotLinesAndBands.length; + while (i--) { + if (plotLinesAndBands[i].id === id) { + plotLinesAndBands[i].destroy(); + } + } + each([options.plotLines || [], userOptions.plotLines || [], options.plotBands || [], userOptions.plotBands || []], function (arr) { + i = arr.length; + while (i--) { + if (arr[i].id === id) { + erase(arr, arr[i]); + } + } + }); + } + }; - if (axis.isLog) { - len = tickPositions.length; - for (i = 1; i < len; i++) { - minorTickPositions = minorTickPositions.concat( - axis.getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true) - ); - } - } else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314 - minorTickPositions = minorTickPositions.concat( - axis.getTimeTicks( - axis.normalizeTimeTickInterval(minorTickInterval), - min, - max, - options.startOfWeek - ) - ); - } else { - for (pos = min + (tickPositions[0] - min) % minorTickInterval; pos <= max; pos += minorTickInterval) { - minorTickPositions.push(pos); - } - } - } + /** + * Create a new axis object + * @param {Object} chart + * @param {Object} options + */ + var Axis = Highcharts.Axis = function () { + this.init.apply(this, arguments); + }; - if(minorTickPositions.length !== 0) { // don't change the extremes, when there is no minor ticks - axis.trimTicks(minorTickPositions, options.startOnTick, options.endOnTick); // #3652 #3743 #1498 - } - return minorTickPositions; - }, + Axis.prototype = { - /** - * Adjust the min and max for the minimum range. Keep in mind that the series data is - * not yet processed, so we don't have information on data cropping and grouping, or - * updated axis.pointRange or series.pointRange. The data can't be processed until - * we have finally established min and max. - */ - adjustForMinRange: function () { - var axis = this, - options = axis.options, - min = axis.min, - max = axis.max, - zoomOffset, - spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange, - closestDataRange, - i, - distance, - xData, - loopLength, - minArgs, - maxArgs, - minRange; + /** + * Default options for the X axis - the Y axis has extended defaults + */ + defaultOptions: { + // allowDecimals: null, + // alternateGridColor: null, + // categories: [], + dateTimeLabelFormats: { + millisecond: '%H:%M:%S.%L', + second: '%H:%M:%S', + minute: '%H:%M', + hour: '%H:%M', + day: '%e. %b', + week: '%e. %b', + month: '%b \'%y', + year: '%Y' + }, + endOnTick: false, + gridLineColor: '#D8D8D8', + // gridLineDashStyle: 'solid', + // gridLineWidth: 0, + // reversed: false, - // Set the automatic minimum range based on the closest point distance - if (axis.isXAxis && axis.minRange === UNDEFINED && !axis.isLog) { + labels: { + enabled: true, + // rotation: 0, + // align: 'center', + // step: null, + style: { + color: '#606060', + cursor: 'default', + fontSize: '11px' + }, + x: 0, + y: 15 + /*formatter: function () { + return this.value; + },*/ + }, + lineColor: '#C0D0E0', + lineWidth: 1, + //linkedTo: null, + //max: undefined, + //min: undefined, + minPadding: 0.01, + maxPadding: 0.01, + //minRange: null, + minorGridLineColor: '#E0E0E0', + // minorGridLineDashStyle: null, + minorGridLineWidth: 1, + minorTickColor: '#A0A0A0', + //minorTickInterval: null, + minorTickLength: 2, + minorTickPosition: 'outside', // inside or outside + //minorTickWidth: 0, + //opposite: false, + //offset: 0, + //plotBands: [{ + // events: {}, + // zIndex: 1, + // labels: { align, x, verticalAlign, y, style, rotation, textAlign } + //}], + //plotLines: [{ + // events: {} + // dashStyle: {} + // zIndex: + // labels: { align, x, verticalAlign, y, style, rotation, textAlign } + //}], + //reversed: false, + // showFirstLabel: true, + // showLastLabel: true, + startOfWeek: 1, + startOnTick: false, + tickColor: '#C0D0E0', + //tickInterval: null, + tickLength: 10, + tickmarkPlacement: 'between', // on or between + tickPixelInterval: 100, + tickPosition: 'outside', + //tickWidth: 1, + title: { + //text: null, + align: 'middle', // low, middle or high + //margin: 0 for horizontal, 10 for vertical axes, + //rotation: 0, + //side: 'outside', + style: { + color: '#707070' + } + //x: 0, + //y: 0 + }, + type: 'linear' // linear, logarithmic or datetime + //visible: true + }, - if (defined(options.min) || defined(options.max)) { - axis.minRange = null; // don't do this again + /** + * This options set extends the defaultOptions for Y axes + */ + defaultYAxisOptions: { + endOnTick: true, + gridLineWidth: 1, + tickPixelInterval: 72, + showLastLabel: true, + labels: { + x: -8, + y: 3 + }, + lineWidth: 0, + maxPadding: 0.05, + minPadding: 0.05, + startOnTick: true, + //tickWidth: 0, + title: { + rotation: 270, + text: 'Values' + }, + stackLabels: { + enabled: false, + //align: dynamic, + //y: dynamic, + //x: dynamic, + //verticalAlign: dynamic, + //textAlign: dynamic, + //rotation: 0, + formatter: function () { + return Highcharts.numberFormat(this.total, -1); + }, + style: merge(defaultPlotOptions.line.dataLabels.style, { color: '#000000' }) + } + }, - } else { + /** + * These options extend the defaultOptions for left axes + */ + defaultLeftAxisOptions: { + labels: { + x: -15, + y: null + }, + title: { + rotation: 270 + } + }, - // Find the closest distance between raw data points, as opposed to - // closestPointRange that applies to processed points (cropped and grouped) - each(axis.series, function (series) { - xData = series.xData; - loopLength = series.xIncrement ? 1 : xData.length - 1; - for (i = loopLength; i > 0; i--) { - distance = xData[i] - xData[i - 1]; - if (closestDataRange === UNDEFINED || distance < closestDataRange) { - closestDataRange = distance; - } - } - }); - axis.minRange = mathMin(closestDataRange * 5, axis.dataMax - axis.dataMin); - } - } + /** + * These options extend the defaultOptions for right axes + */ + defaultRightAxisOptions: { + labels: { + x: 15, + y: null + }, + title: { + rotation: 90 + } + }, - // if minRange is exceeded, adjust - if (max - min < axis.minRange) { - minRange = axis.minRange; - zoomOffset = (minRange - max + min) / 2; + /** + * These options extend the defaultOptions for bottom axes + */ + defaultBottomAxisOptions: { + labels: { + autoRotation: [-45], + x: 0, + y: null // based on font size + // overflow: undefined, + // staggerLines: null + }, + title: { + rotation: 0 + } + }, + /** + * These options extend the defaultOptions for top axes + */ + defaultTopAxisOptions: { + labels: { + autoRotation: [-45], + x: 0, + y: -15 + // overflow: undefined + // staggerLines: null + }, + title: { + rotation: 0 + } + }, - // if min and max options have been set, don't go beyond it - minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)]; - if (spaceAvailable) { // if space is available, stay within the data range - minArgs[2] = axis.dataMin; - } - min = arrayMax(minArgs); + /** + * Initialize the axis + */ + init: function (chart, userOptions) { - maxArgs = [min + minRange, pick(options.max, min + minRange)]; - if (spaceAvailable) { // if space is availabe, stay within the data range - maxArgs[2] = axis.dataMax; - } - max = arrayMin(maxArgs); + var isXAxis = userOptions.isX, + axis = this; - // now if the max is adjusted, adjust the min back - if (max - min < minRange) { - minArgs[0] = max - minRange; - minArgs[1] = pick(options.min, max - minRange); - min = arrayMax(minArgs); - } - } + axis.chart = chart; - // Record modified extremes - axis.min = min; - axis.max = max; - }, + // Flag, is the axis horizontal + axis.horiz = chart.inverted ? !isXAxis : isXAxis; - /** - * Update translation information - */ - setAxisTranslation: function (saveOld) { - var axis = this, - range = axis.max - axis.min, - pointRange = axis.axisPointRange || 0, - closestPointRange, - minPointOffset = 0, - pointRangePadding = 0, - linkedParent = axis.linkedParent, - ordinalCorrection, - hasCategories = !!axis.categories, - transA = axis.transA, - isXAxis = axis.isXAxis; + // Flag, isXAxis + axis.isXAxis = isXAxis; + axis.coll = isXAxis ? 'xAxis' : 'yAxis'; - // Adjust translation for padding. Y axis with categories need to go through the same (#1784). - if (isXAxis || hasCategories || pointRange) { - if (linkedParent) { - minPointOffset = linkedParent.minPointOffset; - pointRangePadding = linkedParent.pointRangePadding; + axis.opposite = userOptions.opposite; // needed in setOptions + axis.side = userOptions.side || (axis.horiz ? + (axis.opposite ? 0 : 2) : // top : bottom + (axis.opposite ? 1 : 3)); // right : left - } else { - each(axis.series, function (series) { - var seriesPointRange = hasCategories ? 1 : (isXAxis ? series.pointRange : (axis.axisPointRange || 0)), // #2806 - pointPlacement = series.options.pointPlacement, - seriesClosestPointRange = series.closestPointRange; + axis.setOptions(userOptions); - pointRange = mathMax(pointRange, seriesPointRange); - if (!axis.single) { - // minPointOffset is the value padding to the left of the axis in order to make - // room for points with a pointRange, typically columns. When the pointPlacement option - // is 'between' or 'on', this padding does not apply. - minPointOffset = mathMax( - minPointOffset, - isString(pointPlacement) ? 0 : seriesPointRange / 2 - ); + var options = this.options, + type = options.type, + isDatetimeAxis = type === 'datetime'; - // Determine the total padding needed to the length of the axis to make room for the - // pointRange. If the series' pointPlacement is 'on', no padding is added. - pointRangePadding = mathMax( - pointRangePadding, - pointPlacement === 'on' ? 0 : seriesPointRange - ); - } + axis.labelFormatter = options.labels.formatter || axis.defaultLabelFormatter; // can be overwritten by dynamic format - // Set the closestPointRange - if (!series.noSharedTooltip && defined(seriesClosestPointRange)) { - closestPointRange = defined(closestPointRange) ? - mathMin(closestPointRange, seriesClosestPointRange) : - seriesClosestPointRange; - } - }); - } - // Record minPointOffset and pointRangePadding - ordinalCorrection = axis.ordinalSlope && closestPointRange ? axis.ordinalSlope / closestPointRange : 1; // #988, #1853 - axis.minPointOffset = minPointOffset = minPointOffset * ordinalCorrection; - axis.pointRangePadding = pointRangePadding = pointRangePadding * ordinalCorrection; + // Flag, stagger lines or not + axis.userOptions = userOptions; - // pointRange means the width reserved for each point, like in a column chart - axis.pointRange = mathMin(pointRange, range); + //axis.axisTitleMargin = UNDEFINED,// = options.title.margin, + axis.minPixelPadding = 0; - // closestPointRange means the closest distance between points. In columns - // it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange - // is some other value - if (isXAxis) { - axis.closestPointRange = closestPointRange; - } - } + axis.reversed = options.reversed; + axis.visible = options.visible !== false; + axis.zoomEnabled = options.zoomEnabled !== false; - // Secondary values - if (saveOld) { - axis.oldTransA = transA; - } - axis.translationSlope = axis.transA = transA = axis.len / ((range + pointRangePadding) || 1); - axis.transB = axis.horiz ? axis.left : axis.bottom; // translation addend - axis.minPixelPadding = transA * minPointOffset; - }, + // Initial categories + axis.categories = options.categories || type === 'category'; + axis.names = axis.names || []; // Preserve on update (#3830) - minFromRange: function () { - return this.max - this.range; - }, + // Elements + //axis.axisGroup = UNDEFINED; + //axis.gridGroup = UNDEFINED; + //axis.axisTitle = UNDEFINED; + //axis.axisLine = UNDEFINED; - /** - * Set the tick positions to round values and optionally extend the extremes - * to the nearest tick - */ - setTickInterval: function (secondPass) { - var axis = this, - chart = axis.chart, - options = axis.options, - isLog = axis.isLog, - isDatetimeAxis = axis.isDatetimeAxis, - isXAxis = axis.isXAxis, - isLinked = axis.isLinked, - maxPadding = options.maxPadding, - minPadding = options.minPadding, - length, - linkedParentExtremes, - tickIntervalOption = options.tickInterval, - minTickInterval, - tickPixelIntervalOption = options.tickPixelInterval, - categories = axis.categories, - threshold = axis.threshold, - softThreshold = axis.softThreshold, - thresholdMin, - thresholdMax, - hardMin, - hardMax; + // Shorthand types + axis.isLog = type === 'logarithmic'; + axis.isDatetimeAxis = isDatetimeAxis; - if (!isDatetimeAxis && !categories && !isLinked) { - this.getTickAmount(); - } + // Flag, if axis is linked to another axis + axis.isLinked = defined(options.linkedTo); + // Linked axis. + //axis.linkedParent = UNDEFINED; - // Min or max set either by zooming/setExtremes or initial options - hardMin = pick(axis.userMin, options.min); - hardMax = pick(axis.userMax, options.max); + // Tick positions + //axis.tickPositions = UNDEFINED; // array containing predefined positions + // Tick intervals + //axis.tickInterval = UNDEFINED; + //axis.minorTickInterval = UNDEFINED; - // Linked axis gets the extremes from the parent axis - if (isLinked) { - axis.linkedParent = chart[axis.coll][options.linkedTo]; - linkedParentExtremes = axis.linkedParent.getExtremes(); - axis.min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin); - axis.max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax); - if (options.type !== axis.linkedParent.options.type) { - error(11, 1); // Can't link axes of different type - } - // Initial min and max from the extreme data values - } else { + // Major ticks + axis.ticks = {}; + axis.labelEdge = []; + // Minor ticks + axis.minorTicks = {}; - // Adjust to hard threshold - if (!softThreshold && defined(threshold)) { - if (axis.dataMin >= threshold) { - thresholdMin = threshold; - minPadding = 0; - } else if (axis.dataMax <= threshold) { - thresholdMax = threshold; - maxPadding = 0; - } - } + // List of plotLines/Bands + axis.plotLinesAndBands = []; - axis.min = pick(hardMin, thresholdMin, axis.dataMin); - axis.max = pick(hardMax, thresholdMax, axis.dataMax); + // Alternate bands + axis.alternateBands = {}; - } + // Axis metrics + //axis.left = UNDEFINED; + //axis.top = UNDEFINED; + //axis.width = UNDEFINED; + //axis.height = UNDEFINED; + //axis.bottom = UNDEFINED; + //axis.right = UNDEFINED; + //axis.transA = UNDEFINED; + //axis.transB = UNDEFINED; + //axis.oldTransA = UNDEFINED; + axis.len = 0; + //axis.oldMin = UNDEFINED; + //axis.oldMax = UNDEFINED; + //axis.oldUserMin = UNDEFINED; + //axis.oldUserMax = UNDEFINED; + //axis.oldAxisLength = UNDEFINED; + axis.minRange = axis.userMinRange = options.minRange || options.maxZoom; + axis.range = options.range; + axis.offset = options.offset || 0; - if (isLog) { - if (!secondPass && mathMin(axis.min, pick(axis.dataMin, axis.min)) <= 0) { // #978 - error(10, 1); // Can't plot negative values on log axis - } - // The correctFloat cures #934, float errors on full tens. But it - // was too aggressive for #4360 because of conversion back to lin, - // therefore use precision 15. - axis.min = correctFloat(log2lin(axis.min), 15); - axis.max = correctFloat(log2lin(axis.max), 15); - } - // handle zoomed range - if (axis.range && defined(axis.max)) { - axis.userMin = axis.min = hardMin = mathMax(axis.min, axis.minFromRange()); // #618 - axis.userMax = hardMax = axis.max; + // Dictionary for stacks + axis.stacks = {}; + axis.oldStacks = {}; + axis.stacksTouched = 0; - axis.range = null; // don't use it when running setExtremes - } + // Min and max in the data + //axis.dataMin = UNDEFINED, + //axis.dataMax = UNDEFINED, - // Hook for adjusting this.min and this.max. Used by bubble series. - if (axis.beforePadding) { - axis.beforePadding(); - } + // The axis range + axis.max = null; + axis.min = null; - // adjust min and max for the minimum range - axis.adjustForMinRange(); + // User set min and max + //axis.userMin = UNDEFINED, + //axis.userMax = UNDEFINED, - // Pad the values to get clear of the chart's edges. To avoid tickInterval taking the padding - // into account, we do this after computing tick interval (#1337). - if (!categories && !axis.axisPointRange && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) { - length = axis.max - axis.min; - if (length) { - if (!defined(hardMin) && minPadding) { - axis.min -= length * minPadding; - } - if (!defined(hardMax) && maxPadding) { - axis.max += length * maxPadding; - } - } - } + // Crosshair options + axis.crosshair = pick(options.crosshair, splat(chart.options.tooltip.crosshairs)[isXAxis ? 0 : 1], false); + // Run Axis - // Stay within floor and ceiling - if (isNumber(options.floor)) { - axis.min = mathMax(axis.min, options.floor); - } - if (isNumber(options.ceiling)) { - axis.max = mathMin(axis.max, options.ceiling); - } + var eventType, + events = axis.options.events; - // 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 - // default minPadding and startOnTick options. This is prevented by the softThreshold - // option. - if (softThreshold && defined(axis.dataMin)) { - threshold = threshold || 0; - if (!defined(hardMin) && axis.min < threshold && axis.dataMin >= threshold) { - axis.min = threshold; - } else if (!defined(hardMax) && axis.max > threshold && axis.dataMax <= threshold) { - axis.max = threshold; - } - } + // Register + if (inArray(axis, chart.axes) === -1) { // don't add it again on Axis.update() + if (isXAxis && !this.isColorAxis) { // #2713 + chart.axes.splice(chart.xAxis.length, 0, axis); + } else { + chart.axes.push(axis); + } + chart[axis.coll].push(axis); + } - // get tickInterval - if (axis.min === axis.max || axis.min === undefined || axis.max === undefined) { - axis.tickInterval = 1; - } else if (isLinked && !tickIntervalOption && - tickPixelIntervalOption === axis.linkedParent.options.tickPixelInterval) { - axis.tickInterval = tickIntervalOption = axis.linkedParent.tickInterval; - } else { - axis.tickInterval = pick( - tickIntervalOption, - this.tickAmount ? ((axis.max - axis.min) / mathMax(this.tickAmount - 1, 1)) : undefined, - categories ? // for categoried axis, 1 is default, for linear axis use tickPix - 1 : - // don't let it be more than the data range - (axis.max - axis.min) * tickPixelIntervalOption / mathMax(axis.len, tickPixelIntervalOption) - ); - } + axis.series = axis.series || []; // populated by Series - // Now we're finished detecting min and max, crop and group series data. This - // is in turn needed in order to find tick positions in ordinal axes. - if (isXAxis && !secondPass) { - each(axis.series, function (series) { - series.processData(axis.min !== axis.oldMin || axis.max !== axis.oldMax); - }); - } + // inverted charts have reversed xAxes as default + if (chart.inverted && isXAxis && axis.reversed === UNDEFINED) { + axis.reversed = true; + } - // set the translation factor used in translate function - axis.setAxisTranslation(true); + axis.removePlotBand = axis.removePlotBandOrLine; + axis.removePlotLine = axis.removePlotBandOrLine; - // hook for ordinal axes and radial axes - if (axis.beforeSetTickPositions) { - axis.beforeSetTickPositions(); - } - // hook for extensions, used in Highstock ordinal axes - if (axis.postProcessTickInterval) { - axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval); - } + // register event listeners + for (eventType in events) { + addEvent(axis, eventType, events[eventType]); + } - // In column-like charts, don't cramp in more ticks than there are points (#1943) - if (axis.pointRange) { - axis.tickInterval = mathMax(axis.pointRange, axis.tickInterval); - } + // extend logarithmic axis + if (axis.isLog) { + axis.val2lin = log2lin; + axis.lin2val = lin2log; + } + }, - // Before normalizing the tick interval, handle minimum tick interval. This applies only if tickInterval is not defined. - minTickInterval = pick(options.minTickInterval, axis.isDatetimeAxis && axis.closestPointRange); - if (!tickIntervalOption && axis.tickInterval < minTickInterval) { - axis.tickInterval = minTickInterval; - } + /** + * Merge and set options + */ + setOptions: function (userOptions) { + this.options = merge( + this.defaultOptions, + this.isXAxis ? {} : this.defaultYAxisOptions, + [this.defaultTopAxisOptions, this.defaultRightAxisOptions, + this.defaultBottomAxisOptions, this.defaultLeftAxisOptions][this.side], + merge( + defaultOptions[this.coll], // if set in setOptions (#1053) + userOptions + ) + ); + }, - // for linear axes, get magnitude and normalize the interval - if (!isDatetimeAxis && !isLog && !tickIntervalOption) { - axis.tickInterval = normalizeTickInterval( - axis.tickInterval, - null, - getMagnitude(axis.tickInterval), - // If the tick interval is between 0.5 and 5 and the axis max is in the order of - // thousands, chances are we are dealing with years. Don't allow decimals. #3363. - pick(options.allowDecimals, !(axis.tickInterval > 0.5 && axis.tickInterval < 5 && axis.max > 1000 && axis.max < 9999)), - !!this.tickAmount - ); - } - - // Prevent ticks from getting so close that we can't draw the labels - if (!this.tickAmount && this.len) { // Color axis with disabled legend has no length - axis.tickInterval = axis.unsquish(); - } + /** + * The default label formatter. The context is a special config object for the label. + */ + defaultLabelFormatter: function () { + var axis = this.axis, + value = this.value, + categories = axis.categories, + dateTimeLabelFormat = this.dateTimeLabelFormat, + numericSymbols = defaultOptions.lang.numericSymbols, + i = numericSymbols && numericSymbols.length, + multi, + ret, + formatOption = axis.options.labels.format, - this.setTickPositions(); - }, + // make sure the same symbol is added for all labels on a linear axis + numericSymbolDetector = axis.isLog ? value : axis.tickInterval; - /** - * Now we have computed the normalized tickInterval, get the tick positions - */ - setTickPositions: function () { + if (formatOption) { + ret = format(formatOption, this); - var options = this.options, - tickPositions, - tickPositionsOption = options.tickPositions, - tickPositioner = options.tickPositioner, - startOnTick = options.startOnTick, - endOnTick = options.endOnTick, - single; + } else if (categories) { + ret = value; - // Set the tickmarkOffset - this.tickmarkOffset = (this.categories && options.tickmarkPlacement === 'between' && - this.tickInterval === 1) ? 0.5 : 0; // #3202 + } else if (dateTimeLabelFormat) { // datetime axis + ret = dateFormat(dateTimeLabelFormat, value); + } 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); + if (numericSymbolDetector >= multi && (value * 10) % multi === 0 && numericSymbols[i] !== null) { + ret = Highcharts.numberFormat(value / multi, -1) + numericSymbols[i]; + } + } + } - // get minorTickInterval - this.minorTickInterval = options.minorTickInterval === 'auto' && this.tickInterval ? - this.tickInterval / 5 : options.minorTickInterval; + if (ret === UNDEFINED) { + if (mathAbs(value) >= 10000) { // add thousands separators + ret = Highcharts.numberFormat(value, -1); - // Find the tick positions - this.tickPositions = tickPositions = tickPositionsOption && tickPositionsOption.slice(); // Work on a copy (#1565) - if (!tickPositions) { + } else { // small numbers + ret = Highcharts.numberFormat(value, -1, UNDEFINED, ''); // #2466 + } + } - if (this.isDatetimeAxis) { - tickPositions = this.getTimeTicks( - this.normalizeTimeTickInterval(this.tickInterval, options.units), - this.min, - this.max, - options.startOfWeek, - this.ordinalPositions, - this.closestPointRange, - true - ); - } else if (this.isLog) { - tickPositions = this.getLogTickPositions(this.tickInterval, this.min, this.max); - } else { - tickPositions = this.getLinearTickPositions(this.tickInterval, this.min, this.max); - } + return ret; + }, - // Too dense ticks, keep only the first and last (#4477) - if (tickPositions.length > this.len) { - tickPositions = [tickPositions[0], tickPositions.pop()]; - } + /** + * Get the minimum and maximum for the series of each axis + */ + getSeriesExtremes: function () { + var axis = this, + chart = axis.chart; - this.tickPositions = tickPositions; + axis.hasVisibleSeries = false; - // Run the tick positioner callback, that allows modifying auto tick positions. - if (tickPositioner) { - tickPositioner = tickPositioner.apply(this, [this.min, this.max]); - if (tickPositioner) { - this.tickPositions = tickPositions = tickPositioner; - } - } + // Reset properties in case we're redrawing (#3353) + axis.dataMin = axis.dataMax = axis.threshold = null; + axis.softThreshold = !axis.isXAxis; - } + if (axis.buildStacks) { + axis.buildStacks(); + } - if (!this.isLinked) { + // loop through this axis' series + each(axis.series, function (series) { - // reset min/max or remove extremes based on start/end on tick - this.trimTicks(tickPositions, startOnTick, endOnTick); + if (series.visible || !chart.options.chart.ignoreHiddenSeries) { - // When there is only one point, or all points have the same value on this axis, then min - // and max are equal and tickPositions.length is 0 or 1. In this case, add some padding - // in order to center the point, but leave it with one tick. #1337. - if (this.min === this.max && defined(this.min) && !this.tickAmount) { - // Substract half a unit (#2619, #2846, #2515, #3390) - single = true; - this.min -= 0.5; - this.max += 0.5; - } - this.single = single; + var seriesOptions = series.options, + xData, + threshold = seriesOptions.threshold, + seriesDataMin, + seriesDataMax; - if (!tickPositionsOption && !tickPositioner) { - this.adjustTickAmount(); - } - } - }, + axis.hasVisibleSeries = true; - /** - * Handle startOnTick and endOnTick by either adapting to padding min/max or rounded min/max - */ - trimTicks: function (tickPositions, startOnTick, endOnTick) { - var roundedMin = tickPositions[0], - roundedMax = tickPositions[tickPositions.length - 1], - minPointOffset = this.minPointOffset || 0; - - if (startOnTick) { - this.min = roundedMin; - } else if (this.min - minPointOffset > roundedMin) { - tickPositions.shift(); - } + // Validate threshold in logarithmic axes + if (axis.isLog && threshold <= 0) { + threshold = null; + } - if (endOnTick) { - this.max = roundedMax; - } else if (this.max + minPointOffset < roundedMax) { - tickPositions.pop(); - } + // Get dataMin and dataMax for X axes + if (axis.isXAxis) { + xData = series.xData; + if (xData.length) { + axis.dataMin = mathMin(pick(axis.dataMin, xData[0]), arrayMin(xData)); + axis.dataMax = mathMax(pick(axis.dataMax, xData[0]), arrayMax(xData)); + } - // If no tick are left, set one tick in the middle (#3195) - if (tickPositions.length === 0 && defined(roundedMin)) { - tickPositions.push((roundedMax + roundedMin) / 2); - } - }, + // Get dataMin and dataMax for Y axes, as well as handle stacking and processed data + } else { - /** - * Set the max ticks of either the x and y axis collection - */ - getTickAmount: function () { - var others = {}, // Whether there is another axis to pair with this one - hasOther, - options = this.options, - tickAmount = options.tickAmount, - tickPixelInterval = options.tickPixelInterval; + // Get this particular series extremes + series.getExtremes(); + seriesDataMax = series.dataMax; + seriesDataMin = series.dataMin; - if (!defined(options.tickInterval) && this.len < tickPixelInterval && !this.isRadial && - !this.isLog && options.startOnTick && options.endOnTick) { - tickAmount = 2; - } + // Get the dataMin and dataMax so far. If percentage is used, the min and max are + // always 0 and 100. If seriesDataMin and seriesDataMax is null, then series + // doesn't have active y data, we continue with nulls + if (defined(seriesDataMin) && defined(seriesDataMax)) { + axis.dataMin = mathMin(pick(axis.dataMin, seriesDataMin), seriesDataMin); + axis.dataMax = mathMax(pick(axis.dataMax, seriesDataMax), seriesDataMax); + } - if (!tickAmount && this.chart.options.chart.alignTicks !== false && options.alignTicks !== false) { - // Check if there are multiple axes in the same pane - each(this.chart[this.coll], function (axis) { - var options = axis.options, - horiz = axis.horiz, - key = [horiz ? options.left : options.top, horiz ? options.width : options.height, options.pane].join(','); - - if (axis.series.length) { // #4442 - if (others[key]) { - hasOther = true; // #4201 - } else { - others[key] = 1; - } - } - }); + // Adjust to threshold + if (defined(threshold)) { + axis.threshold = threshold; + } + // If any series has a hard threshold, it takes precedence + if (!seriesOptions.softThreshold || axis.isLog) { + axis.softThreshold = false; + } + } + } + }); + }, - if (hasOther) { - // Add 1 because 4 tick intervals require 5 ticks (including first and last) - tickAmount = mathCeil(this.len / tickPixelInterval) + 1; - } - } + /** + * Translate from axis value to pixel position on the chart, or back + * + */ + translate: function (val, backwards, cvsCoord, old, handleLog, pointPlacement) { + var axis = this.linkedParent || this, // #1417 + sign = 1, + cvsOffset = 0, + localA = old ? axis.oldTransA : axis.transA, + localMin = old ? axis.oldMin : axis.min, + returnValue, + minPixelPadding = axis.minPixelPadding, + doPostTranslate = (axis.doPostTranslate || (axis.isLog && handleLog)) && axis.lin2val; - // For tick amounts of 2 and 3, compute five ticks and remove the intermediate ones. This - // prevents the axis from adding ticks that are too far away from the data extremes. - if (tickAmount < 4) { - this.finalTickAmt = tickAmount; - tickAmount = 5; - } - - this.tickAmount = tickAmount; - }, + if (!localA) { + localA = axis.transA; + } - /** - * When using multiple axes, adjust the number of ticks to match the highest - * number of ticks in that group - */ - adjustTickAmount: function () { - var tickInterval = this.tickInterval, - tickPositions = this.tickPositions, - tickAmount = this.tickAmount, - finalTickAmt = this.finalTickAmt, - currentTickAmount = tickPositions && tickPositions.length, - i, - len; + // In vertical axes, the canvas coordinates start from 0 at the top like in + // SVG. + if (cvsCoord) { + sign *= -1; // canvas coordinates inverts the value + cvsOffset = axis.len; + } - if (currentTickAmount < tickAmount) { // TODO: Check #3411 - while (tickPositions.length < tickAmount) { - tickPositions.push(correctFloat( - tickPositions[tickPositions.length - 1] + tickInterval - )); - } - this.transA *= (currentTickAmount - 1) / (tickAmount - 1); - this.max = tickPositions[tickPositions.length - 1]; + // Handle reversed axis + if (axis.reversed) { + sign *= -1; + cvsOffset -= sign * (axis.sector || axis.len); + } - // We have too many ticks, run second pass to try to reduce ticks - } else if (currentTickAmount > tickAmount) { - this.tickInterval *= 2; - this.setTickPositions(); - } + // From pixels to value + if (backwards) { // reverse translation - // The finalTickAmt property is set in getTickAmount - if (defined(finalTickAmt)) { - i = len = tickPositions.length; - while (i--) { - if ( - (finalTickAmt === 3 && i % 2 === 1) || // Remove every other tick - (finalTickAmt <= 2 && i > 0 && i < len - 1) // Remove all but first and last - ) { - tickPositions.splice(i, 1); - } - } - this.finalTickAmt = UNDEFINED; - } - }, + val = val * sign + cvsOffset; + val -= minPixelPadding; + returnValue = val / localA + localMin; // from chart pixel to value + if (doPostTranslate) { // log and ordinal axes + returnValue = axis.lin2val(returnValue); + } - /** - * Set the scale based on data min and max, user set min and max or options - * - */ - setScale: function () { - var axis = this, - isDirtyData, - isDirtyAxisLength; + // 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); + } - axis.oldMin = axis.min; - axis.oldMax = axis.max; - axis.oldAxisLength = axis.len; + return returnValue; + }, - // set the new axisLength - axis.setAxisSize(); - //axisLength = horiz ? axisWidth : axisHeight; - isDirtyAxisLength = axis.len !== axis.oldAxisLength; + /** + * Utility method to translate an axis value to pixel position. + * @param {Number} value A value in terms of axis units + * @param {Boolean} paneCoordinates Whether to return the pixel coordinate relative to the chart + * or just the axis/pane itself. + */ + toPixels: function (value, paneCoordinates) { + return this.translate(value, false, !this.horiz, null, true) + (paneCoordinates ? 0 : this.pos); + }, - // is there new data? - each(axis.series, function (series) { - if (series.isDirtyData || series.isDirty || - series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well - isDirtyData = true; - } - }); + /* + * Utility method to translate a pixel position in to an axis value + * @param {Number} pixel The pixel value coordinate + * @param {Boolean} paneCoordiantes Whether the input pixel is relative to the chart or just the + * axis/pane itself. + */ + toValue: function (pixel, paneCoordinates) { + return this.translate(pixel - (paneCoordinates ? 0 : this.pos), true, !this.horiz, null, true); + }, - // do we really need to go through all this? - if (isDirtyAxisLength || isDirtyData || axis.isLinked || axis.forceRedraw || - axis.userMin !== axis.oldUserMin || axis.userMax !== axis.oldUserMax) { + /** + * Create the path for a plot line that goes from the given value on + * this axis, across the plot to the opposite side + * @param {Number} value + * @param {Number} lineWidth Used for calculation crisp line + * @param {Number] old Use old coordinates (for resizing and rescaling) + */ + getPlotLinePath: function (value, lineWidth, old, force, translatedValue) { + var axis = this, + chart = axis.chart, + axisLeft = axis.left, + axisTop = axis.top, + x1, + y1, + x2, + y2, + cHeight = (old && chart.oldChartHeight) || chart.chartHeight, + cWidth = (old && chart.oldChartWidth) || chart.chartWidth, + skip, + transB = axis.transB, + /** + * Check if x is between a and b. If not, either move to a/b or skip, + * depending on the force parameter. + */ + between = function (x, a, b) { + if (x < a || x > b) { + if (force) { + x = mathMin(mathMax(a, x), b); + } else { + skip = true; + } + } + return x; + }; - if (axis.resetStacks) { - axis.resetStacks(); - } + translatedValue = pick(translatedValue, axis.translate(value, null, null, old)); + x1 = x2 = mathRound(translatedValue + transB); + y1 = y2 = mathRound(cHeight - translatedValue - transB); - axis.forceRedraw = false; + if (isNaN(translatedValue)) { // no min or max + skip = true; - // get data extremes if needed - axis.getSeriesExtremes(); + } else if (axis.horiz) { + y1 = axisTop; + y2 = cHeight - axis.bottom; + x1 = x2 = between(x1, axisLeft, axisLeft + axis.width); + } else { + x1 = axisLeft; + x2 = cWidth - axis.right; + y1 = y2 = between(y1, axisTop, axisTop + axis.height); + } + return skip && !force ? + null : + chart.renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 1); + }, - // get fixed positions based on tickInterval - axis.setTickInterval(); + /** + * Set the tick positions of a linear axis to round values like whole tens or every five. + */ + getLinearTickPositions: function (tickInterval, min, max) { + var pos, + lastPos, + roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval), + roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval), + tickPositions = []; - // record old values to decide whether a rescale is necessary later on (#540) - axis.oldUserMin = axis.userMin; - axis.oldUserMax = axis.userMax; + // For single points, add a tick regardless of the relative position (#2662) + if (min === max && isNumber(min)) { + return [min]; + } - // Mark as dirty if it is not already set to dirty and extremes have changed. #595. - if (!axis.isDirty) { - axis.isDirty = isDirtyAxisLength || axis.min !== axis.oldMin || axis.max !== axis.oldMax; - } - } else if (axis.cleanStacks) { - axis.cleanStacks(); - } - }, + // Populate the intermediate values + pos = roundedMin; + while (pos <= roundedMax) { - /** - * Set the extremes and optionally redraw - * @param {Number} newMin - * @param {Number} newMax - * @param {Boolean} redraw - * @param {Boolean|Object} animation Whether to apply animation, and optionally animation - * configuration - * @param {Object} eventArguments - * - */ - setExtremes: function (newMin, newMax, redraw, animation, eventArguments) { - var axis = this, - chart = axis.chart; + // Place the tick on the rounded value + tickPositions.push(pos); - redraw = pick(redraw, true); // defaults to true + // Always add the raw tickInterval, not the corrected one. + pos = correctFloat(pos + tickInterval); - each(axis.series, function (serie) { - delete serie.kdTree; - }); + // If the interval is not big enough in the current min - max range to actually increase + // the loop variable, we need to break out to prevent endless loop. Issue #619 + if (pos === lastPos) { + break; + } - // Extend the arguments with min and max - eventArguments = extend(eventArguments, { - min: newMin, - max: newMax - }); + // Record the last value + lastPos = pos; + } + return tickPositions; + }, - // Fire the event - fireEvent(axis, 'setExtremes', eventArguments, function () { // the default event handler + /** + * Return the minor tick positions. For logarithmic axes, reuse the same logic + * as for major ticks. + */ + getMinorTickPositions: function () { + var axis = this, + options = axis.options, + tickPositions = axis.tickPositions, + minorTickInterval = axis.minorTickInterval, + minorTickPositions = [], + pos, + i, + pointRangePadding = axis.pointRangePadding || 0, + min = axis.min - pointRangePadding, // #1498 + max = axis.max + pointRangePadding, // #1498 + range = max - min, + len; - axis.userMin = newMin; - axis.userMax = newMax; - axis.eventArgs = eventArguments; + // If minor ticks get too dense, they are hard to read, and may cause long running script. So we don't draw them. + if (range && range / minorTickInterval < axis.len / 3) { // #3875 - if (redraw) { - chart.redraw(animation); - } - }); - }, + if (axis.isLog) { + len = tickPositions.length; + for (i = 1; i < len; i++) { + minorTickPositions = minorTickPositions.concat( + axis.getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true) + ); + } + } else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314 + minorTickPositions = minorTickPositions.concat( + axis.getTimeTicks( + axis.normalizeTimeTickInterval(minorTickInterval), + min, + max, + options.startOfWeek + ) + ); + } else { + for (pos = min + (tickPositions[0] - min) % minorTickInterval; pos <= max; pos += minorTickInterval) { + minorTickPositions.push(pos); + } + } + } - /** - * Overridable method for zooming chart. Pulled out in a separate method to allow overriding - * in stock charts. - */ - zoom: function (newMin, newMax) { - var dataMin = this.dataMin, - dataMax = this.dataMax, - options = this.options, - min = mathMin(dataMin, pick(options.min, dataMin)), - max = mathMax(dataMax, pick(options.max, dataMax)); + if (minorTickPositions.length !== 0) { // don't change the extremes, when there is no minor ticks + axis.trimTicks(minorTickPositions, options.startOnTick, options.endOnTick); // #3652 #3743 #1498 + } + return minorTickPositions; + }, - // 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; - } - } + /** + * Adjust the min and max for the minimum range. Keep in mind that the series data is + * not yet processed, so we don't have information on data cropping and grouping, or + * updated axis.pointRange or series.pointRange. The data can't be processed until + * we have finally established min and max. + */ + adjustForMinRange: function () { + var axis = this, + options = axis.options, + min = axis.min, + max = axis.max, + zoomOffset, + spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange, + closestDataRange, + i, + distance, + xData, + loopLength, + minArgs, + maxArgs, + minRange; - // In full view, displaying the reset zoom button is not required - this.displayBtn = newMin !== UNDEFINED || newMax !== UNDEFINED; + // Set the automatic minimum range based on the closest point distance + if (axis.isXAxis && axis.minRange === UNDEFINED && !axis.isLog) { - // Do it - this.setExtremes( - newMin, - newMax, - false, - UNDEFINED, - { trigger: 'zoom' } - ); - return true; - }, + if (defined(options.min) || defined(options.max)) { + axis.minRange = null; // don't do this again - /** - * Update the axis metrics - */ - setAxisSize: function () { - var chart = this.chart, - options = this.options, - offsetLeft = options.offsetLeft || 0, - offsetRight = options.offsetRight || 0, - horiz = this.horiz, - width = pick(options.width, chart.plotWidth - offsetLeft + offsetRight), - height = pick(options.height, chart.plotHeight), - top = pick(options.top, chart.plotTop), - left = pick(options.left, chart.plotLeft + offsetLeft), - percentRegex = /%$/; + } else { - // Check for percentage based input values - if (percentRegex.test(height)) { - height = parseFloat(height) / 100 * chart.plotHeight; - } - if (percentRegex.test(top)) { - top = parseFloat(top) / 100 * chart.plotHeight + chart.plotTop; - } + // Find the closest distance between raw data points, as opposed to + // closestPointRange that applies to processed points (cropped and grouped) + each(axis.series, function (series) { + xData = series.xData; + loopLength = series.xIncrement ? 1 : xData.length - 1; + for (i = loopLength; i > 0; i--) { + distance = xData[i] - xData[i - 1]; + if (closestDataRange === UNDEFINED || distance < closestDataRange) { + closestDataRange = distance; + } + } + }); + axis.minRange = mathMin(closestDataRange * 5, axis.dataMax - axis.dataMin); + } + } - // Expose basic values to use in Series object and navigator - this.left = left; - this.top = top; - this.width = width; - this.height = height; - this.bottom = chart.chartHeight - height - top; - this.right = chart.chartWidth - width - left; + // if minRange is exceeded, adjust + if (max - min < axis.minRange) { + minRange = axis.minRange; + zoomOffset = (minRange - max + min) / 2; - // Direction agnostic properties - this.len = mathMax(horiz ? width : height, 0); // mathMax fixes #905 - this.pos = horiz ? left : top; // distance from SVG origin - }, + // if min and max options have been set, don't go beyond it + minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)]; + if (spaceAvailable) { // if space is available, stay within the data range + minArgs[2] = axis.dataMin; + } + min = arrayMax(minArgs); - /** - * Get the actual axis extremes - */ - getExtremes: function () { - var axis = this, - isLog = axis.isLog; + maxArgs = [min + minRange, pick(options.max, min + minRange)]; + if (spaceAvailable) { // if space is availabe, stay within the data range + maxArgs[2] = axis.dataMax; + } - return { - min: isLog ? correctFloat(lin2log(axis.min)) : axis.min, - max: isLog ? correctFloat(lin2log(axis.max)) : axis.max, - dataMin: axis.dataMin, - dataMax: axis.dataMax, - userMin: axis.userMin, - userMax: axis.userMax - }; - }, + max = arrayMin(maxArgs); - /** - * Get the zero plane either based on zero or on the min or max value. - * Used in bar and area plots - */ - getThreshold: function (threshold) { - var axis = this, - isLog = axis.isLog, - realMin = isLog ? lin2log(axis.min) : axis.min, - realMax = isLog ? lin2log(axis.max) : axis.max; + // now if the max is adjusted, adjust the min back + if (max - min < minRange) { + minArgs[0] = max - minRange; + minArgs[1] = pick(options.min, max - minRange); + min = arrayMax(minArgs); + } + } - // With a threshold of null, make the columns/areas rise from the top or bottom - // depending on the value, assuming an actual threshold of 0 (#4233). - if (threshold === null) { - threshold = realMax < 0 ? realMax : realMin; - } else if (realMin > threshold) { - threshold = realMin; - } else if (realMax < threshold) { - threshold = realMax; - } + // Record modified extremes + axis.min = min; + axis.max = max; + }, - return axis.translate(threshold, 0, 1, 0, 1); - }, + /** + * Update translation information + */ + setAxisTranslation: function (saveOld) { + var axis = this, + range = axis.max - axis.min, + pointRange = axis.axisPointRange || 0, + closestPointRange, + minPointOffset = 0, + pointRangePadding = 0, + linkedParent = axis.linkedParent, + ordinalCorrection, + hasCategories = !!axis.categories, + transA = axis.transA, + isXAxis = axis.isXAxis; - /** - * Compute auto alignment for the axis label based on which side the axis is on - * and the given rotation for the label - */ - autoLabelAlign: function (rotation) { - var ret, - angle = (pick(rotation, 0) - (this.side * 90) + 720) % 360; + // Adjust translation for padding. Y axis with categories need to go through the same (#1784). + if (isXAxis || hasCategories || pointRange) { + if (linkedParent) { + minPointOffset = linkedParent.minPointOffset; + pointRangePadding = linkedParent.pointRangePadding; - if (angle > 15 && angle < 165) { - ret = 'right'; - } else if (angle > 195 && angle < 345) { - ret = 'left'; - } else { - ret = 'center'; - } - return ret; - }, + } else { + // Find the closestPointRange across all series + each(axis.series, function (series) { + var seriesClosest = series.closestPointRange; + if (!series.noSharedTooltip && defined(seriesClosest)) { + closestPointRange = defined(closestPointRange) ? + mathMin(closestPointRange, seriesClosest) : + seriesClosest; + } + }); - /** - * Prevent the ticks from getting so close we can't draw the labels. On a horizontal - * axis, this is handled by rotating the labels, removing ticks and adding ellipsis. - * On a vertical axis remove ticks and add ellipsis. - */ - unsquish: function () { - var chart = this.chart, - ticks = this.ticks, - labelOptions = this.options.labels, - horiz = this.horiz, - tickInterval = this.tickInterval, - newTickInterval = tickInterval, - slotSize = this.len / (((this.categories ? 1 : 0) + this.max - this.min) / tickInterval), - rotation, - rotationOption = labelOptions.rotation, - labelMetrics = chart.renderer.fontMetrics(labelOptions.style.fontSize, ticks[0] && ticks[0].label), - step, - bestScore = Number.MAX_VALUE, - autoRotation, - // Return the multiple of tickInterval that is needed to avoid collision - getStep = function (spaceNeeded) { - var step = spaceNeeded / (slotSize || 1); - step = step > 1 ? mathCeil(step) : 1; - return step * tickInterval; - }; - - if (horiz) { - autoRotation = !labelOptions.staggerLines && !labelOptions.step && ( // #3971 - defined(rotationOption) ? - [rotationOption] : - slotSize < pick(labelOptions.autoRotationLimit, 80) && labelOptions.autoRotation - ); + each(axis.series, function (series) { + var seriesPointRange = hasCategories ? + 1 : + (isXAxis ? + pick(series.options.pointRange, closestPointRange, 0) : + (axis.axisPointRange || 0)), // #2806 + pointPlacement = series.options.pointPlacement; - if (autoRotation) { + pointRange = mathMax(pointRange, seriesPointRange); - // Loop over the given autoRotation options, and determine which gives the best score. The - // best score is that with the lowest number of steps and a rotation closest to horizontal. - each(autoRotation, function (rot) { - var score; + if (!axis.single) { + // minPointOffset is the value padding to the left of the axis in order to make + // room for points with a pointRange, typically columns. When the pointPlacement option + // is 'between' or 'on', this padding does not apply. + minPointOffset = mathMax( + minPointOffset, + isString(pointPlacement) ? 0 : seriesPointRange / 2 + ); - if (rot === rotationOption || (rot && rot >= -90 && rot <= 90)) { // #3891 - - step = getStep(mathAbs(labelMetrics.h / mathSin(deg2rad * rot))); + // Determine the total padding needed to the length of the axis to make room for the + // pointRange. If the series' pointPlacement is 'on', no padding is added. + pointRangePadding = mathMax( + pointRangePadding, + pointPlacement === 'on' ? 0 : seriesPointRange + ); + } + }); + } - score = step + mathAbs(rot / 360); + // Record minPointOffset and pointRangePadding + ordinalCorrection = axis.ordinalSlope && closestPointRange ? axis.ordinalSlope / closestPointRange : 1; // #988, #1853 + axis.minPointOffset = minPointOffset = minPointOffset * ordinalCorrection; + axis.pointRangePadding = pointRangePadding = pointRangePadding * ordinalCorrection; - if (score < bestScore) { - bestScore = score; - rotation = rot; - newTickInterval = step; - } - } - }); - } + // pointRange means the width reserved for each point, like in a column chart + axis.pointRange = mathMin(pointRange, range); - } else if (!labelOptions.step) { // #4411 - newTickInterval = getStep(labelMetrics.h); - } + // closestPointRange means the closest distance between points. In columns + // it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange + // is some other value + if (isXAxis) { + axis.closestPointRange = closestPointRange; + } + } - this.autoRotation = autoRotation; - this.labelRotation = pick(rotation, rotationOption); + // Secondary values + if (saveOld) { + axis.oldTransA = transA; + } + axis.translationSlope = axis.transA = transA = axis.len / ((range + pointRangePadding) || 1); + axis.transB = axis.horiz ? axis.left : axis.bottom; // translation addend + axis.minPixelPadding = transA * minPointOffset; + }, - return newTickInterval; - }, + minFromRange: function () { + return this.max - this.range; + }, - renderUnsquish: function () { - var chart = this.chart, - renderer = chart.renderer, - tickPositions = this.tickPositions, - ticks = this.ticks, - labelOptions = this.options.labels, - horiz = this.horiz, - margin = chart.margin, - slotCount = this.categories ? tickPositions.length : tickPositions.length - 1, - slotWidth = this.slotWidth = (horiz && !labelOptions.step && !labelOptions.rotation && - ((this.staggerLines || 1) * chart.plotWidth) / slotCount) || - (!horiz && ((margin[3] && (margin[3] - chart.spacing[3])) || chart.chartWidth * 0.33)), // #1580, #1931, - innerWidth = mathMax(1, mathRound(slotWidth - 2 * (labelOptions.padding || 5))), - attr = {}, - labelMetrics = renderer.fontMetrics(labelOptions.style.fontSize, ticks[0] && ticks[0].label), - textOverflowOption = labelOptions.style.textOverflow, - css, - labelLength = 0, - label, - i, - pos; + /** + * Set the tick positions to round values and optionally extend the extremes + * to the nearest tick + */ + setTickInterval: function (secondPass) { + var axis = this, + chart = axis.chart, + options = axis.options, + isLog = axis.isLog, + isDatetimeAxis = axis.isDatetimeAxis, + isXAxis = axis.isXAxis, + isLinked = axis.isLinked, + maxPadding = options.maxPadding, + minPadding = options.minPadding, + length, + linkedParentExtremes, + tickIntervalOption = options.tickInterval, + minTickInterval, + tickPixelIntervalOption = options.tickPixelInterval, + categories = axis.categories, + threshold = axis.threshold, + softThreshold = axis.softThreshold, + thresholdMin, + thresholdMax, + hardMin, + hardMax; - // Set rotation option unless it is "auto", like in gauges - if (!isString(labelOptions.rotation)) { - attr.rotation = labelOptions.rotation || 0; // #4443 - } - - // Handle auto rotation on horizontal axis - if (this.autoRotation) { + if (!isDatetimeAxis && !categories && !isLinked) { + this.getTickAmount(); + } - // Get the longest label length - each(tickPositions, function (tick) { - tick = ticks[tick]; - if (tick && tick.labelLength > labelLength) { - labelLength = tick.labelLength; - } - }); - - // Apply rotation only if the label is too wide for the slot, and - // the label is wider than its height. - if (labelLength > innerWidth && labelLength > labelMetrics.h) { - attr.rotation = this.labelRotation; - } else { - this.labelRotation = 0; - } + // Min or max set either by zooming/setExtremes or initial options + hardMin = pick(axis.userMin, options.min); + hardMax = pick(axis.userMax, options.max); - // Handle word-wrap or ellipsis on vertical axis - } else if (slotWidth) { - // For word-wrap or ellipsis - css = { width: innerWidth + PX }; + // Linked axis gets the extremes from the parent axis + if (isLinked) { + axis.linkedParent = chart[axis.coll][options.linkedTo]; + linkedParentExtremes = axis.linkedParent.getExtremes(); + axis.min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin); + axis.max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax); + if (options.type !== axis.linkedParent.options.type) { + error(11, 1); // Can't link axes of different type + } - if (!textOverflowOption) { - css.textOverflow = 'clip'; + // Initial min and max from the extreme data values + } else { - // On vertical axis, only allow word wrap if there is room for more lines. - i = tickPositions.length; - while (!horiz && i--) { - pos = tickPositions[i]; - label = ticks[pos].label; - if (label) { - // Reset ellipsis in order to get the correct bounding box (#4070) - if (label.styles.textOverflow === 'ellipsis') { - label.css({ textOverflow: 'clip' }); - } - if (label.getBBox().height > this.len / tickPositions.length - (labelMetrics.h - labelMetrics.f)) { - label.specCss = { textOverflow: 'ellipsis' }; - } - } - } - } - } + // Adjust to hard threshold + if (!softThreshold && defined(threshold)) { + if (axis.dataMin >= threshold) { + thresholdMin = threshold; + minPadding = 0; + } else if (axis.dataMax <= threshold) { + thresholdMax = threshold; + maxPadding = 0; + } + } + axis.min = pick(hardMin, thresholdMin, axis.dataMin); + axis.max = pick(hardMax, thresholdMax, axis.dataMax); - // Add ellipsis if the label length is significantly longer than ideal - if (attr.rotation) { - css = { - width: (labelLength > chart.chartHeight * 0.5 ? chart.chartHeight * 0.33 : chart.chartHeight) + PX - }; - if (!textOverflowOption) { - css.textOverflow = 'ellipsis'; - } - } + } - // Set the explicit or automatic label alignment - this.labelAlign = attr.align = labelOptions.align || this.autoLabelAlign(this.labelRotation); + if (isLog) { + if (!secondPass && mathMin(axis.min, pick(axis.dataMin, axis.min)) <= 0) { // #978 + error(10, 1); // Can't plot negative values on log axis + } + // The correctFloat cures #934, float errors on full tens. But it + // was too aggressive for #4360 because of conversion back to lin, + // therefore use precision 15. + axis.min = correctFloat(log2lin(axis.min), 15); + axis.max = correctFloat(log2lin(axis.max), 15); + } - // Apply general and specific CSS - each(tickPositions, function (pos) { - var tick = ticks[pos], - label = tick && tick.label; - if (label) { - label.attr(attr); // This needs to go before the CSS in old IE (#4502) - if (css) { - label.css(merge(css, label.specCss)); - } - delete label.specCss; - tick.rotation = attr.rotation; - } - }); + // handle zoomed range + if (axis.range && defined(axis.max)) { + axis.userMin = axis.min = hardMin = mathMax(axis.min, axis.minFromRange()); // #618 + axis.userMax = hardMax = axis.max; - // TODO: Why not part of getLabelPosition? - this.tickRotCorr = renderer.rotCorr(labelMetrics.b, this.labelRotation || 0, this.side === 2); - }, + axis.range = null; // don't use it when running setExtremes + } - /** - * Return true if the axis has associated data - */ - hasData: function () { - return this.hasVisibleSeries || (defined(this.min) && defined(this.max) && !!this.tickPositions); - }, + // Hook for adjusting this.min and this.max. Used by bubble series. + if (axis.beforePadding) { + axis.beforePadding(); + } - /** - * Render the tick labels to a preliminary position to get their sizes - */ - getOffset: function () { - var axis = this, - chart = axis.chart, - renderer = chart.renderer, - options = axis.options, - tickPositions = axis.tickPositions, - ticks = axis.ticks, - horiz = axis.horiz, - side = axis.side, - invertedSide = chart.inverted ? [1, 0, 3, 2][side] : side, - hasData, - showAxis, - titleOffset = 0, - titleOffsetOption, - titleMargin = 0, - axisTitleOptions = options.title, - labelOptions = options.labels, - labelOffset = 0, // reset - labelOffsetPadded, - axisOffset = chart.axisOffset, - clipOffset = chart.clipOffset, - clip, - directionFactor = [-1, 1, 1, -1][side], - n, - axisParent = axis.axisParent, // Used in color axis - lineHeightCorrection; + // adjust min and max for the minimum range + axis.adjustForMinRange(); - // For reuse in Axis.render - hasData = axis.hasData(); - axis.showAxis = showAxis = hasData || pick(options.showEmpty, true); + // Pad the values to get clear of the chart's edges. To avoid tickInterval taking the padding + // into account, we do this after computing tick interval (#1337). + if (!categories && !axis.axisPointRange && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) { + length = axis.max - axis.min; + if (length) { + if (!defined(hardMin) && minPadding) { + axis.min -= length * minPadding; + } + if (!defined(hardMax) && maxPadding) { + axis.max += length * maxPadding; + } + } + } - // Set/reset staggerLines - axis.staggerLines = axis.horiz && labelOptions.staggerLines; + // Stay within floor and ceiling + if (isNumber(options.floor)) { + axis.min = mathMax(axis.min, options.floor); + } + if (isNumber(options.ceiling)) { + axis.max = mathMin(axis.max, options.ceiling); + } - // Create the axisGroup and gridGroup elements on first iteration - if (!axis.axisGroup) { - axis.gridGroup = renderer.g('grid') - .attr({ zIndex: options.gridZIndex || 1 }) - .add(axisParent); - axis.axisGroup = renderer.g('axis') - .attr({ zIndex: options.zIndex || 2 }) - .add(axisParent); - axis.labelGroup = renderer.g('axis-labels') - .attr({ zIndex: labelOptions.zIndex || 7 }) - .addClass(PREFIX + axis.coll.toLowerCase() + '-labels') - .add(axisParent); - } + // 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 + // default minPadding and startOnTick options. This is prevented by the softThreshold + // option. + if (softThreshold && defined(axis.dataMin)) { + threshold = threshold || 0; + if (!defined(hardMin) && axis.min < threshold && axis.dataMin >= threshold) { + axis.min = threshold; + } else if (!defined(hardMax) && axis.max > threshold && axis.dataMax <= threshold) { + axis.max = threshold; + } + } - if (hasData || axis.isLinked) { - - // Generate ticks - each(tickPositions, function (pos) { - if (!ticks[pos]) { - ticks[pos] = new Tick(axis, pos); - } else { - ticks[pos].addLabel(); // update labels depending on tick interval - } - }); - axis.renderUnsquish(); + // get tickInterval + if (axis.min === axis.max || axis.min === undefined || axis.max === undefined) { + axis.tickInterval = 1; + } else if (isLinked && !tickIntervalOption && + tickPixelIntervalOption === axis.linkedParent.options.tickPixelInterval) { + axis.tickInterval = tickIntervalOption = axis.linkedParent.tickInterval; + } else { + axis.tickInterval = pick( + tickIntervalOption, + this.tickAmount ? ((axis.max - axis.min) / mathMax(this.tickAmount - 1, 1)) : undefined, + categories ? // for categoried axis, 1 is default, for linear axis use tickPix + 1 : + // don't let it be more than the data range + (axis.max - axis.min) * tickPixelIntervalOption / mathMax(axis.len, tickPixelIntervalOption) + ); + } - each(tickPositions, function (pos) { - // left side must be align: right and right side must have align: left for labels - if (side === 0 || side === 2 || { 1: 'left', 3: 'right' }[side] === axis.labelAlign) { + // Now we're finished detecting min and max, crop and group series data. This + // is in turn needed in order to find tick positions in ordinal axes. + if (isXAxis && !secondPass) { + each(axis.series, function (series) { + series.processData(axis.min !== axis.oldMin || axis.max !== axis.oldMax); + }); + } - // get the highest offset - labelOffset = mathMax( - ticks[pos].getLabelSize(), - labelOffset - ); - } - }); + // set the translation factor used in translate function + axis.setAxisTranslation(true); - if (axis.staggerLines) { - labelOffset *= axis.staggerLines; - axis.labelOffset = labelOffset; - } + // hook for ordinal axes and radial axes + if (axis.beforeSetTickPositions) { + axis.beforeSetTickPositions(); + } + // hook for extensions, used in Highstock ordinal axes + if (axis.postProcessTickInterval) { + axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval); + } - } else { // doesn't have data - for (n in ticks) { - ticks[n].destroy(); - delete ticks[n]; - } - } + // In column-like charts, don't cramp in more ticks than there are points (#1943, #4184) + if (axis.pointRange && !tickIntervalOption) { + axis.tickInterval = mathMax(axis.pointRange, axis.tickInterval); + } - if (axisTitleOptions && axisTitleOptions.text && axisTitleOptions.enabled !== false) { - if (!axis.axisTitle) { - axis.axisTitle = renderer.text( - axisTitleOptions.text, - 0, - 0, - axisTitleOptions.useHTML - ) - .attr({ - zIndex: 7, - rotation: axisTitleOptions.rotation || 0, - align: - axisTitleOptions.textAlign || - { low: 'left', middle: 'center', high: 'right' }[axisTitleOptions.align] - }) - .addClass(PREFIX + this.coll.toLowerCase() + '-title') - .css(axisTitleOptions.style) - .add(axis.axisGroup); - axis.axisTitle.isNew = true; - } + // Before normalizing the tick interval, handle minimum tick interval. This applies only if tickInterval is not defined. + minTickInterval = pick(options.minTickInterval, axis.isDatetimeAxis && axis.closestPointRange); + if (!tickIntervalOption && axis.tickInterval < minTickInterval) { + axis.tickInterval = minTickInterval; + } - if (showAxis) { - titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width']; - titleOffsetOption = axisTitleOptions.offset; - titleMargin = defined(titleOffsetOption) ? 0 : pick(axisTitleOptions.margin, horiz ? 5 : 10); - } + // for linear axes, get magnitude and normalize the interval + if (!isDatetimeAxis && !isLog && !tickIntervalOption) { + axis.tickInterval = normalizeTickInterval( + axis.tickInterval, + null, + getMagnitude(axis.tickInterval), + // If the tick interval is between 0.5 and 5 and the axis max is in the order of + // thousands, chances are we are dealing with years. Don't allow decimals. #3363. + pick(options.allowDecimals, !(axis.tickInterval > 0.5 && axis.tickInterval < 5 && axis.max > 1000 && axis.max < 9999)), + !!this.tickAmount + ); + } - // hide or show the title depending on whether showEmpty is set - axis.axisTitle[showAxis ? 'show' : 'hide'](); - } + // Prevent ticks from getting so close that we can't draw the labels + if (!this.tickAmount && this.len) { // Color axis with disabled legend has no length + axis.tickInterval = axis.unsquish(); + } - // handle automatic or user set offset - axis.offset = directionFactor * pick(options.offset, axisOffset[side]); + this.setTickPositions(); + }, - axis.tickRotCorr = axis.tickRotCorr || { x: 0, y: 0 }; // polar - lineHeightCorrection = side === 2 ? axis.tickRotCorr.y : 0; - labelOffsetPadded = labelOffset + titleMargin + - (labelOffset && (directionFactor * (horiz ? pick(labelOptions.y, axis.tickRotCorr.y + 8) : labelOptions.x) - lineHeightCorrection)); - axis.axisTitleMargin = pick(titleOffsetOption, labelOffsetPadded); + /** + * Now we have computed the normalized tickInterval, get the tick positions + */ + setTickPositions: function () { - axisOffset[side] = mathMax( - axisOffset[side], - axis.axisTitleMargin + titleOffset + directionFactor * axis.offset, - labelOffsetPadded // #3027 - ); + var options = this.options, + tickPositions, + tickPositionsOption = options.tickPositions, + tickPositioner = options.tickPositioner, + startOnTick = options.startOnTick, + endOnTick = options.endOnTick, + single; - // Decide the clipping needed to keep the graph inside the plot area and axis lines - clip = options.offset ? 0 : mathFloor(options.lineWidth / 2) * 2; // #4308, #4371 - clipOffset[invertedSide] = mathMax(clipOffset[invertedSide], clip); - }, + // Set the tickmarkOffset + this.tickmarkOffset = (this.categories && options.tickmarkPlacement === 'between' && + this.tickInterval === 1) ? 0.5 : 0; // #3202 - /** - * Get the path for the axis line - */ - getLinePath: function (lineWidth) { - var chart = this.chart, - opposite = this.opposite, - offset = this.offset, - horiz = this.horiz, - lineLeft = this.left + (opposite ? this.width : 0) + offset, - lineTop = chart.chartHeight - this.bottom - (opposite ? this.height : 0) + offset; - if (opposite) { - lineWidth *= -1; // crispify the other way - #1480, #1687 - } + // get minorTickInterval + this.minorTickInterval = options.minorTickInterval === 'auto' && this.tickInterval ? + this.tickInterval / 5 : options.minorTickInterval; - return chart.renderer.crispLine([ - M, - horiz ? - this.left : - lineLeft, - horiz ? - lineTop : - this.top, - L, - horiz ? - chart.chartWidth - this.right : - lineLeft, - horiz ? - lineTop : - chart.chartHeight - this.bottom - ], lineWidth); - }, + // Find the tick positions + this.tickPositions = tickPositions = tickPositionsOption && tickPositionsOption.slice(); // Work on a copy (#1565) + if (!tickPositions) { - /** - * Position the title - */ - getTitlePosition: function () { - // compute anchor points for each of the title align options - var horiz = this.horiz, - axisLeft = this.left, - axisTop = this.top, - axisLength = this.len, - axisTitleOptions = this.options.title, - margin = horiz ? axisLeft : axisTop, - opposite = this.opposite, - offset = this.offset, - xOption = axisTitleOptions.x || 0, - yOption = axisTitleOptions.y || 0, - fontSize = pInt(axisTitleOptions.style.fontSize || 12), + if (this.isDatetimeAxis) { + tickPositions = this.getTimeTicks( + this.normalizeTimeTickInterval(this.tickInterval, options.units), + this.min, + this.max, + options.startOfWeek, + this.ordinalPositions, + this.closestPointRange, + true + ); + } else if (this.isLog) { + tickPositions = this.getLogTickPositions(this.tickInterval, this.min, this.max); + } else { + tickPositions = this.getLinearTickPositions(this.tickInterval, this.min, this.max); + } - // the position in the length direction of the axis - alongAxis = { - low: margin + (horiz ? 0 : axisLength), - middle: margin + axisLength / 2, - high: margin + (horiz ? axisLength : 0) - }[axisTitleOptions.align], + // Too dense ticks, keep only the first and last (#4477) + if (tickPositions.length > this.len) { + tickPositions = [tickPositions[0], tickPositions.pop()]; + } - // the position in the perpendicular direction of the axis - offAxis = (horiz ? axisTop + this.height : axisLeft) + - (horiz ? 1 : -1) * // horizontal axis reverses the margin - (opposite ? -1 : 1) * // so does opposite axes - this.axisTitleMargin + - (this.side === 2 ? fontSize : 0); + this.tickPositions = tickPositions; - return { - x: horiz ? - alongAxis + xOption : - offAxis + (opposite ? this.width : 0) + offset + xOption, - y: horiz ? - offAxis + yOption - (opposite ? this.height : 0) + offset : - alongAxis + yOption - }; - }, + // Run the tick positioner callback, that allows modifying auto tick positions. + if (tickPositioner) { + tickPositioner = tickPositioner.apply(this, [this.min, this.max]); + if (tickPositioner) { + this.tickPositions = tickPositions = tickPositioner; + } + } - /** - * Render the axis - */ - render: function () { - var axis = this, - chart = axis.chart, - renderer = chart.renderer, - options = axis.options, - isLog = axis.isLog, - isLinked = axis.isLinked, - tickPositions = axis.tickPositions, - axisTitle = axis.axisTitle, - ticks = axis.ticks, - minorTicks = axis.minorTicks, - alternateBands = axis.alternateBands, - stackLabelOptions = options.stackLabels, - alternateGridColor = options.alternateGridColor, - tickmarkOffset = axis.tickmarkOffset, - lineWidth = options.lineWidth, - linePath, - hasRendered = chart.hasRendered, - slideInTicks = hasRendered && defined(axis.oldMin) && !isNaN(axis.oldMin), - showAxis = axis.showAxis, - globalAnimation = renderer.globalAnimation, - from, - to; + } - // Reset - axis.labelEdge.length = 0; - //axis.justifyToPlot = overflow === 'justify'; - axis.overlap = false; + if (!this.isLinked) { - // Mark all elements inActive before we go over and mark the active ones - each([ticks, minorTicks, alternateBands], function (coll) { - var pos; - for (pos in coll) { - coll[pos].isActive = false; - } - }); + // reset min/max or remove extremes based on start/end on tick + this.trimTicks(tickPositions, startOnTick, endOnTick); - // If the series has data draw the ticks. Else only the line and title - if (axis.hasData() || isLinked) { + // When there is only one point, or all points have the same value on this axis, then min + // and max are equal and tickPositions.length is 0 or 1. In this case, add some padding + // in order to center the point, but leave it with one tick. #1337. + if (this.min === this.max && defined(this.min) && !this.tickAmount) { + // Substract half a unit (#2619, #2846, #2515, #3390) + single = true; + this.min -= 0.5; + this.max += 0.5; + } + this.single = single; - // minor ticks - if (axis.minorTickInterval && !axis.categories) { - each(axis.getMinorTickPositions(), function (pos) { - if (!minorTicks[pos]) { - minorTicks[pos] = new Tick(axis, pos, 'minor'); - } + if (!tickPositionsOption && !tickPositioner) { + this.adjustTickAmount(); + } + } + }, - // render new ticks in old position - if (slideInTicks && minorTicks[pos].isNew) { - minorTicks[pos].render(null, true); - } + /** + * Handle startOnTick and endOnTick by either adapting to padding min/max or rounded min/max + */ + trimTicks: function (tickPositions, startOnTick, endOnTick) { + var roundedMin = tickPositions[0], + roundedMax = tickPositions[tickPositions.length - 1], + minPointOffset = this.minPointOffset || 0; - minorTicks[pos].render(null, false, 1); - }); - } + if (startOnTick) { + this.min = roundedMin; + } else if (this.min - minPointOffset > roundedMin) { + tickPositions.shift(); + } - // Major ticks. Pull out the first item and render it last so that - // we can get the position of the neighbour label. #808. - if (tickPositions.length) { // #1300 - each(tickPositions, function (pos, i) { + if (endOnTick) { + this.max = roundedMax; + } else if (this.max + minPointOffset < roundedMax) { + tickPositions.pop(); + } - // linked axes need an extra check to find out if - if (!isLinked || (pos >= axis.min && pos <= axis.max)) { + // If no tick are left, set one tick in the middle (#3195) + if (tickPositions.length === 0 && defined(roundedMin)) { + tickPositions.push((roundedMax + roundedMin) / 2); + } + }, - if (!ticks[pos]) { - ticks[pos] = new Tick(axis, pos); - } + /** + * Check if there are multiple axes in the same pane + * @returns {Boolean} There are other axes + */ + alignToOthers: function () { + var others = {}, // Whether there is another axis to pair with this one + hasOther, + options = this.options; - // render new ticks in old position - if (slideInTicks && ticks[pos].isNew) { - ticks[pos].render(i, true, 0.1); - } + if (this.chart.options.chart.alignTicks !== false && options.alignTicks !== false) { + each(this.chart[this.coll], function (axis) { + var otherOptions = axis.options, + horiz = axis.horiz, + key = [ + horiz ? otherOptions.left : otherOptions.top, + otherOptions.width, + otherOptions.height, + otherOptions.pane + ].join(','); - ticks[pos].render(i); - } - }); - // In a categorized axis, the tick marks are displayed between labels. So - // we need to add a tick mark and grid line at the left edge of the X axis. - if (tickmarkOffset && (axis.min === 0 || axis.single)) { - if (!ticks[-1]) { - ticks[-1] = new Tick(axis, -1, null, true); - } - ticks[-1].render(-1); - } + if (axis.series.length) { // #4442 + if (others[key]) { + hasOther = true; // #4201 + } else { + others[key] = 1; + } + } + }); + } + return hasOther; + }, - } + /** + * Set the max ticks of either the x and y axis collection + */ + getTickAmount: function () { + var options = this.options, + tickAmount = options.tickAmount, + tickPixelInterval = options.tickPixelInterval; - // alternate grid color - if (alternateGridColor) { - each(tickPositions, function (pos, i) { - to = tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] + tickmarkOffset : axis.max - tickmarkOffset; - if (i % 2 === 0 && pos < axis.max && to <= axis.max - tickmarkOffset) { // #2248 - if (!alternateBands[pos]) { - alternateBands[pos] = new Highcharts.PlotLineOrBand(axis); - } - from = pos + tickmarkOffset; // #949 - alternateBands[pos].options = { - from: isLog ? lin2log(from) : from, - to: isLog ? lin2log(to) : to, - color: alternateGridColor - }; - alternateBands[pos].render(); - alternateBands[pos].isActive = true; - } - }); - } + if (!defined(options.tickInterval) && this.len < tickPixelInterval && !this.isRadial && + !this.isLog && options.startOnTick && options.endOnTick) { + tickAmount = 2; + } - // custom plot lines and bands - if (!axis._addedPlotLB) { // only first time - each((options.plotLines || []).concat(options.plotBands || []), function (plotLineOptions) { - axis.addPlotBandOrLine(plotLineOptions); - }); - axis._addedPlotLB = true; - } + if (!tickAmount && this.alignToOthers()) { + // Add 1 because 4 tick intervals require 5 ticks (including first and last) + tickAmount = mathCeil(this.len / tickPixelInterval) + 1; + } - } // end if hasData + // For tick amounts of 2 and 3, compute five ticks and remove the intermediate ones. This + // prevents the axis from adding ticks that are too far away from the data extremes. + if (tickAmount < 4) { + this.finalTickAmt = tickAmount; + tickAmount = 5; + } - // Remove inactive ticks - each([ticks, minorTicks, alternateBands], function (coll) { - var pos, - i, - forDestruction = [], - delay = globalAnimation ? globalAnimation.duration || 500 : 0, - destroyInactiveItems = function () { - i = forDestruction.length; - while (i--) { - // When resizing rapidly, the same items may be destroyed in different timeouts, - // or the may be reactivated - if (coll[forDestruction[i]] && !coll[forDestruction[i]].isActive) { - coll[forDestruction[i]].destroy(); - delete coll[forDestruction[i]]; - } - } + this.tickAmount = tickAmount; + }, - }; + /** + * When using multiple axes, adjust the number of ticks to match the highest + * number of ticks in that group + */ + adjustTickAmount: function () { + var tickInterval = this.tickInterval, + tickPositions = this.tickPositions, + tickAmount = this.tickAmount, + finalTickAmt = this.finalTickAmt, + currentTickAmount = tickPositions && tickPositions.length, + i, + len; - for (pos in coll) { + if (currentTickAmount < tickAmount) { + while (tickPositions.length < tickAmount) { + tickPositions.push(correctFloat( + tickPositions[tickPositions.length - 1] + tickInterval + )); + } + this.transA *= (currentTickAmount - 1) / (tickAmount - 1); + this.max = tickPositions[tickPositions.length - 1]; - if (!coll[pos].isActive) { - // Render to zero opacity - coll[pos].render(pos, false, 0); - coll[pos].isActive = false; - forDestruction.push(pos); - } - } + // We have too many ticks, run second pass to try to reduce ticks + } else if (currentTickAmount > tickAmount) { + this.tickInterval *= 2; + this.setTickPositions(); + } - // When the objects are finished fading out, destroy them - if (coll === alternateBands || !chart.hasRendered || !delay) { - destroyInactiveItems(); - } else if (delay) { - setTimeout(destroyInactiveItems, delay); - } - }); + // The finalTickAmt property is set in getTickAmount + if (defined(finalTickAmt)) { + i = len = tickPositions.length; + while (i--) { + if ( + (finalTickAmt === 3 && i % 2 === 1) || // Remove every other tick + (finalTickAmt <= 2 && i > 0 && i < len - 1) // Remove all but first and last + ) { + tickPositions.splice(i, 1); + } + } + this.finalTickAmt = UNDEFINED; + } + }, - // Static items. As the axis group is cleared on subsequent calls - // to render, these items are added outside the group. - // axis line - if (lineWidth) { - linePath = axis.getLinePath(lineWidth); - if (!axis.axisLine) { - axis.axisLine = renderer.path(linePath) - .attr({ - stroke: options.lineColor, - 'stroke-width': lineWidth, - zIndex: 7 - }) - .add(axis.axisGroup); - } else { - axis.axisLine.animate({ d: linePath }); - } + /** + * Set the scale based on data min and max, user set min and max or options + * + */ + setScale: function () { + var axis = this, + isDirtyData, + isDirtyAxisLength; - // show or hide the line depending on options.showEmpty - axis.axisLine[showAxis ? 'show' : 'hide'](); - } + axis.oldMin = axis.min; + axis.oldMax = axis.max; + axis.oldAxisLength = axis.len; - if (axisTitle && showAxis) { + // set the new axisLength + axis.setAxisSize(); + //axisLength = horiz ? axisWidth : axisHeight; + isDirtyAxisLength = axis.len !== axis.oldAxisLength; - axisTitle[axisTitle.isNew ? 'attr' : 'animate']( - axis.getTitlePosition() - ); - axisTitle.isNew = false; - } + // is there new data? + each(axis.series, function (series) { + if (series.isDirtyData || series.isDirty || + series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well + isDirtyData = true; + } + }); - // Stacked totals: - if (stackLabelOptions && stackLabelOptions.enabled) { - axis.renderStackTotals(); - } - // End stacked totals + // do we really need to go through all this? + if (isDirtyAxisLength || isDirtyData || axis.isLinked || axis.forceRedraw || + axis.userMin !== axis.oldUserMin || axis.userMax !== axis.oldUserMax || axis.alignToOthers()) { - axis.isDirty = false; - }, + if (axis.resetStacks) { + axis.resetStacks(); + } - /** - * Redraw the axis to reflect changes in the data or axis extremes - */ - redraw: function () { - - if (this.visible) { - // render the axis - this.render(); + axis.forceRedraw = false; - // move plot lines and bands - each(this.plotLinesAndBands, function (plotLine) { - plotLine.render(); - }); - } + // get data extremes if needed + axis.getSeriesExtremes(); - // mark associated series as dirty and ready for redraw - each(this.series, function (series) { - series.isDirty = true; - }); + // get fixed positions based on tickInterval + axis.setTickInterval(); - }, + // record old values to decide whether a rescale is necessary later on (#540) + axis.oldUserMin = axis.userMin; + axis.oldUserMax = axis.userMax; - /** - * Destroys an Axis instance. - */ - destroy: function (keepEvents) { - var axis = this, - stacks = axis.stacks, - stackKey, - plotLinesAndBands = axis.plotLinesAndBands, - i; + // Mark as dirty if it is not already set to dirty and extremes have changed. #595. + if (!axis.isDirty) { + axis.isDirty = isDirtyAxisLength || axis.min !== axis.oldMin || axis.max !== axis.oldMax; + } + } else if (axis.cleanStacks) { + axis.cleanStacks(); + } + }, - // Remove the events - if (!keepEvents) { - removeEvent(axis); - } + /** + * Set the extremes and optionally redraw + * @param {Number} newMin + * @param {Number} newMax + * @param {Boolean} redraw + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + * @param {Object} eventArguments + * + */ + setExtremes: function (newMin, newMax, redraw, animation, eventArguments) { + var axis = this, + chart = axis.chart; - // Destroy each stack total - for (stackKey in stacks) { - destroyObjectProperties(stacks[stackKey]); + redraw = pick(redraw, true); // defaults to true - stacks[stackKey] = null; - } + each(axis.series, function (serie) { + delete serie.kdTree; + }); - // Destroy collections - each([axis.ticks, axis.minorTicks, axis.alternateBands], function (coll) { - destroyObjectProperties(coll); - }); - i = plotLinesAndBands.length; - while (i--) { // #1975 - plotLinesAndBands[i].destroy(); - } + // Extend the arguments with min and max + eventArguments = extend(eventArguments, { + min: newMin, + max: newMax + }); - // Destroy local variables - each(['stackTotalGroup', 'axisLine', 'axisTitle', 'axisGroup', 'cross', 'gridGroup', 'labelGroup'], function (prop) { - if (axis[prop]) { - axis[prop] = axis[prop].destroy(); - } - }); + // Fire the event + fireEvent(axis, 'setExtremes', eventArguments, function () { // the default event handler - // Destroy crosshair - if (this.cross) { - this.cross.destroy(); - } - }, + axis.userMin = newMin; + axis.userMax = newMax; + axis.eventArgs = eventArguments; - /** - * Draw the crosshair - */ - drawCrosshair: function (e, point) { // docs: Missing docs for Axis.crosshair. Also for properties. + if (redraw) { + chart.redraw(animation); + } + }); + }, - var path, - options = this.crosshair, - animation = options.animation, - pos, - attribs, - categorized; - - if ( - // Disabled in options - !this.crosshair || - // Snap - ((defined(point) || !pick(this.crosshair.snap, true)) === false) || - // Not on this axis (#4095, #2888) - (point && point.series && point.series[this.coll] !== this) - ) { - this.hideCrosshair(); - - } else { + /** + * Overridable method for zooming chart. Pulled out in a separate method to allow overriding + * in stock charts. + */ + zoom: function (newMin, newMax) { + var dataMin = this.dataMin, + dataMax = this.dataMax, + options = this.options, + min = mathMin(dataMin, pick(options.min, dataMin)), + max = mathMax(dataMax, pick(options.max, dataMax)); - // Get the path - if (!pick(options.snap, true)) { - pos = (this.horiz ? e.chartX - this.pos : this.len - e.chartY + this.pos); - } else if (defined(point)) { - /*jslint eqeq: true*/ - pos = this.isXAxis ? point.plotX : this.len - point.plotY; // #3834 - /*jslint eqeq: false*/ - } + // 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 (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 - } + // In full view, displaying the reset zoom button is not required + this.displayBtn = newMin !== UNDEFINED || newMax !== UNDEFINED; - if (path === null) { - this.hideCrosshair(); - return; - } + // Do it + this.setExtremes( + newMin, + newMax, + false, + UNDEFINED, + { trigger: 'zoom' } + ); + return true; + }, - // Draw the cross - if (this.cross) { - this.cross - .attr({ visibility: VISIBLE })[animation ? 'animate' : 'attr']({ d: path }, animation); - } else { - categorized = this.categories && !this.isRadial; - attribs = { - 'stroke-width': options.width || (categorized ? this.transA : 1), - stroke: options.color || (categorized ? 'rgba(155,200,255,0.2)' : '#C0C0C0'), - zIndex: options.zIndex || 2 - }; - if (options.dashStyle) { - attribs.dashstyle = options.dashStyle; - } - this.cross = this.chart.renderer.path(path).attr(attribs).add(); - } + /** + * Update the axis metrics + */ + setAxisSize: function () { + var chart = this.chart, + options = this.options, + offsetLeft = options.offsetLeft || 0, + offsetRight = options.offsetRight || 0, + horiz = this.horiz, + width = pick(options.width, chart.plotWidth - offsetLeft + offsetRight), + height = pick(options.height, chart.plotHeight), + top = pick(options.top, chart.plotTop), + left = pick(options.left, chart.plotLeft + offsetLeft), + percentRegex = /%$/; - } + // Check for percentage based input values + if (percentRegex.test(height)) { + height = parseFloat(height) / 100 * chart.plotHeight; + } + if (percentRegex.test(top)) { + top = parseFloat(top) / 100 * chart.plotHeight + chart.plotTop; + } - }, + // Expose basic values to use in Series object and navigator + this.left = left; + this.top = top; + this.width = width; + this.height = height; + this.bottom = chart.chartHeight - height - top; + this.right = chart.chartWidth - width - left; - /** - * Hide the crosshair. - */ - hideCrosshair: function () { - if (this.cross) { - this.cross.hide(); - } - } -}; // end Axis + // Direction agnostic properties + this.len = mathMax(horiz ? width : height, 0); // mathMax fixes #905 + this.pos = horiz ? left : top; // distance from SVG origin + }, -extend(Axis.prototype, AxisPlotLineOrBandExtension); + /** + * Get the actual axis extremes + */ + getExtremes: function () { + var axis = this, + isLog = axis.isLog; -/** - * Set the tick positions to a time unit that makes sense, for example - * on the first of each month or on every Monday. Return an array - * with the time positions. Used in datetime axes as well as for grouping - * data on a datetime axis. - * - * @param {Object} normalizedInterval The interval in axis values (ms) and the count - * @param {Number} min The minimum in axis values - * @param {Number} max The maximum in axis values - * @param {Number} startOfWeek - */ -Axis.prototype.getTimeTicks = function (normalizedInterval, min, max, startOfWeek) { - var tickPositions = [], - i, - higherRanks = {}, - useUTC = defaultOptions.global.useUTC, - minYear, // used in months and years as a basis for Date.UTC() - minDate = new Date(min - getTZOffset(min)), - interval = normalizedInterval.unitRange, - count = normalizedInterval.count; + return { + min: isLog ? correctFloat(lin2log(axis.min)) : axis.min, + max: isLog ? correctFloat(lin2log(axis.max)) : axis.max, + dataMin: axis.dataMin, + dataMax: axis.dataMax, + userMin: axis.userMin, + userMax: axis.userMax + }; + }, - if (defined(min)) { // #1300 - minDate[setMilliseconds](interval >= timeUnits.second ? 0 : // #3935 - count * mathFloor(minDate.getMilliseconds() / count)); // #3652, #3654 + /** + * Get the zero plane either based on zero or on the min or max value. + * Used in bar and area plots + */ + getThreshold: function (threshold) { + var axis = this, + isLog = axis.isLog, + realMin = isLog ? lin2log(axis.min) : axis.min, + realMax = isLog ? lin2log(axis.max) : axis.max; - if (interval >= timeUnits.second) { // second - minDate[setSeconds](interval >= timeUnits.minute ? 0 : // #3935 - count * mathFloor(minDate.getSeconds() / count)); - } - - if (interval >= timeUnits.minute) { // minute - minDate[setMinutes](interval >= timeUnits.hour ? 0 : - count * mathFloor(minDate[getMinutes]() / count)); - } - - if (interval >= timeUnits.hour) { // hour - minDate[setHours](interval >= timeUnits.day ? 0 : - count * mathFloor(minDate[getHours]() / count)); - } - - if (interval >= timeUnits.day) { // day - minDate[setDate](interval >= timeUnits.month ? 1 : - count * mathFloor(minDate[getDate]() / count)); - } - - if (interval >= timeUnits.month) { // month - minDate[setMonth](interval >= timeUnits.year ? 0 : - count * mathFloor(minDate[getMonth]() / count)); - minYear = minDate[getFullYear](); - } - - if (interval >= timeUnits.year) { // year - minYear -= minYear % count; - minDate[setFullYear](minYear); - } - - // week is a special case that runs outside the hierarchy - if (interval === timeUnits.week) { - // get start of current week, independent of count - minDate[setDate](minDate[getDate]() - minDate[getDay]() + - pick(startOfWeek, 1)); - } - - - // get tick positions - i = 1; - if (timezoneOffset || getTimezoneOffset) { - minDate = minDate.getTime(); - minDate = new Date(minDate + getTZOffset(minDate)); - } - minYear = minDate[getFullYear](); - var time = minDate.getTime(), - minMonth = minDate[getMonth](), - minDateDate = minDate[getDate](), - localTimezoneOffset = (timeUnits.day + - (useUTC ? getTZOffset(minDate) : minDate.getTimezoneOffset() * 60 * 1000) - ) % timeUnits.day; // #950, #3359 - - // iterate and add tick positions at appropriate values - while (time < max) { - tickPositions.push(time); - - // if the interval is years, use Date.UTC to increase years - if (interval === timeUnits.year) { - time = makeTime(minYear + i * count, 0); - - // if the interval is months, use Date.UTC to increase months - } else if (interval === timeUnits.month) { - time = makeTime(minYear, minMonth + i * count); - - // if we're using global time, the interval is not fixed as it jumps - // one hour at the DST crossover - } else if (!useUTC && (interval === timeUnits.day || interval === timeUnits.week)) { - time = makeTime(minYear, minMonth, minDateDate + - i * count * (interval === timeUnits.day ? 1 : 7)); - - // else, the interval is fixed and we use simple addition - } else { - time += interval * count; - } - - i++; - } - - // push the last time - tickPositions.push(time); + // With a threshold of null, make the columns/areas rise from the top or bottom + // depending on the value, assuming an actual threshold of 0 (#4233). + if (threshold === null) { + threshold = realMax < 0 ? realMax : realMin; + } else if (realMin > threshold) { + threshold = realMin; + } else if (realMax < threshold) { + threshold = realMax; + } + return axis.translate(threshold, 0, 1, 0, 1); + }, - // 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'; - }); - } + /** + * Compute auto alignment for the axis label based on which side the axis is on + * and the given rotation for the label + */ + autoLabelAlign: function (rotation) { + var ret, + angle = (pick(rotation, 0) - (this.side * 90) + 720) % 360; + if (angle > 15 && angle < 165) { + ret = 'right'; + } else if (angle > 195 && angle < 345) { + ret = 'left'; + } else { + ret = 'center'; + } + return ret; + }, - // record information on the chosen unit - for dynamic label formatter - tickPositions.info = extend(normalizedInterval, { - higherRanks: higherRanks, - totalRange: interval * count - }); + /** + * Prevent the ticks from getting so close we can't draw the labels. On a horizontal + * axis, this is handled by rotating the labels, removing ticks and adding ellipsis. + * On a vertical axis remove ticks and add ellipsis. + */ + unsquish: function () { + var chart = this.chart, + ticks = this.ticks, + labelOptions = this.options.labels, + horiz = this.horiz, + tickInterval = this.tickInterval, + newTickInterval = tickInterval, + slotSize = this.len / (((this.categories ? 1 : 0) + this.max - this.min) / tickInterval), + rotation, + rotationOption = labelOptions.rotation, + labelMetrics = chart.renderer.fontMetrics(labelOptions.style.fontSize, ticks[0] && ticks[0].label), + step, + bestScore = Number.MAX_VALUE, + autoRotation, + // Return the multiple of tickInterval that is needed to avoid collision + getStep = function (spaceNeeded) { + var step = spaceNeeded / (slotSize || 1); + step = step > 1 ? mathCeil(step) : 1; + return step * tickInterval; + }; - return tickPositions; -}; + if (horiz) { + autoRotation = !labelOptions.staggerLines && !labelOptions.step && ( // #3971 + defined(rotationOption) ? + [rotationOption] : + slotSize < pick(labelOptions.autoRotationLimit, 80) && labelOptions.autoRotation + ); -/** - * Get a normalized tick interval for dates. Returns a configuration object with - * unit range (interval), count and name. Used to prepare data for getTimeTicks. - * Previously this logic was part of getTimeTicks, but as getTimeTicks now runs - * of segments in stock charts, the normalizing logic was extracted in order to - * prevent it for running over again for each segment having the same interval. - * #662, #697. - */ -Axis.prototype.normalizeTimeTickInterval = function (tickInterval, unitsOption) { - var units = unitsOption || [[ - 'millisecond', // unit name - [1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples - ], [ - 'second', - [1, 2, 5, 10, 15, 30] - ], [ - 'minute', - [1, 2, 5, 10, 15, 30] - ], [ - 'hour', - [1, 2, 3, 4, 6, 8, 12] - ], [ - 'day', - [1, 2] - ], [ - 'week', - [1, 2] - ], [ - 'month', - [1, 2, 3, 4, 6] - ], [ - 'year', - null - ]], - unit = units[units.length - 1], // default unit is years - interval = timeUnits[unit[0]], - multiples = unit[1], - count, - i; - - // loop through the units to find the one that best fits the tickInterval - for (i = 0; i < units.length; i++) { - unit = units[i]; - interval = timeUnits[unit[0]]; - multiples = unit[1]; + if (autoRotation) { + // Loop over the given autoRotation options, and determine which gives the best score. The + // best score is that with the lowest number of steps and a rotation closest to horizontal. + each(autoRotation, function (rot) { + var score; - if (units[i + 1]) { - // lessThan is in the middle between the highest multiple and the next unit. - var lessThan = (interval * multiples[multiples.length - 1] + - timeUnits[units[i + 1][0]]) / 2; + if (rot === rotationOption || (rot && rot >= -90 && rot <= 90)) { // #3891 - // break and keep the current unit - if (tickInterval <= lessThan) { - break; - } - } - } + step = getStep(mathAbs(labelMetrics.h / mathSin(deg2rad * rot))); - // prevent 2.5 years intervals, though 25, 250 etc. are allowed - if (interval === timeUnits.year && tickInterval < 5 * interval) { - multiples = [1, 2, 5]; - } + score = step + mathAbs(rot / 360); - // get the count - count = normalizeTickInterval( - tickInterval / interval, - multiples, - unit[0] === 'year' ? mathMax(getMagnitude(tickInterval / interval), 1) : 1 // #1913, #2360 - ); - - return { - unitRange: interval, - count: count, - unitName: unit[0] - }; -};/** - * Methods defined on the Axis prototype - */ + if (score < bestScore) { + bestScore = score; + rotation = rot; + newTickInterval = step; + } + } + }); + } -/** - * Set the tick positions of a logarithmic axis - */ -Axis.prototype.getLogTickPositions = function (interval, min, max, minor) { - var axis = this, - options = axis.options, - axisLength = axis.len, - // Since we use this method for both major and minor ticks, - // use a local variable and return the result - positions = []; - - // Reset - if (!minor) { - axis._minorAutoInterval = null; - } - - // First case: All ticks fall on whole logarithms: 1, 10, 100 etc. - if (interval >= 0.5) { - interval = mathRound(interval); - positions = axis.getLinearTickPositions(interval, min, max); - - // Second case: We need intermediary ticks. For example - // 1, 2, 4, 6, 8, 10, 20, 40 etc. - } else if (interval >= 0.08) { - var roundedMin = mathFloor(min), - intermediate, - i, - j, - len, - pos, - lastPos, - break2; - - if (interval > 0.3) { - intermediate = [1, 2, 4]; - } else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc - intermediate = [1, 2, 4, 6, 8]; - } else { // 0.1 equals ten minor ticks per 1, 10, 100 etc - intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9]; - } - - for (i = roundedMin; i < max + 1 && !break2; i++) { - len = intermediate.length; - for (j = 0; j < len && !break2; j++) { - pos = log2lin(lin2log(i) * intermediate[j]); - if (pos > min && (!minor || lastPos <= max) && lastPos !== UNDEFINED) { // #1670, lastPos is #3113 - positions.push(lastPos); - } - - if (lastPos > max) { - break2 = true; - } - lastPos = pos; - } - } - - // Third case: We are so deep in between whole logarithmic values that - // we might as well handle the tick positions like a linear axis. For - // example 1.01, 1.02, 1.03, 1.04. - } else { - var realMin = lin2log(min), - realMax = lin2log(max), - tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'], - filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption, - tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1), - totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength; - - interval = pick( - filteredTickIntervalOption, - axis._minorAutoInterval, - (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1) - ); - - interval = normalizeTickInterval( - interval, - null, - getMagnitude(interval) - ); - - positions = map(axis.getLinearTickPositions( - interval, - realMin, - realMax - ), log2lin); - - if (!minor) { - axis._minorAutoInterval = interval / 5; - } - } - - // Set the axis-level tickInterval variable - if (!minor) { - axis.tickInterval = interval; - } - return positions; -};/** - * The tooltip object - * @param {Object} chart The chart instance - * @param {Object} options Tooltip options - */ -var Tooltip = Highcharts.Tooltip = function () { - this.init.apply(this, arguments); -}; + } else if (!labelOptions.step) { // #4411 + newTickInterval = getStep(labelMetrics.h); + } -Tooltip.prototype = { + this.autoRotation = autoRotation; + this.labelRotation = pick(rotation, rotationOption); - init: function (chart, options) { + return newTickInterval; + }, - var borderWidth = options.borderWidth, - style = options.style, - padding = pInt(style.padding); + renderUnsquish: function () { + var chart = this.chart, + renderer = chart.renderer, + tickPositions = this.tickPositions, + ticks = this.ticks, + labelOptions = this.options.labels, + horiz = this.horiz, + margin = chart.margin, + slotCount = this.categories ? tickPositions.length : tickPositions.length - 1, + slotWidth = this.slotWidth = (horiz && (labelOptions.step || 0) < 2 && !labelOptions.rotation && // #4415 + ((this.staggerLines || 1) * chart.plotWidth) / slotCount) || + (!horiz && ((margin[3] && (margin[3] - chart.spacing[3])) || chart.chartWidth * 0.33)), // #1580, #1931, + innerWidth = mathMax(1, mathRound(slotWidth - 2 * (labelOptions.padding || 5))), + attr = {}, + labelMetrics = renderer.fontMetrics(labelOptions.style.fontSize, ticks[0] && ticks[0].label), + textOverflowOption = labelOptions.style.textOverflow, + css, + labelLength = 0, + label, + i, + pos; - // Save the chart and options - this.chart = chart; - this.options = options; + // Set rotation option unless it is "auto", like in gauges + if (!isString(labelOptions.rotation)) { + attr.rotation = labelOptions.rotation || 0; // #4443 + } - // Keep track of the current series - //this.currentSeries = UNDEFINED; + // Handle auto rotation on horizontal axis + if (this.autoRotation) { - // List of crosshairs - this.crosshairs = []; + // Get the longest label length + each(tickPositions, function (tick) { + tick = ticks[tick]; + if (tick && tick.labelLength > labelLength) { + labelLength = tick.labelLength; + } + }); - // Current values of x and y when animating - this.now = { x: 0, y: 0 }; + // Apply rotation only if the label is too wide for the slot, and + // the label is wider than its height. + if (labelLength > innerWidth && labelLength > labelMetrics.h) { + attr.rotation = this.labelRotation; + } else { + this.labelRotation = 0; + } - // The tooltip is initially hidden - this.isHidden = true; + // Handle word-wrap or ellipsis on vertical axis + } else if (slotWidth) { + // For word-wrap or ellipsis + css = { width: innerWidth + PX }; + if (!textOverflowOption) { + css.textOverflow = 'clip'; - // create the label - this.label = chart.renderer.label('', 0, 0, options.shape || 'callout', null, null, options.useHTML, null, 'tooltip') - .attr({ - padding: padding, - fill: options.backgroundColor, - 'stroke-width': borderWidth, - r: options.borderRadius, - zIndex: 8 - }) - .css(style) - .css({ padding: 0 }) // Remove it from VML, the padding is applied as an attribute instead (#1117) - .add() - .attr({ y: -9999 }); // #2301, #2657 + // On vertical axis, only allow word wrap if there is room for more lines. + i = tickPositions.length; + while (!horiz && i--) { + pos = tickPositions[i]; + label = ticks[pos].label; + if (label) { + // Reset ellipsis in order to get the correct bounding box (#4070) + if (label.styles.textOverflow === 'ellipsis') { + label.css({ textOverflow: 'clip' }); + } + if (label.getBBox().height > this.len / tickPositions.length - (labelMetrics.h - labelMetrics.f) || + ticks[pos].labelLength > slotWidth) { // #4678 + label.specCss = { textOverflow: 'ellipsis' }; + } + } + } + } + } - // When using canVG the shadow shows up as a gray circle - // even if the tooltip is hidden. - if (!useCanVG) { - this.label.shadow(options.shadow); - } - // Public property for getting the shared state. - this.shared = options.shared; - }, + // Add ellipsis if the label length is significantly longer than ideal + if (attr.rotation) { + css = { + width: (labelLength > chart.chartHeight * 0.5 ? chart.chartHeight * 0.33 : chart.chartHeight) + PX + }; + if (!textOverflowOption) { + css.textOverflow = 'ellipsis'; + } + } - /** - * Destroy the tooltip and its elements. - */ - destroy: function () { - // Destroy and clear local variables - if (this.label) { - this.label = this.label.destroy(); - } - clearTimeout(this.hideTimer); - clearTimeout(this.tooltipTimeout); - }, + // Set the explicit or automatic label alignment + this.labelAlign = attr.align = labelOptions.align || this.autoLabelAlign(this.labelRotation); - /** - * Provide a soft movement for the tooltip - * - * @param {Number} x - * @param {Number} y - * @private - */ - move: function (x, y, anchorX, anchorY) { - var tooltip = this, - now = tooltip.now, - animate = tooltip.options.animation !== false && !tooltip.isHidden && - // When we get close to the target position, abort animation and land on the right place (#3056) - (mathAbs(x - now.x) > 1 || mathAbs(y - now.y) > 1), - skipAnchor = tooltip.followPointer || tooltip.len > 1; + // Apply general and specific CSS + each(tickPositions, function (pos) { + var tick = ticks[pos], + label = tick && tick.label; + if (label) { + label.attr(attr); // This needs to go before the CSS in old IE (#4502) + if (css) { + label.css(merge(css, label.specCss)); + } + delete label.specCss; + tick.rotation = attr.rotation; + } + }); - // Get intermediate values for animation - extend(now, { - x: animate ? (2 * now.x + x) / 3 : x, - y: animate ? (now.y + y) / 2 : y, - anchorX: skipAnchor ? UNDEFINED : animate ? (2 * now.anchorX + anchorX) / 3 : anchorX, - anchorY: skipAnchor ? UNDEFINED : animate ? (now.anchorY + anchorY) / 2 : anchorY - }); + // Note: Why is this not part of getLabelPosition? + this.tickRotCorr = renderer.rotCorr(labelMetrics.b, this.labelRotation || 0, this.side !== 0); + }, - // Move to the intermediate value - tooltip.label.attr(now); + /** + * Return true if the axis has associated data + */ + hasData: function () { + return this.hasVisibleSeries || (defined(this.min) && defined(this.max) && !!this.tickPositions); + }, - - // 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. - if (tooltip) { - tooltip.move(x, y, anchorX, anchorY); - } - }, 32); - - } - }, + /** + * Render the tick labels to a preliminary position to get their sizes + */ + getOffset: function () { + var axis = this, + chart = axis.chart, + renderer = chart.renderer, + options = axis.options, + tickPositions = axis.tickPositions, + ticks = axis.ticks, + horiz = axis.horiz, + side = axis.side, + invertedSide = chart.inverted ? [1, 0, 3, 2][side] : side, + hasData, + showAxis, + titleOffset = 0, + titleOffsetOption, + titleMargin = 0, + axisTitleOptions = options.title, + labelOptions = options.labels, + labelOffset = 0, // reset + labelOffsetPadded, + opposite = axis.opposite, + axisOffset = chart.axisOffset, + clipOffset = chart.clipOffset, + clip, + directionFactor = [-1, 1, 1, -1][side], + n, + axisParent = axis.axisParent, // Used in color axis + lineHeightCorrection; - /** - * Hide the tooltip - */ - hide: function (delay) { - var tooltip = this, - hoverPoints; - - clearTimeout(this.hideTimer); // disallow duplicate timers (#1728, #1766) - if (!this.isHidden) { - hoverPoints = this.chart.hoverPoints; + // For reuse in Axis.render + hasData = axis.hasData(); + axis.showAxis = showAxis = hasData || pick(options.showEmpty, true); - this.hideTimer = setTimeout(function () { - tooltip.label.fadeOut(); - tooltip.isHidden = true; - }, pick(delay, this.options.hideDelay, 500)); - } - }, - - /** - * Extendable method to get the anchor position of the tooltip - * from a point or set of points - */ - getAnchor: function (points, mouseEvent) { - var ret, - chart = this.chart, - inverted = chart.inverted, - plotTop = chart.plotTop, - plotLeft = chart.plotLeft, - plotX = 0, - plotY = 0, - yAxis, - xAxis; - - points = splat(points); - - // Pie uses a special tooltipPos - ret = points[0].tooltipPos; - - // When tooltip follows mouse, relate the position to the mouse - if (this.followPointer && mouseEvent) { - if (mouseEvent.chartX === UNDEFINED) { - mouseEvent = chart.pointer.normalize(mouseEvent); - } - ret = [ - mouseEvent.chartX - chart.plotLeft, - mouseEvent.chartY - plotTop - ]; - } - // When shared, use the average position - if (!ret) { - each(points, function (point) { - yAxis = point.series.yAxis; - xAxis = point.series.xAxis; - plotX += point.plotX + (!inverted && xAxis ? xAxis.left - plotLeft : 0); - plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) + - (!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151 - }); - - plotX /= points.length; - plotY /= points.length; - - ret = [ - inverted ? chart.plotWidth - plotY : plotX, - this.shared && !inverted && points.length > 1 && mouseEvent ? - mouseEvent.chartY - plotTop : // place shared tooltip next to the mouse (#424) - inverted ? chart.plotHeight - plotX : plotY - ]; - } + // Set/reset staggerLines + axis.staggerLines = axis.horiz && labelOptions.staggerLines; - return map(ret, mathRound); - }, - - /** - * Place the tooltip in a chart without spilling over - * and not covering the point it self. - */ - getPosition: function (boxWidth, boxHeight, point) { - - 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], - // The far side is right or bottom - preferFarSide = pick(point.ttBelow, (chart.inverted && !point.negative) || (!chart.inverted && point.negative)), - /** - * Handle the preferred dimension. When the preferred dimension is tooltip - * on top or bottom of the point, it will look for space there. - */ - firstDimension = function (dim, outerSize, innerSize, point, min, max) { - var roomLeft = innerSize < point - distance, - roomRight = point + distance + innerSize < outerSize, - alignedLeft = point - distance - innerSize, - alignedRight = point + distance; + // Create the axisGroup and gridGroup elements on first iteration + if (!axis.axisGroup) { + axis.gridGroup = renderer.g('grid') + .attr({ zIndex: options.gridZIndex || 1 }) + .add(axisParent); + axis.axisGroup = renderer.g('axis') + .attr({ zIndex: options.zIndex || 2 }) + .add(axisParent); + axis.labelGroup = renderer.g('axis-labels') + .attr({ zIndex: labelOptions.zIndex || 7 }) + .addClass(PREFIX + axis.coll.toLowerCase() + '-labels') + .add(axisParent); + } - if (preferFarSide && roomRight) { - ret[dim] = alignedRight; - } else if (!preferFarSide && roomLeft) { - ret[dim] = alignedLeft; - } else if (roomLeft) { - ret[dim] = mathMin(max - innerSize, alignedLeft - h < 0 ? alignedLeft : alignedLeft - h); - } else if (roomRight) { - ret[dim] = mathMax(min, alignedRight + h + innerSize > outerSize ? alignedRight : alignedRight + h); - } else { - return false; - } - }, - /** - * Handle the secondary dimension. If the preferred dimension is tooltip - * on top or bottom of the point, the second dimension is to align the tooltip - * above the point, trying to align center but allowing left or right - * align within the chart box. - */ - secondDimension = function (dim, outerSize, innerSize, point) { - // Too close to the edge, return false and swap dimensions - if (point < distance || point > outerSize - distance) { - return false; - - // Align left/top - } else if (point < innerSize / 2) { - ret[dim] = 1; - // Align right/bottom - } else if (point > outerSize - innerSize / 2) { - ret[dim] = outerSize - innerSize - 2; - // Align center - } else { - ret[dim] = point - innerSize / 2; - } - }, - /** - * Swap the dimensions - */ - swap = function (count) { - var temp = first; - first = second; - second = temp; - swapped = count; - }, - run = function () { - if (firstDimension.apply(0, first) !== false) { - if (secondDimension.apply(0, second) === false && !swapped) { - swap(true); - run(); - } - } else if (!swapped) { - swap(true); - run(); - } else { - ret.x = ret.y = 0; - } - }; + if (hasData || axis.isLinked) { - // Under these conditions, prefer the tooltip on the side of the point - if (chart.inverted || this.len > 1) { - swap(); - } - run(); + // Generate ticks + each(tickPositions, function (pos) { + if (!ticks[pos]) { + ticks[pos] = new Tick(axis, pos); + } else { + ticks[pos].addLabel(); // update labels depending on tick interval + } + }); - return ret; - - }, + axis.renderUnsquish(); - /** - * In case no user defined formatter is given, this will be used. Note that the context - * here is an object holding point, series, x, y etc. - */ - 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 + // Left side must be align: right and right side must have align: left for labels + if (labelOptions.reserveSpace !== false && (side === 0 || side === 2 || + { 1: 'left', 3: 'right' }[side] === axis.labelAlign || axis.labelAlign === 'center')) { + each(tickPositions, function (pos) { - // build the values - s = s.concat(tooltip.bodyFormatter(items)); + // get the highest offset + labelOffset = mathMax( + ticks[pos].getLabelSize(), + labelOffset + ); + }); + } - // footer - s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true)); //#3397: abstraction to enable formatting of footer and header + if (axis.staggerLines) { + labelOffset *= axis.staggerLines; + axis.labelOffset = labelOffset * (axis.opposite ? -1 : 1); + } - return s.join(''); - }, - /** - * Refresh the tooltip's text and position. - * @param {Object} point - */ - refresh: function (point, mouseEvent) { - var tooltip = this, - chart = tooltip.chart, - label = tooltip.label, - options = tooltip.options, - x, - y, - anchor, - textConfig = {}, - text, - pointConfig = [], - formatter = options.formatter || tooltip.defaultFormatter, - hoverPoints = chart.hoverPoints, - borderColor, - shared = tooltip.shared, - currentSeries; - - clearTimeout(this.hideTimer); - - // get the reference point coordinates (pie charts use tooltipPos) - tooltip.followPointer = splat(point)[0].series.tooltipOptions.followPointer; - anchor = tooltip.getAnchor(point, mouseEvent); - x = anchor[0]; - y = anchor[1]; + } else { // doesn't have data + for (n in ticks) { + ticks[n].destroy(); + delete ticks[n]; + } + } - // shared tooltip, array is sent over - if (shared && !(point.series && point.series.noSharedTooltip)) { - - // hide previous hoverPoints and set new - - chart.hoverPoints = point; - if (hoverPoints) { - each(hoverPoints, function (point) { - point.setState(); - }); - } + if (axisTitleOptions && axisTitleOptions.text && axisTitleOptions.enabled !== false) { + if (!axis.axisTitle) { + axis.axisTitle = renderer.text( + axisTitleOptions.text, + 0, + 0, + axisTitleOptions.useHTML + ) + .attr({ + zIndex: 7, + rotation: axisTitleOptions.rotation || 0, + align: + axisTitleOptions.textAlign || + { + low: opposite ? 'right' : 'left', + middle: 'center', + high: opposite ? 'left' : 'right' + }[axisTitleOptions.align] + }) + .addClass(PREFIX + this.coll.toLowerCase() + '-title') + .css(axisTitleOptions.style) + .add(axis.axisGroup); + axis.axisTitle.isNew = true; + } - each(point, function (item) { - item.setState(HOVER_STATE); + if (showAxis) { + titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width']; + titleOffsetOption = axisTitleOptions.offset; + titleMargin = defined(titleOffsetOption) ? 0 : pick(axisTitleOptions.margin, horiz ? 5 : 10); + } - pointConfig.push(item.getLabelConfig()); - }); + // hide or show the title depending on whether showEmpty is set + axis.axisTitle[showAxis ? 'show' : 'hide'](true); + } - textConfig = { - x: point[0].category, - y: point[0].y - }; - textConfig.points = pointConfig; - this.len = pointConfig.length; - point = point[0]; + // handle automatic or user set offset + axis.offset = directionFactor * pick(options.offset, axisOffset[side]); - // single point tooltip - } else { - textConfig = point.getLabelConfig(); - } - text = formatter.call(textConfig, tooltip); + axis.tickRotCorr = axis.tickRotCorr || { x: 0, y: 0 }; // polar + lineHeightCorrection = side === 2 ? axis.tickRotCorr.y : 0; + labelOffsetPadded = Math.abs(labelOffset) + titleMargin + + (labelOffset && (directionFactor * (horiz ? pick(labelOptions.y, axis.tickRotCorr.y + 8) : labelOptions.x) - lineHeightCorrection)); + axis.axisTitleMargin = pick(titleOffsetOption, labelOffsetPadded); - // register the current series - currentSeries = point.series; - this.distance = pick(currentSeries.tooltipOptions.distance, 16); + axisOffset[side] = mathMax( + axisOffset[side], + axis.axisTitleMargin + titleOffset + directionFactor * axis.offset, + labelOffsetPadded // #3027 + ); - // update the inner HTML - if (text === false) { - this.hide(); - } else { + // Decide the clipping needed to keep the graph inside the plot area and axis lines + clip = options.offset ? 0 : mathFloor(options.lineWidth / 2) * 2; // #4308, #4371 + clipOffset[invertedSide] = mathMax(clipOffset[invertedSide], clip); + }, - // show it - if (tooltip.isHidden) { - stop(label); - label.attr('opacity', 1).show(); - } + /** + * Get the path for the axis line + */ + getLinePath: function (lineWidth) { + var chart = this.chart, + opposite = this.opposite, + offset = this.offset, + horiz = this.horiz, + lineLeft = this.left + (opposite ? this.width : 0) + offset, + lineTop = chart.chartHeight - this.bottom - (opposite ? this.height : 0) + offset; - // update text - label.attr({ - text: text - }); + if (opposite) { + lineWidth *= -1; // crispify the other way - #1480, #1687 + } - // set the stroke color of the box - borderColor = options.borderColor || point.color || currentSeries.color || '#606060'; - label.attr({ - stroke: borderColor - }); - tooltip.updatePosition({ - plotX: x, - plotY: y, - negative: point.negative, - ttBelow: point.ttBelow, - h: anchor[2] || 0 - }); - - this.isHidden = false; - } - fireEvent(chart, 'tooltipRefresh', { - text: text, - x: x + chart.plotLeft, - y: y + chart.plotTop, - borderColor: borderColor - }); - }, - - /** - * Find the new position and perform the move - */ - updatePosition: function (point) { - var chart = this.chart, - label = this.label, - pos = (this.options.positioner || this.getPosition).call( - this, - label.width, - label.height, - point - ); + return chart.renderer + .crispLine([ + M, + horiz ? + this.left : + lineLeft, + horiz ? + lineTop : + this.top, + L, + horiz ? + chart.chartWidth - this.right : + lineLeft, + horiz ? + lineTop : + chart.chartHeight - this.bottom + ], lineWidth); + }, - // do the move - this.move( - mathRound(pos.x), - mathRound(pos.y || 0), // can be undefined (#3977) - point.plotX + chart.plotLeft, - point.plotY + chart.plotTop - ); - }, + /** + * Position the title + */ + getTitlePosition: function () { + // compute anchor points for each of the title align options + var horiz = this.horiz, + axisLeft = this.left, + axisTop = this.top, + axisLength = this.len, + axisTitleOptions = this.options.title, + margin = horiz ? axisLeft : axisTop, + opposite = this.opposite, + offset = this.offset, + xOption = axisTitleOptions.x || 0, + yOption = axisTitleOptions.y || 0, + fontSize = pInt(axisTitleOptions.style.fontSize || 12), - /** - * Get the best X date format based on the closest point range on the axis. - */ - getXDateFormat: function (point, options, xAxis) { - var xDateFormat, - dateTimeLabelFormats = options.dateTimeLabelFormats, - closestPointRange = xAxis && xAxis.closestPointRange, - n, - blank = '01-01 00:00:00.000', - strpos = { - millisecond: 15, - second: 12, - minute: 9, - hour: 6, - day: 3 - }, - date, - lastN = 'millisecond'; // for sub-millisecond data, #4223 + // the position in the length direction of the axis + alongAxis = { + low: margin + (horiz ? 0 : axisLength), + middle: margin + axisLength / 2, + high: margin + (horiz ? axisLength : 0) + }[axisTitleOptions.align], - if (closestPointRange) { - date = dateFormat('%m-%d %H:%M:%S.%L', point.x); - for (n in timeUnits) { + // the position in the perpendicular direction of the axis + offAxis = (horiz ? axisTop + this.height : axisLeft) + + (horiz ? 1 : -1) * // horizontal axis reverses the margin + (opposite ? -1 : 1) * // so does opposite axes + this.axisTitleMargin + + (this.side === 2 ? fontSize : 0); - // If the range is exactly one week and we're looking at a Sunday/Monday, go for the week format - if (closestPointRange === timeUnits.week && +dateFormat('%w', point.x) === xAxis.options.startOfWeek && - date.substr(6) === blank.substr(6)) { - n = 'week'; - break; + return { + x: horiz ? + alongAxis + xOption : + offAxis + (opposite ? this.width : 0) + offset + xOption, + y: horiz ? + offAxis + yOption - (opposite ? this.height : 0) + offset : + alongAxis + yOption + }; + }, - // The first format that is too great for the range - } else if (timeUnits[n] > closestPointRange) { - n = lastN; - break; - - // If the point is placed every day at 23:59, we need to show - // the minutes as well. #2637. - } else if (strpos[n] && date.substr(strpos[n]) !== blank.substr(strpos[n])) { - break; - } + /** + * Render the axis + */ + render: function () { + var axis = this, + chart = axis.chart, + renderer = chart.renderer, + options = axis.options, + isLog = axis.isLog, + isLinked = axis.isLinked, + tickPositions = axis.tickPositions, + axisTitle = axis.axisTitle, + ticks = axis.ticks, + minorTicks = axis.minorTicks, + alternateBands = axis.alternateBands, + stackLabelOptions = options.stackLabels, + alternateGridColor = options.alternateGridColor, + tickmarkOffset = axis.tickmarkOffset, + lineWidth = options.lineWidth, + linePath, + hasRendered = chart.hasRendered, + slideInTicks = hasRendered && defined(axis.oldMin) && !isNaN(axis.oldMin), + showAxis = axis.showAxis, + globalAnimation = renderer.globalAnimation, + from, + to; - // Weeks are outside the hierarchy, only apply them on Mondays/Sundays like in the first condition - if (n !== 'week') { - lastN = n; - } - } - - if (n) { - xDateFormat = dateTimeLabelFormats[n]; - } - } else { - xDateFormat = dateTimeLabelFormats.day; - } + // Reset + axis.labelEdge.length = 0; + //axis.justifyToPlot = overflow === 'justify'; + axis.overlap = false; - return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581 - }, + // Mark all elements inActive before we go over and mark the active ones + each([ticks, minorTicks, alternateBands], function (coll) { + var pos; + for (pos in coll) { + coll[pos].isActive = false; + } + }); - /** - * Format the footer/header of the tooltip - * #3397: abstraction to enable formatting of footer and header - */ - tooltipFooterHeaderFormatter: function (point, isFooter) { - var footOrHead = isFooter ? 'footer' : 'header', - series = point.series, - tooltipOptions = series.tooltipOptions, - xDateFormat = tooltipOptions.xDateFormat, - xAxis = series.xAxis, - isDateTime = xAxis && xAxis.options.type === 'datetime' && isNumber(point.key), - formatString = tooltipOptions[footOrHead+'Format']; + // If the series has data draw the ticks. Else only the line and title + if (axis.hasData() || isLinked) { - // Guess the best date format based on the closest point distance (#568, #3418) - if (isDateTime && !xDateFormat) { - xDateFormat = this.getXDateFormat(point, tooltipOptions, xAxis); - } + // minor ticks + if (axis.minorTickInterval && !axis.categories) { + each(axis.getMinorTickPositions(), function (pos) { + if (!minorTicks[pos]) { + minorTicks[pos] = new Tick(axis, pos, 'minor'); + } - // Insert the footer date format if any - if (isDateTime && xDateFormat) { - formatString = formatString.replace('{point.key}', '{point.key:' + xDateFormat + '}'); - } + // render new ticks in old position + if (slideInTicks && minorTicks[pos].isNew) { + minorTicks[pos].render(null, true); + } - return format(formatString, { - point: point, - series: series - }); - }, + minorTicks[pos].render(null, false, 1); + }); + } - /** - * Build the body (lines) of the tooltip by iterating over the items and returning one entry for each item, - * 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); + // Major ticks. Pull out the first item and render it last so that + // we can get the position of the neighbour label. #808. + if (tickPositions.length) { // #1300 + each(tickPositions, function (pos, i) { + + // linked axes need an extra check to find out if + if (!isLinked || (pos >= axis.min && pos <= axis.max)) { + + if (!ticks[pos]) { + ticks[pos] = new Tick(axis, pos); + } + + // render new ticks in old position + if (slideInTicks && ticks[pos].isNew) { + ticks[pos].render(i, true, 0.1); + } + + ticks[pos].render(i); + } + + }); + // In a categorized axis, the tick marks are displayed between labels. So + // we need to add a tick mark and grid line at the left edge of the X axis. + if (tickmarkOffset && (axis.min === 0 || axis.single)) { + if (!ticks[-1]) { + ticks[-1] = new Tick(axis, -1, null, true); + } + ticks[-1].render(-1); + } + + } + + // alternate grid color + if (alternateGridColor) { + each(tickPositions, function (pos, i) { + to = tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] + tickmarkOffset : axis.max - tickmarkOffset; + if (i % 2 === 0 && pos < axis.max && to <= axis.max + (chart.polar ? -tickmarkOffset : tickmarkOffset)) { // #2248, #4660 + if (!alternateBands[pos]) { + alternateBands[pos] = new Highcharts.PlotLineOrBand(axis); + } + from = pos + tickmarkOffset; // #949 + alternateBands[pos].options = { + from: isLog ? lin2log(from) : from, + to: isLog ? lin2log(to) : to, + color: alternateGridColor + }; + alternateBands[pos].render(); + alternateBands[pos].isActive = true; + } + }); + } + + // custom plot lines and bands + if (!axis._addedPlotLB) { // only first time + each((options.plotLines || []).concat(options.plotBands || []), function (plotLineOptions) { + axis.addPlotBandOrLine(plotLineOptions); + }); + axis._addedPlotLB = true; + } + + } // end if hasData + + // Remove inactive ticks + each([ticks, minorTicks, alternateBands], function (coll) { + var pos, + i, + forDestruction = [], + delay = globalAnimation ? globalAnimation.duration || 500 : 0, + destroyInactiveItems = function () { + i = forDestruction.length; + while (i--) { + // When resizing rapidly, the same items may be destroyed in different timeouts, + // or the may be reactivated + if (coll[forDestruction[i]] && !coll[forDestruction[i]].isActive) { + coll[forDestruction[i]].destroy(); + delete coll[forDestruction[i]]; + } + } + + }; + + for (pos in coll) { + + if (!coll[pos].isActive) { + // Render to zero opacity + coll[pos].render(pos, false, 0); + coll[pos].isActive = false; + forDestruction.push(pos); + } + } + + // When the objects are finished fading out, destroy them + syncTimeout( + destroyInactiveItems, + coll === alternateBands || !chart.hasRendered || !delay ? 0 : delay + ); + }); + + // Static items. As the axis group is cleared on subsequent calls + // to render, these items are added outside the group. + // axis line + if (lineWidth) { + linePath = axis.getLinePath(lineWidth); + if (!axis.axisLine) { + axis.axisLine = renderer.path(linePath) + .attr({ + stroke: options.lineColor, + 'stroke-width': lineWidth, + zIndex: 7 + }) + .add(axis.axisGroup); + } else { + axis.axisLine.animate({ d: linePath }); + } + + // show or hide the line depending on options.showEmpty + axis.axisLine[showAxis ? 'show' : 'hide'](true); + } + + if (axisTitle && showAxis) { + + axisTitle[axisTitle.isNew ? 'attr' : 'animate']( + axis.getTitlePosition() + ); + axisTitle.isNew = false; + } + + // Stacked totals: + if (stackLabelOptions && stackLabelOptions.enabled) { + axis.renderStackTotals(); + } + // End stacked totals + + axis.isDirty = false; + }, + + /** + * Redraw the axis to reflect changes in the data or axis extremes + */ + redraw: function () { + + if (this.visible) { + // render the axis + this.render(); + + // move plot lines and bands + each(this.plotLinesAndBands, function (plotLine) { + plotLine.render(); + }); + } + + // mark associated series as dirty and ready for redraw + each(this.series, function (series) { + series.isDirty = true; + }); + + }, + + /** + * Destroys an Axis instance. + */ + destroy: function (keepEvents) { + var axis = this, + stacks = axis.stacks, + stackKey, + plotLinesAndBands = axis.plotLinesAndBands, + i; + + // Remove the events + if (!keepEvents) { + removeEvent(axis); + } + + // Destroy each stack total + for (stackKey in stacks) { + destroyObjectProperties(stacks[stackKey]); + + stacks[stackKey] = null; + } + + // Destroy collections + each([axis.ticks, axis.minorTicks, axis.alternateBands], function (coll) { + destroyObjectProperties(coll); + }); + i = plotLinesAndBands.length; + while (i--) { // #1975 + plotLinesAndBands[i].destroy(); + } + + // Destroy local variables + each(['stackTotalGroup', 'axisLine', 'axisTitle', 'axisGroup', 'cross', 'gridGroup', 'labelGroup'], function (prop) { + if (axis[prop]) { + axis[prop] = axis[prop].destroy(); + } + }); + + // Destroy crosshair + if (this.cross) { + this.cross.destroy(); + } + }, + + /** + * Draw the crosshair + * + * @param {Object} e The event arguments from the modified pointer event + * @param {Object} point The Point object + */ + drawCrosshair: function (e, point) { + + var path, + options = this.crosshair, + pos, + attribs, + categorized, + strokeWidth; + + if ( + // Disabled in options + !this.crosshair || + // Snap + ((defined(point) || !pick(options.snap, true)) === false) || + // Not on this axis (#4095, #2888) + (point && point.series && point.series[this.coll] !== this) + ) { + this.hideCrosshair(); + + } else { + + // Get the path + if (!pick(options.snap, true)) { + pos = (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 (path === null) { + this.hideCrosshair(); + return; + } + + categorized = this.categories && !this.isRadial; + strokeWidth = pick(options.width, (categorized ? this.transA : 1)); + + // Draw the cross + if (this.cross) { + this.cross + .attr({ + d: path, + visibility: 'visible', + 'stroke-width': strokeWidth // #4737 + }); + } else { + attribs = { + 'stroke-width': strokeWidth, + stroke: options.color || (categorized ? 'rgba(155,200,255,0.2)' : '#C0C0C0'), + zIndex: pick(options.zIndex, 2) + }; + if (options.dashStyle) { + attribs.dashstyle = options.dashStyle; + } + this.cross = this.chart.renderer.path(path).attr(attribs).add(); + } + + } + + }, + + /** + * Hide the crosshair. + */ + hideCrosshair: function () { + if (this.cross) { + this.cross.hide(); + } + } + }; // end Axis + + extend(Axis.prototype, AxisPlotLineOrBandExtension); + + /** + * Set the tick positions to a time unit that makes sense, for example + * on the first of each month or on every Monday. Return an array + * with the time positions. Used in datetime axes as well as for grouping + * data on a datetime axis. + * + * @param {Object} normalizedInterval The interval in axis values (ms) and the count + * @param {Number} min The minimum in axis values + * @param {Number} max The maximum in axis values + * @param {Number} startOfWeek + */ + Axis.prototype.getTimeTicks = function (normalizedInterval, min, max, startOfWeek) { + var tickPositions = [], + i, + higherRanks = {}, + useUTC = defaultOptions.global.useUTC, + minYear, // used in months and years as a basis for Date.UTC() + minDate = new Date(min - getTZOffset(min)), + interval = normalizedInterval.unitRange, + count = normalizedInterval.count; + + if (defined(min)) { // #1300 + minDate[setMilliseconds](interval >= timeUnits.second ? 0 : // #3935 + count * mathFloor(minDate.getMilliseconds() / count)); // #3652, #3654 + + if (interval >= timeUnits.second) { // second + minDate[setSeconds](interval >= timeUnits.minute ? 0 : // #3935 + count * mathFloor(minDate.getSeconds() / count)); + } + + if (interval >= timeUnits.minute) { // minute + minDate[setMinutes](interval >= timeUnits.hour ? 0 : + count * mathFloor(minDate[getMinutes]() / count)); + } + + if (interval >= timeUnits.hour) { // hour + minDate[setHours](interval >= timeUnits.day ? 0 : + count * mathFloor(minDate[getHours]() / count)); + } + + if (interval >= timeUnits.day) { // day + minDate[setDate](interval >= timeUnits.month ? 1 : + count * mathFloor(minDate[getDate]() / count)); + } + + if (interval >= timeUnits.month) { // month + minDate[setMonth](interval >= timeUnits.year ? 0 : + count * mathFloor(minDate[getMonth]() / count)); + minYear = minDate[getFullYear](); + } + + if (interval >= timeUnits.year) { // year + minYear -= minYear % count; + minDate[setFullYear](minYear); + } + + // week is a special case that runs outside the hierarchy + if (interval === timeUnits.week) { + // get start of current week, independent of count + minDate[setDate](minDate[getDate]() - minDate[getDay]() + + pick(startOfWeek, 1)); + } + + + // get tick positions + i = 1; + if (timezoneOffset || getTimezoneOffset) { + minDate = minDate.getTime(); + minDate = new Date(minDate + getTZOffset(minDate)); + } + minYear = minDate[getFullYear](); + var time = minDate.getTime(), + minMonth = minDate[getMonth](), + minDateDate = minDate[getDate](), + localTimezoneOffset = (timeUnits.day + + (useUTC ? getTZOffset(minDate) : minDate.getTimezoneOffset() * 60 * 1000) + ) % timeUnits.day; // #950, #3359 + + // iterate and add tick positions at appropriate values + while (time < max) { + tickPositions.push(time); + + // if the interval is years, use Date.UTC to increase years + if (interval === timeUnits.year) { + time = makeTime(minYear + i * count, 0); + + // if the interval is months, use Date.UTC to increase months + } else if (interval === timeUnits.month) { + time = makeTime(minYear, minMonth + i * count); + + // if we're using global time, the interval is not fixed as it jumps + // one hour at the DST crossover + } else if (!useUTC && (interval === timeUnits.day || interval === timeUnits.week)) { + time = makeTime(minYear, minMonth, minDateDate + + i * count * (interval === timeUnits.day ? 1 : 7)); + + // else, the interval is fixed and we use simple addition + } else { + time += interval * count; + } + + i++; + } + + // 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'; + }); + } + + + // record information on the chosen unit - for dynamic label formatter + tickPositions.info = extend(normalizedInterval, { + higherRanks: higherRanks, + totalRange: interval * count }); - } - -}; -var hoverChartIndex; + return tickPositions; + }; -// Global flag for touch support -hasTouch = doc.documentElement.ontouchstart !== UNDEFINED; + /** + * Get a normalized tick interval for dates. Returns a configuration object with + * unit range (interval), count and name. Used to prepare data for getTimeTicks. + * Previously this logic was part of getTimeTicks, but as getTimeTicks now runs + * of segments in stock charts, the normalizing logic was extracted in order to + * prevent it for running over again for each segment having the same interval. + * #662, #697. + */ + Axis.prototype.normalizeTimeTickInterval = function (tickInterval, unitsOption) { + var units = unitsOption || [[ + 'millisecond', // unit name + [1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples + ], [ + 'second', + [1, 2, 5, 10, 15, 30] + ], [ + 'minute', + [1, 2, 5, 10, 15, 30] + ], [ + 'hour', + [1, 2, 3, 4, 6, 8, 12] + ], [ + 'day', + [1, 2] + ], [ + 'week', + [1, 2] + ], [ + 'month', + [1, 2, 3, 4, 6] + ], [ + 'year', + null + ]], + unit = units[units.length - 1], // default unit is years + interval = timeUnits[unit[0]], + multiples = unit[1], + count, + i; -/** - * 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. - * @param {Object} chart The Chart instance - * @param {Object} options The root options object - */ -var Pointer = Highcharts.Pointer = function (chart, options) { - this.init(chart, options); -}; + // loop through the units to find the one that best fits the tickInterval + for (i = 0; i < units.length; i++) { + unit = units[i]; + interval = timeUnits[unit[0]]; + multiples = unit[1]; -Pointer.prototype = { - /** - * Initialize Pointer - */ - init: function (chart, options) { - - var chartOptions = options.chart, - chartEvents = chartOptions.events, - zoomType = useCanVG ? '' : chartOptions.zoomType, - inverted = chart.inverted, - zoomX, - zoomY; - // Store references - this.options = options; - this.chart = chart; - - // Zoom status - 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; + if (units[i + 1]) { + // lessThan is in the middle between the highest multiple and the next unit. + var lessThan = (interval * multiples[multiples.length - 1] + + timeUnits[units[i + 1][0]]) / 2; - // Do we need to handle click on a touch device? - this.runChartClick = chartEvents && !!chartEvents.click; + // break and keep the current unit + if (tickInterval <= lessThan) { + break; + } + } + } - this.pinchDown = []; - this.lastValidTouch = {}; + // prevent 2.5 years intervals, though 25, 250 etc. are allowed + if (interval === timeUnits.year && tickInterval < 5 * interval) { + multiples = [1, 2, 5]; + } - if (Highcharts.Tooltip && options.tooltip.enabled) { - chart.tooltip = new Tooltip(chart, options.tooltip); - this.followTouchMove = pick(options.tooltip.followTouchMove, true); - } + // get the count + count = normalizeTickInterval( + tickInterval / interval, + multiples, + unit[0] === 'year' ? mathMax(getMagnitude(tickInterval / interval), 1) : 1 // #1913, #2360 + ); - this.setDOMEvents(); - }, + return { + unitRange: interval, + count: count, + unitName: unit[0] + }; + }; + /** + * Methods defined on the Axis prototype + */ - /** - * Add crossbrowser support for chartX and chartY - * @param {Object} e The event object in standard browsers - */ - normalize: function (e, chartPosition) { - var chartX, - chartY, - ePos; + /** + * Set the tick positions of a logarithmic axis + */ + Axis.prototype.getLogTickPositions = function (interval, min, max, minor) { + var axis = this, + options = axis.options, + axisLength = axis.len, + // Since we use this method for both major and minor ticks, + // use a local variable and return the result + positions = []; - // common IE normalizing - e = e || window.event; + // Reset + if (!minor) { + axis._minorAutoInterval = null; + } - // Framework specific normalizing (#1165) - e = washMouseEvent(e); + // First case: All ticks fall on whole logarithms: 1, 10, 100 etc. + if (interval >= 0.5) { + interval = mathRound(interval); + positions = axis.getLinearTickPositions(interval, min, max); - // More IE normalizing, needs to go after washMouseEvent - if (!e.target) { - e.target = e.srcElement; - } - - // iOS (#2757) - ePos = e.touches ? (e.touches.length ? e.touches.item(0) : e.changedTouches[0]) : e; + // Second case: We need intermediary ticks. For example + // 1, 2, 4, 6, 8, 10, 20, 40 etc. + } else if (interval >= 0.08) { + var roundedMin = mathFloor(min), + intermediate, + i, + j, + len, + pos, + lastPos, + break2; - // Get mouse position - if (!chartPosition) { - this.chartPosition = chartPosition = offset(this.chart.container); - } + if (interval > 0.3) { + intermediate = [1, 2, 4]; + } else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc + intermediate = [1, 2, 4, 6, 8]; + } else { // 0.1 equals ten minor ticks per 1, 10, 100 etc + intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + } - // chartX and chartY - if (ePos.pageX === UNDEFINED) { // IE < 9. #886. - chartX = mathMax(e.x, e.clientX - chartPosition.left); // #2005, #2129: the second case is - // for IE10 quirks mode within framesets - chartY = e.y; - } else { - chartX = ePos.pageX - chartPosition.left; - chartY = ePos.pageY - chartPosition.top; - } + for (i = roundedMin; i < max + 1 && !break2; i++) { + len = intermediate.length; + for (j = 0; j < len && !break2; j++) { + pos = log2lin(lin2log(i) * intermediate[j]); + if (pos > min && (!minor || lastPos <= max) && lastPos !== UNDEFINED) { // #1670, lastPos is #3113 + positions.push(lastPos); + } - return extend(e, { - chartX: mathRound(chartX), - chartY: mathRound(chartY) - }); - }, + if (lastPos > max) { + break2 = true; + } + lastPos = pos; + } + } - /** - * Get the click position in terms of axis values. - * - * @param {Object} e A pointer event - */ - getCoordinates: function (e) { - var coordinates = { - xAxis: [], - yAxis: [] - }; + // Third case: We are so deep in between whole logarithmic values that + // we might as well handle the tick positions like a linear axis. For + // example 1.01, 1.02, 1.03, 1.04. + } else { + var realMin = lin2log(min), + realMax = lin2log(max), + tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'], + filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption, + tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1), + totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength; - each(this.chart.axes, function (axis) { - coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({ - axis: axis, - value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY']) - }); - }); - return coordinates; - }, - - /** - * With line type charts with a single tracker, get the point closest to the mouse. - * Run Point.onMouseOver and display tooltip for the point or points. - */ - runPointActions: function (e) { + interval = pick( + filteredTickIntervalOption, + axis._minorAutoInterval, + (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1) + ); - var pointer = this, - chart = pointer.chart, - series = chart.series, - tooltip = chart.tooltip, - shared = tooltip ? tooltip.shared : false, - followPointer, - hoverPoint = chart.hoverPoint, - hoverSeries = chart.hoverSeries, - i, - distance = Number.MAX_VALUE, // #4511 - anchor, - noSharedTooltip, - stickToHoverSeries, - directTouch, - kdpoints = [], - kdpoint, - kdpointT; + interval = normalizeTickInterval( + interval, + null, + getMagnitude(interval) + ); - // For hovering over the empty parts of the plot area (hoverSeries is undefined). - // If there is one series with point tracking (combo chart), don't go to nearest neighbour. - if (!shared && !hoverSeries) { - for (i = 0; i < series.length; i++) { - if (series[i].directTouch || !series[i].options.stickyTracking) { - series = []; - } - } - } + positions = map(axis.getLinearTickPositions( + interval, + realMin, + realMax + ), log2lin); - // If it has a hoverPoint and that series requires direct touch (like columns, #3899), or we're on - // a noSharedTooltip series among shared tooltip series (#4546), use the hoverPoint . Otherwise, - // search the k-d tree. - stickToHoverSeries = hoverSeries && (shared ? hoverSeries.noSharedTooltip : hoverSeries.directTouch); - if (stickToHoverSeries && hoverPoint) { - kdpoint = hoverPoint; + if (!minor) { + axis._minorAutoInterval = interval / 5; + } + } - // Handle shared tooltip or cases where a series is not yet hovered - } else { - // Find nearest points on all series - each(series, function (s) { - // Skip hidden series - noSharedTooltip = s.noSharedTooltip && shared; - directTouch = !shared && s.directTouch; - if (s.visible && !noSharedTooltip && !directTouch && pick(s.options.enableMouseTracking, true)) { // #3821 - kdpointT = s.searchPoint(e, !noSharedTooltip && s.kdDimensions === 1); // #3828 - if (kdpointT) { - kdpoints.push(kdpointT); - } - } - }); - // Find absolute nearest point - each(kdpoints, function (p) { - if (p && typeof p.dist === 'number' && p.dist < distance) { - distance = p.dist; - kdpoint = p; - } - }); - } + // Set the axis-level tickInterval variable + if (!minor) { + axis.tickInterval = interval; + } + return positions; + };/** + * The tooltip object + * @param {Object} chart The chart instance + * @param {Object} options Tooltip options + */ + var Tooltip = Highcharts.Tooltip = function () { + this.init.apply(this, arguments); + }; - // Refresh tooltip for kdpoint if new hover point or tooltip was hidden // #3926, #4200 - if (kdpoint && (kdpoint !== this.prevKDPoint || (tooltip && tooltip.isHidden))) { - // Draw tooltip if necessary - if (shared && !kdpoint.series.noSharedTooltip) { - i = kdpoints.length; - while (i--) { - if (kdpoints[i].clientX !== kdpoint.clientX || kdpoints[i].series.noSharedTooltip) { - kdpoints.splice(i, 1); - } - } - if (kdpoints.length && tooltip) { - tooltip.refresh(kdpoints, e); - } + Tooltip.prototype = { - // Do mouseover on all points (#3919, #3985, #4410) - each(kdpoints, function (point) { - point.onMouseOver(e, point !== ((hoverSeries && hoverSeries.directTouch && hoverPoint) || kdpoint)); - }); - } else { - if (tooltip) { - tooltip.refresh(kdpoint, e); - } - if(!hoverSeries || !hoverSeries.directTouch) { // #4448 - kdpoint.onMouseOver(e); - } - } - this.prevKDPoint = kdpoint; - - // Update positions (regardless of kdpoint or hoverPoint) - } else { - followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer; - if (tooltip && followPointer && !tooltip.isHidden) { - anchor = tooltip.getAnchor([{}], e); - tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] }); - } - } + init: function (chart, options) { - // Start the event listener to pick up the tooltip - if (tooltip && !pointer._onDocumentMouseMove) { - pointer._onDocumentMouseMove = function (e) { - if (charts[hoverChartIndex]) { - charts[hoverChartIndex].pointer.onDocumentMouseMove(e); - } - }; - addEvent(doc, 'mousemove', pointer._onDocumentMouseMove); - } - - // Crosshair - each(chart.axes, function (axis) { - axis.drawCrosshair(e, pick(kdpoint, hoverPoint)); - }); - + var borderWidth = options.borderWidth, + style = options.style, + padding = pInt(style.padding); - }, + // Save the chart and options + this.chart = chart; + this.options = options; + // Keep track of the current series + //this.currentSeries = UNDEFINED; + // List of crosshairs + this.crosshairs = []; - /** - * Reset the tracking by hiding the tooltip, the hover series state and the hover point - * - * @param allowMove {Boolean} Instead of destroying the tooltip altogether, allow moving it if possible - */ - reset: function (allowMove, delay) { - var pointer = this, - chart = pointer.chart, - hoverSeries = chart.hoverSeries, - hoverPoint = chart.hoverPoint, - hoverPoints = chart.hoverPoints, - tooltip = chart.tooltip, - tooltipPoints = tooltip && tooltip.shared ? hoverPoints : hoverPoint; - - // Narrow in allowMove - allowMove = allowMove && tooltip && tooltipPoints; - - // Check if the points have moved outside the plot area, #1003 - if (allowMove && splat(tooltipPoints)[0].plotX === UNDEFINED) { - allowMove = false; - } - // Just move the tooltip, #349 - if (allowMove) { - tooltip.refresh(tooltipPoints); - if (hoverPoint) { // #2500 - hoverPoint.setState(hoverPoint.state, true); - each(chart.axes, function (axis) { - if (pick(axis.options.crosshair && axis.options.crosshair.snap, true)) { - axis.drawCrosshair(null, hoverPoint); - } else { - axis.hideCrosshair(); - } - }); - - } + // Current values of x and y when animating + this.now = { x: 0, y: 0 }; - // Full reset - } else { + // The tooltip is initially hidden + this.isHidden = true; - if (hoverPoint) { - hoverPoint.onMouseOut(); - } - if (hoverPoints) { - each(hoverPoints, function (point) { - point.setState(); - }); - } + // create the label + this.label = chart.renderer.label('', 0, 0, options.shape || 'callout', null, null, options.useHTML, null, 'tooltip') + .attr({ + padding: padding, + fill: options.backgroundColor, + 'stroke-width': borderWidth, + r: options.borderRadius, + zIndex: 8 + }) + .css(style) + .css({ padding: 0 }) // Remove it from VML, the padding is applied as an attribute instead (#1117) + .add() + .attr({ y: -9999 }); // #2301, #2657 - if (hoverSeries) { - hoverSeries.onMouseOut(); - } + // When using canVG the shadow shows up as a gray circle + // even if the tooltip is hidden. + if (!useCanVG) { + this.label.shadow(options.shadow); + } - if (tooltip) { - tooltip.hide(delay); - } + // Public property for getting the shared state. + this.shared = options.shared; + }, - if (pointer._onDocumentMouseMove) { - removeEvent(doc, 'mousemove', pointer._onDocumentMouseMove); - pointer._onDocumentMouseMove = null; - } + /** + * Destroy the tooltip and its elements. + */ + destroy: function () { + // Destroy and clear local variables + if (this.label) { + this.label = this.label.destroy(); + } + clearTimeout(this.hideTimer); + clearTimeout(this.tooltipTimeout); + }, - // Remove crosshairs - each(chart.axes, function (axis) { - axis.hideCrosshair(); - }); - - pointer.hoverX = chart.hoverPoints = chart.hoverPoint = null; + /** + * Provide a soft movement for the tooltip + * + * @param {Number} x + * @param {Number} y + * @private + */ + move: function (x, y, anchorX, anchorY) { + var tooltip = this, + now = tooltip.now, + animate = tooltip.options.animation !== false && !tooltip.isHidden && + // When we get close to the target position, abort animation and land on the right place (#3056) + (mathAbs(x - now.x) > 1 || mathAbs(y - now.y) > 1), + skipAnchor = tooltip.followPointer || tooltip.len > 1; - } - }, + // Get intermediate values for animation + extend(now, { + x: animate ? (2 * now.x + x) / 3 : x, + y: animate ? (now.y + y) / 2 : y, + anchorX: skipAnchor ? UNDEFINED : animate ? (2 * now.anchorX + anchorX) / 3 : anchorX, + anchorY: skipAnchor ? UNDEFINED : animate ? (now.anchorY + anchorY) / 2 : anchorY + }); - /** - * Scale series groups to a certain scale and translation - */ - scaleGroups: function (attribs, clip) { + // Move to the intermediate value + tooltip.label.attr(now); - var chart = this.chart, - seriesAttribs; - // Scale each series - each(chart.series, function (series) { - seriesAttribs = attribs || series.getPlotBox(); // #1701 - if (series.xAxis && series.xAxis.zoomEnabled) { - series.group.attr(seriesAttribs); - if (series.markerGroup) { - series.markerGroup.attr(seriesAttribs); - series.markerGroup.clip(clip ? chart.clipRect : null); - } - if (series.dataLabelsGroup) { - series.dataLabelsGroup.attr(seriesAttribs); - } - } - }); - - // Clip - chart.clipRect.attr(clip || chart.clipBox); - }, + // Run on next tick of the mouse tracker + if (animate) { - /** - * Start a drag operation - */ - dragStart: function (e) { - var chart = this.chart; + // Never allow two timeouts + clearTimeout(this.tooltipTimeout); - // Record the start position - chart.mouseIsDown = e.type; - chart.cancelClick = false; - chart.mouseDownX = this.mouseDownX = e.chartX; - chart.mouseDownY = this.mouseDownY = e.chartY; - }, + // 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. + if (tooltip) { + tooltip.move(x, y, anchorX, anchorY); + } + }, 32); - /** - * Perform a drag operation in response to a mousemove event while the mouse is down - */ - drag: function (e) { + } + }, - var chart = this.chart, - chartOptions = chart.options.chart, - chartX = e.chartX, - chartY = e.chartY, - zoomHor = this.zoomHor, - zoomVert = this.zoomVert, - plotLeft = chart.plotLeft, - plotTop = chart.plotTop, - plotWidth = chart.plotWidth, - plotHeight = chart.plotHeight, - clickedInside, - size, - selectionMarker = this.selectionMarker, - mouseDownX = this.mouseDownX, - mouseDownY = this.mouseDownY, - panKey = chartOptions.panKey && e[chartOptions.panKey + 'Key']; + /** + * Hide the tooltip + */ + hide: function (delay) { + 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.isHidden = true; + }, delay); + } + }, - // If the device supports both touch and mouse (like IE11), and we are touch-dragging - // inside the plot area, don't handle the mouse event. #4339. - if (selectionMarker && selectionMarker.touch) { - return; - } + /** + * Extendable method to get the anchor position of the tooltip + * from a point or set of points + */ + getAnchor: function (points, mouseEvent) { + var ret, + chart = this.chart, + inverted = chart.inverted, + plotTop = chart.plotTop, + plotLeft = chart.plotLeft, + plotX = 0, + plotY = 0, + yAxis, + xAxis; - // If the mouse is outside the plot area, adjust to cooordinates - // inside to prevent the selection marker from going outside - if (chartX < plotLeft) { - chartX = plotLeft; - } else if (chartX > plotLeft + plotWidth) { - chartX = plotLeft + plotWidth; - } + points = splat(points); - if (chartY < plotTop) { - chartY = plotTop; - } else if (chartY > plotTop + plotHeight) { - chartY = plotTop + plotHeight; - } - - // determine if the mouse has moved more than 10px - this.hasDragged = Math.sqrt( - Math.pow(mouseDownX - chartX, 2) + - Math.pow(mouseDownY - chartY, 2) - ); - - if (this.hasDragged > 10) { - clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop); + // Pie uses a special tooltipPos + ret = points[0].tooltipPos; - // make a selection - if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside && !panKey) { - if (!selectionMarker) { - this.selectionMarker = selectionMarker = chart.renderer.rect( - plotLeft, - plotTop, - zoomHor ? 1 : plotWidth, - zoomVert ? 1 : plotHeight, - 0 - ) - .attr({ - fill: chartOptions.selectionMarkerFill || 'rgba(69,114,167,0.25)', - zIndex: 7 - }) - .add(); - } - } + // When tooltip follows mouse, relate the position to the mouse + if (this.followPointer && mouseEvent) { + if (mouseEvent.chartX === UNDEFINED) { + mouseEvent = chart.pointer.normalize(mouseEvent); + } + ret = [ + mouseEvent.chartX - chart.plotLeft, + mouseEvent.chartY - plotTop + ]; + } + // When shared, use the average position + if (!ret) { + each(points, function (point) { + yAxis = point.series.yAxis; + xAxis = point.series.xAxis; + plotX += point.plotX + (!inverted && xAxis ? xAxis.left - plotLeft : 0); + plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) + + (!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151 + }); - // adjust the width of the selection marker - if (selectionMarker && zoomHor) { - size = chartX - mouseDownX; - selectionMarker.attr({ - width: mathAbs(size), - x: (size > 0 ? 0 : size) + mouseDownX - }); - } - // adjust the height of the selection marker - if (selectionMarker && zoomVert) { - size = chartY - mouseDownY; - selectionMarker.attr({ - height: mathAbs(size), - y: (size > 0 ? 0 : size) + mouseDownY - }); - } + plotX /= points.length; + plotY /= points.length; - // panning - if (clickedInside && !selectionMarker && chartOptions.panning) { - chart.pan(e, chartOptions.panning); - } - } - }, + ret = [ + inverted ? chart.plotWidth - plotY : plotX, + this.shared && !inverted && points.length > 1 && mouseEvent ? + mouseEvent.chartY - plotTop : // place shared tooltip next to the mouse (#424) + inverted ? chart.plotHeight - plotX : plotY + ]; + } - /** - * On mouse up or touch end across the entire document, drop the selection. - */ - drop: function (e) { - var pointer = this, - chart = this.chart, - hasPinched = this.hasPinched; + return map(ret, mathRound); + }, - if (this.selectionMarker) { - var selectionData = { - xAxis: [], - yAxis: [], - originalEvent: e.originalEvent || e - }, - selectionBox = this.selectionMarker, - selectionLeft = selectionBox.attr ? selectionBox.attr('x') : selectionBox.x, - selectionTop = selectionBox.attr ? selectionBox.attr('y') : selectionBox.y, - selectionWidth = selectionBox.attr ? selectionBox.attr('width') : selectionBox.width, - selectionHeight = selectionBox.attr ? selectionBox.attr('height') : selectionBox.height, - runZoom; + /** + * Place the tooltip in a chart without spilling over + * and not covering the point it self. + */ + getPosition: function (boxWidth, boxHeight, point) { - // a selection has been made - if (this.hasDragged || hasPinched) { + 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], + // The far side is right or bottom + preferFarSide = pick(point.ttBelow, (chart.inverted && !point.negative) || (!chart.inverted && point.negative)), + /** + * Handle the preferred dimension. When the preferred dimension is tooltip + * on top or bottom of the point, it will look for space there. + */ + firstDimension = function (dim, outerSize, innerSize, point, min, max) { + var roomLeft = innerSize < point - distance, + roomRight = point + distance + innerSize < outerSize, + alignedLeft = point - distance - innerSize, + alignedRight = point + distance; - // record each axis' min and max - each(chart.axes, function (axis) { - if (axis.zoomEnabled && defined(axis.min) && (hasPinched || pointer[{ xAxis: 'zoomX', yAxis: 'zoomY' }[axis.coll]])) { // #859, #3569 - var horiz = axis.horiz, - minPixelPadding = e.type === 'touchend' ? axis.minPixelPadding: 0, // #1207, #3075 - selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop) + minPixelPadding), - selectionMax = axis.toValue((horiz ? selectionLeft + selectionWidth : selectionTop + selectionHeight) - minPixelPadding); + if (preferFarSide && roomRight) { + ret[dim] = alignedRight; + } else if (!preferFarSide && roomLeft) { + ret[dim] = alignedLeft; + } else if (roomLeft) { + ret[dim] = mathMin(max - innerSize, alignedLeft - h < 0 ? alignedLeft : alignedLeft - h); + } else if (roomRight) { + ret[dim] = mathMax(min, alignedRight + h + innerSize > outerSize ? alignedRight : alignedRight + h); + } else { + return false; + } + }, + /** + * Handle the secondary dimension. If the preferred dimension is tooltip + * on top or bottom of the point, the second dimension is to align the tooltip + * above the point, trying to align center but allowing left or right + * align within the chart box. + */ + secondDimension = function (dim, outerSize, innerSize, point) { + var retVal; - selectionData[axis.coll].push({ - axis: axis, - min: mathMin(selectionMin, selectionMax), // for reversed axes - max: mathMax(selectionMin, selectionMax) - }); - runZoom = true; - } - }); - if (runZoom) { - fireEvent(chart, 'selection', selectionData, function (args) { - chart.zoom(extend(args, hasPinched ? { animation: false } : null)); - }); - } + // Too close to the edge, return false and swap dimensions + if (point < distance || point > outerSize - distance) { + retVal = false; + // Align left/top + } else if (point < innerSize / 2) { + ret[dim] = 1; + // Align right/bottom + } else if (point > outerSize - innerSize / 2) { + ret[dim] = outerSize - innerSize - 2; + // Align center + } else { + ret[dim] = point - innerSize / 2; + } + return retVal; + }, + /** + * Swap the dimensions + */ + swap = function (count) { + var temp = first; + first = second; + second = temp; + swapped = count; + }, + run = function () { + if (firstDimension.apply(0, first) !== false) { + if (secondDimension.apply(0, second) === false && !swapped) { + swap(true); + run(); + } + } else if (!swapped) { + swap(true); + run(); + } else { + ret.x = ret.y = 0; + } + }; - } - this.selectionMarker = this.selectionMarker.destroy(); + // Under these conditions, prefer the tooltip on the side of the point + if (chart.inverted || this.len > 1) { + swap(); + } + run(); - // Reset scaling preview - if (hasPinched) { - this.scaleGroups(); - } - } + return ret; - // Reset all - if (chart) { // it may be destroyed on mouse up - #877 - css(chart.container, { cursor: chart._cursor }); - chart.cancelClick = this.hasDragged > 10; // #370 - chart.mouseIsDown = this.hasDragged = this.hasPinched = false; - this.pinchDown = []; - } - }, + }, - onContainerMouseDown: function (e) { + /** + * In case no user defined formatter is given, this will be used. Note that the context + * here is an object holding point, series, x, y etc. + */ + defaultFormatter: function (tooltip) { + var items = this.points || splat(this), + s; - e = this.normalize(e); + // build the header + s = [tooltip.tooltipFooterHeaderFormatter(items[0])]; //#3397: abstraction to enable formatting of footer and header - // issue #295, dragging not always working in Firefox - if (e.preventDefault) { - e.preventDefault(); - } - - this.dragStart(e); - }, + // 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 - onDocumentMouseUp: function (e) { - if (charts[hoverChartIndex]) { - charts[hoverChartIndex].pointer.drop(e); - } - }, + return s.join(''); + }, - /** - * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea. - * Issue #149 workaround. The mouseleave event does not always fire. - */ - onDocumentMouseMove: function (e) { - var chart = this.chart, - chartPosition = this.chartPosition; + /** + * Refresh the tooltip's text and position. + * @param {Object} point + */ + refresh: function (point, mouseEvent) { + var tooltip = this, + chart = tooltip.chart, + label = tooltip.label, + options = tooltip.options, + x, + y, + anchor, + textConfig = {}, + text, + pointConfig = [], + formatter = options.formatter || tooltip.defaultFormatter, + hoverPoints = chart.hoverPoints, + borderColor, + shared = tooltip.shared, + currentSeries; - e = this.normalize(e, chartPosition); + clearTimeout(this.hideTimer); - // If we're outside, hide the tooltip - if (chartPosition && !this.inClass(e.target, 'highcharts-tracker') && - !chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { - this.reset(); - } - }, + // get the reference point coordinates (pie charts use tooltipPos) + tooltip.followPointer = splat(point)[0].series.tooltipOptions.followPointer; + anchor = tooltip.getAnchor(point, mouseEvent); + x = anchor[0]; + y = anchor[1]; - /** - * When mouse leaves the container, hide the tooltip. - */ - onContainerMouseLeave: function () { - var chart = charts[hoverChartIndex]; - if (chart) { - chart.pointer.reset(); - chart.pointer.chartPosition = null; // also reset the chart position, used in #149 fix - } - }, + // shared tooltip, array is sent over + if (shared && !(point.series && point.series.noSharedTooltip)) { - // The mousemove, touchmove and touchstart event handler - onContainerMouseMove: function (e) { + // hide previous hoverPoints and set new - var chart = this.chart; + chart.hoverPoints = point; + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } - hoverChartIndex = chart.index; + each(point, function (item) { + item.setState(HOVER_STATE); - e = this.normalize(e); - e.returnValue = false; // #2251, #3224 - - if (chart.mouseIsDown === 'mousedown') { - this.drag(e); - } - - // Show the tooltip and run mouse over events (#977) - if ((this.inClass(e.target, 'highcharts-tracker') || - chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) && !chart.openMenu) { - this.runPointActions(e); - } - }, + pointConfig.push(item.getLabelConfig()); + }); - /** - * Utility to detect whether an element has, or has a parent with, a specific - * class name. Used on detection of tracker objects and on deciding whether - * hovering the tooltip should cause the active series to mouse out. - */ - inClass: function (element, className) { - var elemClassName; - while (element) { - elemClassName = attr(element, 'class'); - if (elemClassName) { - if (elemClassName.indexOf(className) !== -1) { - return true; - } else if (elemClassName.indexOf(PREFIX + 'container') !== -1) { - return false; - } - } - element = element.parentNode; - } - }, + textConfig = { + x: point[0].category, + y: point[0].y + }; + textConfig.points = pointConfig; + this.len = pointConfig.length; + point = point[0]; - onTrackerMouseOut: function (e) { - var series = this.chart.hoverSeries, - relatedTarget = e.relatedTarget || e.toElement; - - if (series && !series.options.stickyTracking && - !this.inClass(relatedTarget, PREFIX + 'tooltip') && - !this.inClass(relatedTarget, PREFIX + 'series-' + series.index)) { // #2499, #4465 - series.onMouseOut(); - } - }, + // single point tooltip + } else { + textConfig = point.getLabelConfig(); + } + text = formatter.call(textConfig, tooltip); - onContainerClick: function (e) { - var chart = this.chart, - hoverPoint = chart.hoverPoint, - plotLeft = chart.plotLeft, - plotTop = chart.plotTop; - - e = this.normalize(e); - e.originalEvent = e; // #3913 + // register the current series + currentSeries = point.series; + this.distance = pick(currentSeries.tooltipOptions.distance, 16); - if (!chart.cancelClick) { - - // On tracker click, fire the series and point events. #783, #1583 - if (hoverPoint && this.inClass(e.target, PREFIX + 'tracker')) { + // update the inner HTML + if (text === false) { + this.hide(); + } else { - // the series click event - fireEvent(hoverPoint.series, 'click', extend(e, { - point: hoverPoint - })); + // show it + if (tooltip.isHidden) { + stop(label); + label.attr('opacity', 1).show(); + } - // the point click event - if (chart.hoverPoint) { // it may be destroyed (#1844) - hoverPoint.firePointEvent('click', e); - } + // update text + label.attr({ + text: text + }); - // When clicking outside a tracker, fire a chart event - } else { - extend(e, this.getCoordinates(e)); + // set the stroke color of the box + borderColor = options.borderColor || point.color || currentSeries.color || '#606060'; + label.attr({ + stroke: borderColor + }); + tooltip.updatePosition({ + plotX: x, + plotY: y, + negative: point.negative, + ttBelow: point.ttBelow, + h: anchor[2] || 0 + }); - // fire a click event in the chart - if (chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) { - fireEvent(chart, 'click', e); - } - } + this.isHidden = false; + } + fireEvent(chart, 'tooltipRefresh', { + text: text, + x: x + chart.plotLeft, + y: y + chart.plotTop, + borderColor: borderColor + }); + }, + /** + * Find the new position and perform the move + */ + updatePosition: function (point) { + var chart = this.chart, + label = this.label, + pos = (this.options.positioner || this.getPosition).call( + this, + label.width, + label.height, + point + ); - } - }, + // do the move + this.move( + mathRound(pos.x), + mathRound(pos.y || 0), // can be undefined (#3977) + point.plotX + chart.plotLeft, + point.plotY + chart.plotTop + ); + }, - /** - * Set the JS DOM events on the container and document. This method should contain - * a one-to-one assignment between methods and their handlers. Any advanced logic should - * be moved to the handler reflecting the event's name. - */ - setDOMEvents: function () { + /** + * Get the best X date format based on the closest point range on the axis. + */ + getXDateFormat: function (point, options, xAxis) { + var xDateFormat, + dateTimeLabelFormats = options.dateTimeLabelFormats, + closestPointRange = xAxis && xAxis.closestPointRange, + n, + blank = '01-01 00:00:00.000', + strpos = { + millisecond: 15, + second: 12, + minute: 9, + hour: 6, + day: 3 + }, + date, + lastN = 'millisecond'; // for sub-millisecond data, #4223 - var pointer = this, - container = pointer.chart.container; + if (closestPointRange) { + date = dateFormat('%m-%d %H:%M:%S.%L', point.x); + for (n in timeUnits) { - container.onmousedown = function (e) { - pointer.onContainerMouseDown(e); - }; - container.onmousemove = function (e) { - pointer.onContainerMouseMove(e); - }; - container.onclick = function (e) { - pointer.onContainerClick(e); - }; - addEvent(container, 'mouseleave', pointer.onContainerMouseLeave); - if (chartCount === 1) { - addEvent(doc, 'mouseup', pointer.onDocumentMouseUp); - } - if (hasTouch) { - container.ontouchstart = function (e) { - pointer.onContainerTouchStart(e); - }; - container.ontouchmove = function (e) { - pointer.onContainerTouchMove(e); - }; - if (chartCount === 1) { - addEvent(doc, 'touchend', pointer.onDocumentTouchEnd); - } - } - - }, + // If the range is exactly one week and we're looking at a Sunday/Monday, go for the week format + if (closestPointRange === timeUnits.week && +dateFormat('%w', point.x) === xAxis.options.startOfWeek && + date.substr(6) === blank.substr(6)) { + n = 'week'; + break; + } - /** - * Destroys the Pointer object and disconnects DOM events. - */ - destroy: function () { - var prop; + // The first format that is too great for the range + if (timeUnits[n] > closestPointRange) { + n = lastN; + break; + } - removeEvent(this.chart.container, 'mouseleave', this.onContainerMouseLeave); - if (!chartCount) { - removeEvent(doc, 'mouseup', this.onDocumentMouseUp); - removeEvent(doc, 'touchend', this.onDocumentTouchEnd); - } + // If the point is placed every day at 23:59, we need to show + // the minutes as well. #2637. + if (strpos[n] && date.substr(strpos[n]) !== blank.substr(strpos[n])) { + break; + } - // memory and CPU leak - clearInterval(this.tooltipTimeout); + // Weeks are outside the hierarchy, only apply them on Mondays/Sundays like in the first condition + if (n !== 'week') { + lastN = n; + } + } - for (prop in this) { - this[prop] = null; - } - } -}; + if (n) { + xDateFormat = dateTimeLabelFormats[n]; + } + } else { + xDateFormat = dateTimeLabelFormats.day; + } + return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581 + }, -/* Support for touch devices */ -extend(Highcharts.Pointer.prototype, { + /** + * Format the footer/header of the tooltip + * #3397: abstraction to enable formatting of footer and header + */ + tooltipFooterHeaderFormatter: function (point, isFooter) { + var footOrHead = isFooter ? 'footer' : 'header', + series = point.series, + tooltipOptions = series.tooltipOptions, + xDateFormat = tooltipOptions.xDateFormat, + xAxis = series.xAxis, + isDateTime = xAxis && xAxis.options.type === 'datetime' && isNumber(point.key), + formatString = tooltipOptions[footOrHead + 'Format']; - /** - * Run translation operations - */ - pinchTranslate: function (pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) { - if (this.zoomHor || this.pinchHor) { - this.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); - } - if (this.zoomVert || this.pinchVert) { - this.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); - } - }, + // Guess the best date format based on the closest point distance (#568, #3418) + if (isDateTime && !xDateFormat) { + xDateFormat = this.getXDateFormat(point, tooltipOptions, xAxis); + } - /** - * Run translation operations for each direction (horizontal and vertical) independently - */ - 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', - plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')], - selectionWH, - selectionXY, - clipXY, - scale = forcedScale || 1, - inverted = chart.inverted, - bounds = chart.bounds[horiz ? 'h' : 'v'], - singleTouch = pinchDown.length === 1, - touch0Start = pinchDown[0][sChartXY], - touch0Now = touches[0][sChartXY], - touch1Start = !singleTouch && pinchDown[1][sChartXY], - touch1Now = !singleTouch && touches[1][sChartXY], - outOfBounds, - transformScale, - scaleKey, - setScale = function () { - if (!singleTouch && mathAbs(touch0Start - touch1Start) > 20) { // Don't zoom if fingers are too close on this axis - scale = forcedScale || mathAbs(touch0Now - touch1Now) / mathAbs(touch0Start - touch1Start); - } - - clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start; - selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale; - }; + // Insert the footer date format if any + if (isDateTime && xDateFormat) { + formatString = formatString.replace('{point.key}', '{point.key:' + xDateFormat + '}'); + } - // Set the scale, first pass - setScale(); + return format(formatString, { + point: point, + series: series + }); + }, - selectionXY = clipXY; // the clip position (x or y) is altered if out of bounds, the selection position is not + /** + * Build the body (lines) of the tooltip by iterating over the items and returning one entry for each item, + * 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); + }); + } - // Out of bounds - if (selectionXY < bounds.min) { - selectionXY = bounds.min; - outOfBounds = true; - } else if (selectionXY + selectionWH > bounds.max) { - selectionXY = bounds.max - selectionWH; - outOfBounds = true; - } - - // Is the chart dragged off its bounds, determined by dataMin and dataMax? - if (outOfBounds) { + }; - // Modify the touchNow position in order to create an elastic drag movement. This indicates - // to the user that the chart is responsive but can't be dragged further. - touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]); - if (!singleTouch) { - touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]); - } + var hoverChartIndex; - // Set the scale, second pass to adapt to the modified touchNow positions - setScale(); + // Global flag for touch support + hasTouch = doc.documentElement.ontouchstart !== UNDEFINED; - } else { - lastValidTouch[xy] = [touch0Now, touch1Now]; - } + /** + * 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. + * @param {Object} chart The Chart instance + * @param {Object} options The root options object + */ + var Pointer = Highcharts.Pointer = function (chart, options) { + this.init(chart, options); + }; - // Set geometry for clipping, selection and transformation - if (!inverted) { // TODO: implement clipping for inverted charts - clip[xy] = clipXY - plotLeftTop; - clip[wh] = selectionWH; - } - scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY; - transformScale = inverted ? 1 / scale : scale; + Pointer.prototype = { + /** + * Initialize Pointer + */ + init: function (chart, options) { - selectionMarker[wh] = selectionWH; - selectionMarker[xy] = selectionXY; - transform[scaleKey] = scale; - transform['translate' + XY] = (transformScale * plotLeftTop) + (touch0Now - (transformScale * touch0Start)); - }, - - /** - * Handle touch events with two touches - */ - pinch: function (e) { + var chartOptions = options.chart, + chartEvents = chartOptions.events, + zoomType = useCanVG ? '' : chartOptions.zoomType, + inverted = chart.inverted, + zoomX, + zoomY; - var self = this, - chart = self.chart, - pinchDown = self.pinchDown, - touches = e.touches, - touchesLength = touches.length, - lastValidTouch = self.lastValidTouch, - hasZoom = self.hasZoom, - selectionMarker = self.selectionMarker, - transform = {}, - fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, PREFIX + 'tracker') && - chart.runTrackerClick) || self.runChartClick), - clip = {}; + // Store references + this.options = options; + this.chart = chart; - // Don't initiate panning until the user has pinched. This prevents us from - // blocking page scrolling as users scroll down a long page (#4210). - if (touchesLength > 1) { - self.initiated = true; - } + // Zoom status + 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; - // On touch devices, only proceed to trigger click if a handler is defined - if (hasZoom && self.initiated && !fireClickEvent) { - e.preventDefault(); - } - - // Normalize each touch - map(touches, function (e) { - return self.normalize(e); - }); - - // Register the touch start position - if (e.type === 'touchstart') { - each(touches, function (e, i) { - pinchDown[i] = { chartX: e.chartX, chartY: e.chartY }; - }); - lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && pinchDown[1].chartX]; - lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && pinchDown[1].chartY]; + // Do we need to handle click on a touch device? + this.runChartClick = chartEvents && !!chartEvents.click; - // Identify the data bounds in pixels - each(chart.axes, function (axis) { - if (axis.zoomEnabled) { - var bounds = chart.bounds[axis.horiz ? 'h' : 'v'], - minPixelPadding = axis.minPixelPadding, - min = axis.toPixels(pick(axis.options.min, axis.dataMin)), - max = axis.toPixels(pick(axis.options.max, axis.dataMax)), - absMin = mathMin(min, max), - absMax = mathMax(min, max); + this.pinchDown = []; + this.lastValidTouch = {}; - // Store the bounds for use in the touchmove handler - bounds.min = mathMin(axis.pos, absMin - minPixelPadding); - bounds.max = mathMax(axis.pos + axis.len, absMax + minPixelPadding); - } - }); - self.res = true; // reset on next move - - // Event type is touchmove, handle panning and pinching - } else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first - + if (Highcharts.Tooltip && options.tooltip.enabled) { + chart.tooltip = new Tooltip(chart, options.tooltip); + this.followTouchMove = pick(options.tooltip.followTouchMove, true); + } - // Set the marker - if (!selectionMarker) { - self.selectionMarker = selectionMarker = extend({ - destroy: noop, - touch: true - }, chart.plotBox); - } - - self.pinchTranslate(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); + this.setDOMEvents(); + }, - self.hasPinched = hasZoom; + /** + * Add crossbrowser support for chartX and chartY + * @param {Object} e The event object in standard browsers + */ + normalize: function (e, chartPosition) { + var chartX, + chartY, + ePos; - // 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) { - self.res = false; - this.reset(false, 0); - } - } - }, + // common IE normalizing + e = e || window.event; - /** - * General touch handler shared by touchstart and touchmove. - */ - touch: function (e, start) { - var chart = this.chart; + // Framework specific normalizing (#1165) + e = washMouseEvent(e); - hoverChartIndex = chart.index; + // More IE normalizing, needs to go after washMouseEvent + if (!e.target) { + e.target = e.srcElement; + } - if (e.touches.length === 1) { + // iOS (#2757) + ePos = e.touches ? (e.touches.length ? e.touches.item(0) : e.changedTouches[0]) : e; - e = this.normalize(e); + // Get mouse position + if (!chartPosition) { + this.chartPosition = chartPosition = offset(this.chart.container); + } - if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop) && !chart.openMenu) { + // chartX and chartY + if (ePos.pageX === UNDEFINED) { // IE < 9. #886. + chartX = mathMax(e.x, e.clientX - chartPosition.left); // #2005, #2129: the second case is + // for IE10 quirks mode within framesets + chartY = e.y; + } else { + chartX = ePos.pageX - chartPosition.left; + chartY = ePos.pageY - chartPosition.top; + } - // Run mouse events and display tooltip etc - if (start) { - this.runPointActions(e); - } + return extend(e, { + chartX: mathRound(chartX), + chartY: mathRound(chartY) + }); + }, - this.pinch(e); + /** + * Get the click position in terms of axis values. + * + * @param {Object} e A pointer event + */ + getCoordinates: function (e) { + var coordinates = { + xAxis: [], + yAxis: [] + }; - } else if (start) { - // Hide the tooltip on touching outside the plot area (#1203) - this.reset(); - } + each(this.chart.axes, function (axis) { + coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({ + axis: axis, + value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY']) + }); + }); + return coordinates; + }, - } else if (e.touches.length === 2) { - this.pinch(e); - } - }, + /** + * With line type charts with a single tracker, get the point closest to the mouse. + * Run Point.onMouseOver and display tooltip for the point or points. + */ + runPointActions: function (e) { - onContainerTouchStart: function (e) { - this.touch(e, true); - }, + var pointer = this, + chart = pointer.chart, + series = chart.series, + tooltip = chart.tooltip, + shared = tooltip ? tooltip.shared : false, + followPointer, + hoverPoint = chart.hoverPoint, + hoverSeries = chart.hoverSeries, + i, + distance = Number.MAX_VALUE, // #4511 + anchor, + noSharedTooltip, + stickToHoverSeries, + directTouch, + pointDistance, + kdpoints = [], + kdpoint, + kdpointT; - onContainerTouchMove: function (e) { - this.touch(e); - }, + // For hovering over the empty parts of the plot area (hoverSeries is undefined). + // If there is one series with point tracking (combo chart), don't go to nearest neighbour. + if (!shared && !hoverSeries) { + for (i = 0; i < series.length; i++) { + if (series[i].directTouch || !series[i].options.stickyTracking) { + series = []; + } + } + } - onDocumentTouchEnd: function (e) { - if (charts[hoverChartIndex]) { - charts[hoverChartIndex].pointer.drop(e); - } - } + // If it has a hoverPoint and that series requires direct touch (like columns, #3899), or we're on + // a noSharedTooltip series among shared tooltip series (#4546), use the hoverPoint . Otherwise, + // search the k-d tree. + stickToHoverSeries = hoverSeries && (shared ? hoverSeries.noSharedTooltip : hoverSeries.directTouch); + if (stickToHoverSeries && hoverPoint) { + kdpoint = hoverPoint; -}); -if (win.PointerEvent || win.MSPointerEvent) { - - // The touches object keeps track of the points being touched at all times - var touches = {}, - hasPointerEvent = !!win.PointerEvent, - getWebkitTouches = function () { - var key, fake = []; - fake.item = function (i) { return this[i]; }; - for (key in touches) { - if (touches.hasOwnProperty(key)) { - fake.push({ - pageX: touches[key].pageX, - pageY: touches[key].pageY, - target: touches[key].target - }); - } - } - return fake; - }, - translateMSPointer = function (e, method, wktype, callback) { - var p; - e = e.originalEvent || e; - if ((e.pointerType === 'touch' || e.pointerType === e.MSPOINTER_TYPE_TOUCH) && charts[hoverChartIndex]) { - callback(e); - p = charts[hoverChartIndex].pointer; - p[method]({ - type: wktype, - target: e.currentTarget, - preventDefault: noop, - touches: getWebkitTouches() - }); - } - }; + // Handle shared tooltip or cases where a series is not yet hovered + } else { + // Find nearest points on all series + each(series, function (s) { + // Skip hidden series + noSharedTooltip = s.noSharedTooltip && shared; + directTouch = !shared && s.directTouch; + if (s.visible && !noSharedTooltip && !directTouch && pick(s.options.enableMouseTracking, true)) { // #3821 + kdpointT = s.searchPoint(e, !noSharedTooltip && s.kdDimensions === 1); // #3828 + if (kdpointT) { + kdpoints.push(kdpointT); + } + } + }); + // Find absolute nearest point + each(kdpoints, function (p) { + pointDistance = !shared && p.series.kdDimensions === 1 ? p.dist : p.distX; // #4645 - /** - * Extend the Pointer prototype with methods for each event handler and more - */ - extend(Pointer.prototype, { - onContainerPointerDown: function (e) { - translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function (e) { - touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY, target: e.currentTarget }; - }); - }, - onContainerPointerMove: function (e) { - translateMSPointer(e, 'onContainerTouchMove', 'touchmove', function (e) { - touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY }; - if (!touches[e.pointerId].target) { - touches[e.pointerId].target = e.currentTarget; - } - }); - }, - onDocumentPointerUp: function (e) { - translateMSPointer(e, 'onDocumentTouchEnd', 'touchend', function (e) { - delete touches[e.pointerId]; - }); - }, + if (p && typeof pointDistance === 'number' && pointDistance < distance) { + distance = pointDistance; + kdpoint = p; + } + }); + } - /** - * Add or remove the MS Pointer specific events - */ - batchMSEvents: function (fn) { - fn(this.chart.container, hasPointerEvent ? 'pointerdown' : 'MSPointerDown', this.onContainerPointerDown); - fn(this.chart.container, hasPointerEvent ? 'pointermove' : 'MSPointerMove', this.onContainerPointerMove); - fn(doc, hasPointerEvent ? 'pointerup' : 'MSPointerUp', this.onDocumentPointerUp); - } - }); + // Refresh tooltip for kdpoint if new hover point or tooltip was hidden // #3926, #4200 + if (kdpoint && (kdpoint !== this.prevKDPoint || (tooltip && tooltip.isHidden))) { + // Draw tooltip if necessary + if (shared && !kdpoint.series.noSharedTooltip) { + i = kdpoints.length; + while (i--) { + if (kdpoints[i].clientX !== kdpoint.clientX || kdpoints[i].series.noSharedTooltip) { + kdpoints.splice(i, 1); + } + } + if (kdpoints.length && tooltip) { + tooltip.refresh(kdpoints, e); + } - // Disable default IE actions for pinch and such on chart element - wrap(Pointer.prototype, 'init', function (proceed, chart, options) { - proceed.call(this, chart, options); - if (this.hasZoom) { // #4014 - css(chart.container, { - '-ms-touch-action': NONE, - 'touch-action': NONE - }); - } - }); + // Do mouseover on all points (#3919, #3985, #4410) + each(kdpoints, function (point) { + point.onMouseOver(e, point !== ((hoverSeries && hoverSeries.directTouch && hoverPoint) || kdpoint)); + }); + } else { + if (tooltip) { + tooltip.refresh(kdpoint, e); + } + if (!hoverSeries || !hoverSeries.directTouch) { // #4448 + kdpoint.onMouseOver(e); + } + } + this.prevKDPoint = kdpoint; - // Add IE specific touch events to chart - wrap(Pointer.prototype, 'setDOMEvents', function (proceed) { - proceed.apply(this); - if (this.hasZoom || this.followTouchMove) { - this.batchMSEvents(addEvent); - } - }); - // Destroy MS events also - wrap(Pointer.prototype, 'destroy', function (proceed) { - this.batchMSEvents(removeEvent); - proceed.call(this); - }); -} -/** - * The overview of the chart's series - */ -var Legend = Highcharts.Legend = function (chart, options) { - this.init(chart, options); -}; + // Update positions (regardless of kdpoint or hoverPoint) + } else { + followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer; + if (tooltip && followPointer && !tooltip.isHidden) { + anchor = tooltip.getAnchor([{}], e); + tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] }); + } + } -Legend.prototype = { - - /** - * Initialize the legend - */ - init: function (chart, options) { - - var legend = this, - itemStyle = options.itemStyle, - padding, - itemMarginTop = options.itemMarginTop || 0; - - this.options = options; + // Start the event listener to pick up the tooltip and crosshairs + if (!pointer._onDocumentMouseMove) { + pointer._onDocumentMouseMove = function (e) { + if (charts[hoverChartIndex]) { + charts[hoverChartIndex].pointer.onDocumentMouseMove(e); + } + }; + addEvent(doc, 'mousemove', pointer._onDocumentMouseMove); + } - if (!options.enabled) { - return; - } - - legend.itemStyle = itemStyle; - legend.itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle); - legend.itemMarginTop = itemMarginTop; - legend.padding = padding = pick(options.padding, 8); - legend.initialItemX = padding; - legend.initialItemY = padding - 5; // 5 is the number of pixels above the text - legend.maxItemWidth = 0; - legend.chart = chart; - legend.itemHeight = 0; - legend.symbolWidth = pick(options.symbolWidth, 16); - legend.pages = []; + // Crosshair + each(chart.axes, function (axis) { + axis.drawCrosshair(e, pick(kdpoint, hoverPoint)); + }); - // Render it - legend.render(); + }, - // move checkboxes - addEvent(legend.chart, 'endResize', function () { - legend.positionCheckboxes(); - }); - }, - /** - * Set the colors for the legend item - * @param {Object} item A Series or Point instance - * @param {Object} visible Dimmed or colored - */ - colorizeItem: function (item, visible) { - var legend = this, - options = legend.options, - legendItem = item.legendItem, - legendLine = item.legendLine, - legendSymbol = item.legendSymbol, - hiddenColor = legend.itemHiddenStyle.color, - textColor = visible ? options.itemStyle.color : hiddenColor, - symbolColor = visible ? (item.legendColor || item.color || '#CCC') : hiddenColor, - markerOptions = item.options && item.options.marker, - symbolAttr = { fill: symbolColor }, - key, - val; - - if (legendItem) { - legendItem.css({ fill: textColor, color: textColor }); // color for #1553, oldIE - } - if (legendLine) { - legendLine.attr({ stroke: symbolColor }); - } - - if (legendSymbol) { - - // Apply marker options - if (markerOptions && legendSymbol.isMarker) { // #585 - symbolAttr.stroke = symbolColor; - markerOptions = item.convertAttribs(markerOptions); - for (key in markerOptions) { - val = markerOptions[key]; - if (val !== UNDEFINED) { - symbolAttr[key] = val; - } - } - } + /** + * Reset the tracking by hiding the tooltip, the hover series state and the hover point + * + * @param allowMove {Boolean} Instead of destroying the tooltip altogether, allow moving it if possible + */ + reset: function (allowMove, delay) { + var pointer = this, + chart = pointer.chart, + hoverSeries = chart.hoverSeries, + hoverPoint = chart.hoverPoint, + hoverPoints = chart.hoverPoints, + tooltip = chart.tooltip, + tooltipPoints = tooltip && tooltip.shared ? hoverPoints : hoverPoint; - legendSymbol.attr(symbolAttr); - } - }, + // Narrow in allowMove + allowMove = allowMove && tooltip && tooltipPoints; - /** - * Position the legend item - * @param {Object} item A Series or Point instance - */ - positionItem: function (item) { - var legend = this, - options = legend.options, - symbolPadding = options.symbolPadding, - ltr = !options.rtl, - legendItemPos = item._legendItemPos, - itemX = legendItemPos[0], - itemY = legendItemPos[1], - checkbox = item.checkbox, - legendGroup = item.legendGroup; + // Check if the points have moved outside the plot area (#1003, #4736) + if (allowMove) { + each(splat(tooltipPoints), function (point) { + if (point.plotX === undefined) { + allowMove = false; + } + }); + } + + // Just move the tooltip, #349 + if (allowMove) { + tooltip.refresh(tooltipPoints); + if (hoverPoint) { // #2500 + hoverPoint.setState(hoverPoint.state, true); + each(chart.axes, function (axis) { + if (pick(axis.options.crosshair && axis.options.crosshair.snap, true)) { + axis.drawCrosshair(null, hoverPoint); + } else { + axis.hideCrosshair(); + } + }); - if (legendGroup && legendGroup.element) { - legendGroup.translate( - ltr ? itemX : legend.legendWidth - itemX - 2 * symbolPadding - 4, - itemY - ); - } + } - if (checkbox) { - checkbox.x = itemX; - checkbox.y = itemY; - } - }, + // Full reset + } else { - /** - * Destroy a single legend item - * @param {Object} item The series or point - */ - destroyItem: function (item) { - var checkbox = item.checkbox; + if (hoverPoint) { + hoverPoint.onMouseOut(); + } - // destroy SVG elements - each(['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], function (key) { - if (item[key]) { - item[key] = item[key].destroy(); - } - }); + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } - if (checkbox) { - discardElement(item.checkbox); - } - }, + if (hoverSeries) { + hoverSeries.onMouseOut(); + } - /** - * Destroys the legend. - */ - destroy: function () { - var legend = this, - legendGroup = legend.group, - box = legend.box; + if (tooltip) { + tooltip.hide(delay); + } - if (box) { - legend.box = box.destroy(); - } + if (pointer._onDocumentMouseMove) { + removeEvent(doc, 'mousemove', pointer._onDocumentMouseMove); + pointer._onDocumentMouseMove = null; + } - if (legendGroup) { - legend.group = legendGroup.destroy(); - } - }, + // Remove crosshairs + each(chart.axes, function (axis) { + axis.hideCrosshair(); + }); - /** - * Position the checkboxes after the width is determined - */ - positionCheckboxes: function (scrollOffset) { - var alignAttr = this.group.alignAttr, - translateY, - clipHeight = this.clipHeight || this.legendHeight; + pointer.hoverX = chart.hoverPoints = chart.hoverPoint = null; - if (alignAttr) { - translateY = alignAttr.translateY; - each(this.allItems, function (item) { - var checkbox = item.checkbox, - top; - - if (checkbox) { - top = (translateY + checkbox.y + (scrollOffset || 0) + 3); - css(checkbox, { - left: (alignAttr.translateX + item.checkboxOffset + checkbox.x - 20) + PX, - top: top + PX, - display: top > translateY - 6 && top < translateY + clipHeight - 6 ? '' : NONE - }); - } - }); - } - }, - - /** - * Render the legend title on top of the legend - */ - renderTitle: function () { - var options = this.options, - padding = this.padding, - titleOptions = options.title, - titleHeight = 0, - bBox; - - if (titleOptions.text) { - if (!this.title) { - this.title = this.chart.renderer.label(titleOptions.text, padding - 3, padding - 4, null, null, null, null, null, 'legend-title') - .attr({ zIndex: 1 }) - .css(titleOptions.style) - .add(this.group); - } - bBox = this.title.getBBox(); - titleHeight = bBox.height; - this.offsetWidth = bBox.width; // #1717 - this.contentGroup.attr({ translateY: titleHeight }); - } - this.titleHeight = titleHeight; - }, + } + }, - /** - * Set the legend item text - */ - setText: function (item) { - var options = this.options; - item.legendItem.attr({ - text: options.labelFormat ? format(options.labelFormat, item) : options.labelFormatter.call(item) - }); - }, + /** + * Scale series groups to a certain scale and translation + */ + scaleGroups: function (attribs, clip) { - /** - * Render a single specific legend item - * @param {Object} item A series or point - */ - renderItem: function (item) { - var legend = this, - chart = legend.chart, - renderer = chart.renderer, - options = legend.options, - horizontal = options.layout === 'horizontal', - symbolWidth = legend.symbolWidth, - symbolPadding = options.symbolPadding, - itemStyle = legend.itemStyle, - itemHiddenStyle = legend.itemHiddenStyle, - padding = legend.padding, - itemDistance = horizontal ? pick(options.itemDistance, 20) : 0, - ltr = !options.rtl, - itemHeight, - widthOption = options.width, - itemMarginBottom = options.itemMarginBottom || 0, - itemMarginTop = legend.itemMarginTop, - initialItemX = legend.initialItemX, - bBox, - itemWidth, - li = item.legendItem, - series = item.series && item.series.drawLegendSymbol ? item.series : item, - seriesOptions = series.options, - showCheckbox = legend.createCheckboxForItem && seriesOptions && seriesOptions.showCheckbox, - useHTML = options.useHTML; + var chart = this.chart, + seriesAttribs; - if (!li) { // generate it once, later move it + // Scale each series + each(chart.series, function (series) { + seriesAttribs = attribs || series.getPlotBox(); // #1701 + if (series.xAxis && series.xAxis.zoomEnabled) { + series.group.attr(seriesAttribs); + if (series.markerGroup) { + series.markerGroup.attr(seriesAttribs); + series.markerGroup.clip(clip ? chart.clipRect : null); + } + if (series.dataLabelsGroup) { + series.dataLabelsGroup.attr(seriesAttribs); + } + } + }); - // 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') - .attr({ zIndex: 1 }) - .add(legend.scrollGroup); + // Clip + chart.clipRect.attr(clip || chart.clipBox); + }, - // Generate the list item text and add it to the group - item.legendItem = li = renderer.text( - '', - ltr ? symbolWidth + symbolPadding : -symbolPadding, - legend.baseline || 0, - useHTML - ) - .css(merge(item.visible ? itemStyle : itemHiddenStyle)) // merge to prevent modifying original (#1021) - .attr({ - align: ltr ? 'left' : 'right', - zIndex: 2 - }) - .add(item.legendGroup); + /** + * Start a drag operation + */ + dragStart: function (e) { + var chart = this.chart; - // Get the baseline for the first item - the font size is equal for all - if (!legend.baseline) { - legend.fontMetrics = renderer.fontMetrics(itemStyle.fontSize, li); - legend.baseline = legend.fontMetrics.f + 3 + itemMarginTop; - li.attr('y', legend.baseline); - } + // Record the start position + chart.mouseIsDown = e.type; + chart.cancelClick = false; + chart.mouseDownX = this.mouseDownX = e.chartX; + chart.mouseDownY = this.mouseDownY = e.chartY; + }, - // Draw the legend symbol inside the group box - series.drawLegendSymbol(legend, item); + /** + * Perform a drag operation in response to a mousemove event while the mouse is down + */ + drag: function (e) { - if (legend.setItemEvents) { - legend.setItemEvents(item, li, useHTML, itemStyle, itemHiddenStyle); - } + var chart = this.chart, + chartOptions = chart.options.chart, + chartX = e.chartX, + chartY = e.chartY, + zoomHor = this.zoomHor, + zoomVert = this.zoomVert, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + clickedInside, + size, + selectionMarker = this.selectionMarker, + mouseDownX = this.mouseDownX, + mouseDownY = this.mouseDownY, + panKey = chartOptions.panKey && e[chartOptions.panKey + 'Key']; - // Colorize the items - legend.colorizeItem(item, item.visible); + // If the device supports both touch and mouse (like IE11), and we are touch-dragging + // inside the plot area, don't handle the mouse event. #4339. + if (selectionMarker && selectionMarker.touch) { + return; + } - // add the HTML checkbox on top - if (showCheckbox) { - legend.createCheckboxForItem(item); - } - } + // If the mouse is outside the plot area, adjust to cooordinates + // inside to prevent the selection marker from going outside + if (chartX < plotLeft) { + chartX = plotLeft; + } else if (chartX > plotLeft + plotWidth) { + chartX = plotLeft + plotWidth; + } - // Always update the text - legend.setText(item); + if (chartY < plotTop) { + chartY = plotTop; + } else if (chartY > plotTop + plotHeight) { + chartY = plotTop + plotHeight; + } - // calculate the positions for the next line - bBox = li.getBBox(); + // determine if the mouse has moved more than 10px + this.hasDragged = Math.sqrt( + Math.pow(mouseDownX - chartX, 2) + + Math.pow(mouseDownY - chartY, 2) + ); - itemWidth = item.checkboxOffset = - options.itemWidth || - item.legendItemWidth || - symbolWidth + symbolPadding + bBox.width + itemDistance + (showCheckbox ? 20 : 0); - legend.itemHeight = itemHeight = mathRound(item.legendItemHeight || bBox.height); + if (this.hasDragged > 10) { + clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop); - // if the item exceeds the width, start a new line - if (horizontal && legend.itemX - initialItemX + itemWidth > - (widthOption || (chart.chartWidth - 2 * padding - initialItemX - options.x))) { - legend.itemX = initialItemX; - legend.itemY += itemMarginTop + legend.lastLineHeight + itemMarginBottom; - legend.lastLineHeight = 0; // reset for next line (#915, #3976) - } + // make a selection + if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside && !panKey) { + if (!selectionMarker) { + this.selectionMarker = selectionMarker = chart.renderer.rect( + plotLeft, + plotTop, + zoomHor ? 1 : plotWidth, + zoomVert ? 1 : plotHeight, + 0 + ) + .attr({ + fill: chartOptions.selectionMarkerFill || 'rgba(69,114,167,0.25)', + zIndex: 7 + }) + .add(); + } + } - // If the item exceeds the height, start a new column - /*if (!horizontal && legend.itemY + options.y + itemHeight > chart.chartHeight - spacingTop - spacingBottom) { - legend.itemY = legend.initialItemY; - legend.itemX += legend.maxItemWidth; - legend.maxItemWidth = 0; - }*/ + // adjust the width of the selection marker + if (selectionMarker && zoomHor) { + size = chartX - mouseDownX; + selectionMarker.attr({ + width: mathAbs(size), + x: (size > 0 ? 0 : size) + mouseDownX + }); + } + // adjust the height of the selection marker + if (selectionMarker && zoomVert) { + size = chartY - mouseDownY; + selectionMarker.attr({ + height: mathAbs(size), + y: (size > 0 ? 0 : size) + mouseDownY + }); + } - // Set the edge positions - legend.maxItemWidth = mathMax(legend.maxItemWidth, itemWidth); - legend.lastItemY = itemMarginTop + legend.itemY + itemMarginBottom; - legend.lastLineHeight = mathMax(itemHeight, legend.lastLineHeight); // #915 + // panning + if (clickedInside && !selectionMarker && chartOptions.panning) { + chart.pan(e, chartOptions.panning); + } + } + }, - // cache the position of the newly generated or reordered items - item._legendItemPos = [legend.itemX, legend.itemY]; + /** + * On mouse up or touch end across the entire document, drop the selection. + */ + drop: function (e) { + var pointer = this, + chart = this.chart, + hasPinched = this.hasPinched; - // advance - if (horizontal) { - legend.itemX += itemWidth; + if (this.selectionMarker) { + var selectionData = { + xAxis: [], + yAxis: [], + originalEvent: e.originalEvent || e + }, + selectionBox = this.selectionMarker, + selectionLeft = selectionBox.attr ? selectionBox.attr('x') : selectionBox.x, + selectionTop = selectionBox.attr ? selectionBox.attr('y') : selectionBox.y, + selectionWidth = selectionBox.attr ? selectionBox.attr('width') : selectionBox.width, + selectionHeight = selectionBox.attr ? selectionBox.attr('height') : selectionBox.height, + runZoom; - } else { - legend.itemY += itemMarginTop + itemHeight + itemMarginBottom; - legend.lastLineHeight = itemHeight; - } + // a selection has been made + if (this.hasDragged || hasPinched) { - // the width of the widest item - legend.offsetWidth = widthOption || mathMax( - (horizontal ? legend.itemX - initialItemX - itemDistance : itemWidth) + padding, - legend.offsetWidth - ); - }, + // record each axis' min and max + each(chart.axes, function (axis) { + if (axis.zoomEnabled && defined(axis.min) && (hasPinched || pointer[{ xAxis: 'zoomX', yAxis: 'zoomY' }[axis.coll]])) { // #859, #3569 + var horiz = axis.horiz, + minPixelPadding = e.type === 'touchend' ? axis.minPixelPadding : 0, // #1207, #3075 + selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop) + minPixelPadding), + selectionMax = axis.toValue((horiz ? selectionLeft + selectionWidth : selectionTop + selectionHeight) - minPixelPadding); - /** - * Get all items, which is one item per series for normal series and one item per point - * for pie series. - */ - getAllItems: function () { - var allItems = []; - each(this.chart.series, function (series) { - var seriesOptions = series.options; + selectionData[axis.coll].push({ + axis: axis, + min: mathMin(selectionMin, selectionMax), // for reversed axes + max: mathMax(selectionMin, selectionMax) + }); + runZoom = true; + } + }); + if (runZoom) { + fireEvent(chart, 'selection', selectionData, function (args) { + chart.zoom(extend(args, hasPinched ? { animation: false } : null)); + }); + } - // Handle showInLegend. If the series is linked to another series, defaults to false. - if (!pick(seriesOptions.showInLegend, !defined(seriesOptions.linkedTo) ? UNDEFINED : false, true)) { - return; - } + } + this.selectionMarker = this.selectionMarker.destroy(); - // use points or series for the legend item depending on legendType - allItems = allItems.concat( - series.legendItems || - (seriesOptions.legendType === 'point' ? - series.data : - series) - ); - }); - return allItems; - }, + // Reset scaling preview + if (hasPinched) { + this.scaleGroups(); + } + } - /** - * Adjust the chart margins by reserving space for the legend on only one side - * of the chart. If the position is set to a corner, top or bottom is reserved - * for horizontal legends and left or right for vertical ones. - */ - adjustMargins: function (margin, spacing) { - var chart = this.chart, - options = this.options, - // Use the first letter of each alignment option in order to detect the side - alignment = options.align.charAt(0) + options.verticalAlign.charAt(0) + options.layout.charAt(0); // #4189 - use charAt(x) notation instead of [x] for IE7 - - if (this.display && !options.floating) { + // Reset all + if (chart) { // it may be destroyed on mouse up - #877 + css(chart.container, { cursor: chart._cursor }); + chart.cancelClick = this.hasDragged > 10; // #370 + chart.mouseIsDown = this.hasDragged = this.hasPinched = false; + this.pinchDown = []; + } + }, - each([ - /(lth|ct|rth)/, - /(rtv|rm|rbv)/, - /(rbh|cb|lbh)/, - /(lbv|lm|ltv)/ - ], function (alignments, side) { - if (alignments.test(alignment) && !defined(margin[side])) { - // Now we have detected on which side of the chart we should reserve space for the legend - chart[marginNames[side]] = mathMax( - chart[marginNames[side]], - chart.legend[(side + 1) % 2 ? 'legendHeight' : 'legendWidth'] + - [1, -1, -1, 1][side] * options[(side % 2) ? 'x' : 'y'] + - pick(options.margin, 12) + - spacing[side] - ); - } - }); - } - }, + onContainerMouseDown: function (e) { - /** - * Render the legend. This method can be called both before and after - * chart.render. If called after, it will only rearrange items instead - * of creating new ones. - */ - render: function () { - var legend = this, - chart = legend.chart, - renderer = chart.renderer, - legendGroup = legend.group, - allItems, - display, - legendWidth, - legendHeight, - box = legend.box, - options = legend.options, - padding = legend.padding, - legendBorderWidth = options.borderWidth, - legendBackgroundColor = options.backgroundColor; + e = this.normalize(e); - legend.itemX = legend.initialItemX; - legend.itemY = legend.initialItemY; - legend.offsetWidth = 0; - legend.lastItemY = 0; + // issue #295, dragging not always working in Firefox + if (e.preventDefault) { + e.preventDefault(); + } - if (!legendGroup) { - legend.group = legendGroup = renderer.g('legend') - .attr({ zIndex: 7 }) - .add(); - legend.contentGroup = renderer.g() - .attr({ zIndex: 1 }) // above background - .add(legendGroup); - legend.scrollGroup = renderer.g() - .add(legend.contentGroup); - } - - legend.renderTitle(); + this.dragStart(e); + }, - // add each series or point - allItems = legend.getAllItems(); - // sort by legendIndex - stableSort(allItems, function (a, b) { - return ((a.options && a.options.legendIndex) || 0) - ((b.options && b.options.legendIndex) || 0); - }); - // reversed legend - if (options.reversed) { - allItems.reverse(); - } + onDocumentMouseUp: function (e) { + if (charts[hoverChartIndex]) { + charts[hoverChartIndex].pointer.drop(e); + } + }, - legend.allItems = allItems; - legend.display = display = !!allItems.length; + /** + * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea. + * Issue #149 workaround. The mouseleave event does not always fire. + */ + onDocumentMouseMove: function (e) { + var chart = this.chart, + chartPosition = this.chartPosition; - // render the items - legend.lastLineHeight = 0; - each(allItems, function (item) { - legend.renderItem(item); - }); + e = this.normalize(e, chartPosition); - // Get the box - legendWidth = (options.width || legend.offsetWidth) + padding; - legendHeight = legend.lastItemY + legend.lastLineHeight + legend.titleHeight; - legendHeight = legend.handleOverflow(legendHeight); - legendHeight += padding; + // If we're outside, hide the tooltip + if (chartPosition && !this.inClass(e.target, 'highcharts-tracker') && + !chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { + this.reset(); + } + }, - // Draw the border and/or background - if (legendBorderWidth || legendBackgroundColor) { + /** + * When mouse leaves the container, hide the tooltip. + */ + onContainerMouseLeave: function () { + var chart = charts[hoverChartIndex]; + if (chart) { + chart.pointer.reset(); + chart.pointer.chartPosition = null; // also reset the chart position, used in #149 fix + } + }, - if (!box) { - legend.box = box = renderer.rect( - 0, - 0, - legendWidth, - legendHeight, - options.borderRadius, - legendBorderWidth || 0 - ).attr({ - stroke: options.borderColor, - 'stroke-width': legendBorderWidth || 0, - fill: legendBackgroundColor || NONE - }) - .add(legendGroup) - .shadow(options.shadow); - box.isNew = true; + // The mousemove, touchmove and touchstart event handler + onContainerMouseMove: function (e) { - } else if (legendWidth > 0 && legendHeight > 0) { - box[box.isNew ? 'attr' : 'animate']( - box.crisp({ width: legendWidth, height: legendHeight }) - ); - box.isNew = false; - } + var chart = this.chart; - // hide the border if no items - box[display ? 'show' : 'hide'](); - } - - legend.legendWidth = legendWidth; - legend.legendHeight = legendHeight; + hoverChartIndex = chart.index; - // Now that the legend width and height are established, put the items in the - // final position - each(allItems, function (item) { - legend.positionItem(item); - }); + e = this.normalize(e); + e.returnValue = false; // #2251, #3224 - // 1.x compatibility: positioning based on style - /*var props = ['left', 'right', 'top', 'bottom'], - prop, - i = 4; - while (i--) { - prop = props[i]; - if (options.style[prop] && options.style[prop] !== 'auto') { - options[i < 2 ? 'align' : 'verticalAlign'] = prop; - options[i < 2 ? 'x' : 'y'] = pInt(options.style[prop]) * (i % 2 ? -1 : 1); - } - }*/ + if (chart.mouseIsDown === 'mousedown') { + this.drag(e); + } - if (display) { - legendGroup.align(extend({ - width: legendWidth, - height: legendHeight - }, options), true, 'spacingBox'); - } + // Show the tooltip and run mouse over events (#977) + if ((this.inClass(e.target, 'highcharts-tracker') || + chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) && !chart.openMenu) { + this.runPointActions(e); + } + }, - if (!chart.isResizing) { - this.positionCheckboxes(); - } - }, - - /** - * Set up the overflow handling by adding navigation with up and down arrows below the - * legend. - */ - handleOverflow: function (legendHeight) { - var legend = this, - chart = this.chart, - renderer = chart.renderer, - options = this.options, - optionsY = options.y, - alignTop = options.verticalAlign === 'top', - spaceHeight = chart.spacingBox.height + (alignTop ? -optionsY : optionsY) - this.padding, - maxHeight = options.maxHeight, - clipHeight, - clipRect = this.clipRect, - navOptions = options.navigation, - animation = pick(navOptions.animation, true), - arrowSize = navOptions.arrowSize || 12, - nav = this.nav, - pages = this.pages, - padding = this.padding, - lastY, - allItems = this.allItems, - clipToHeight = function (height) { - clipRect.attr({ - height: height - }); + /** + * Utility to detect whether an element has, or has a parent with, a specific + * class name. Used on detection of tracker objects and on deciding whether + * hovering the tooltip should cause the active series to mouse out. + */ + inClass: function (element, className) { + var elemClassName; + while (element) { + elemClassName = attr(element, 'class'); + if (elemClassName) { + if (elemClassName.indexOf(className) !== -1) { + return true; + } + if (elemClassName.indexOf(PREFIX + 'container') !== -1) { + return false; + } + } + element = element.parentNode; + } + }, - // useHTML - if (legend.contentGroup.div) { - legend.contentGroup.div.style.clip = 'rect(' + padding + 'px,9999px,' + (padding + height) + 'px,0)'; - } - }; + onTrackerMouseOut: function (e) { + var series = this.chart.hoverSeries, + relatedTarget = e.relatedTarget || e.toElement; - - // Adjust the height - if (options.layout === 'horizontal') { - spaceHeight /= 2; - } - if (maxHeight) { - spaceHeight = mathMin(spaceHeight, maxHeight); - } - - // Reset the legend height and adjust the clipping rectangle - pages.length = 0; - if (legendHeight > spaceHeight) { + if (series && !series.options.stickyTracking && + !this.inClass(relatedTarget, PREFIX + 'tooltip') && + !this.inClass(relatedTarget, PREFIX + 'series-' + series.index)) { // #2499, #4465 + series.onMouseOut(); + } + }, - this.clipHeight = clipHeight = mathMax(spaceHeight - 20 - this.titleHeight - padding, 0); - this.currentPage = pick(this.currentPage, 1); - this.fullHeight = legendHeight; - - // Fill pages with Y positions so that the top of each a legend item defines - // the scroll top for each page (#2098) - each(allItems, function (item, i) { - var y = item._legendItemPos[1], - h = mathRound(item.legendItem.getBBox().height), - len = pages.length; - - if (!len || (y - pages[len - 1] > clipHeight && (lastY || y) !== pages[len - 1])) { - pages.push(lastY || y); - len++; - } - - if (i === allItems.length - 1 && y + h - pages[len - 1] > clipHeight) { - pages.push(y); - } - if (y !== lastY) { - lastY = y; - } - }); + onContainerClick: function (e) { + var chart = this.chart, + hoverPoint = chart.hoverPoint, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop; - // Only apply clipping if needed. Clipping causes blurred legend in PDF export (#1787) - if (!clipRect) { - clipRect = legend.clipRect = renderer.clipRect(0, padding, 9999, 0); - legend.contentGroup.clip(clipRect); - } - - clipToHeight(clipHeight); + e = this.normalize(e); + e.originalEvent = e; // #3913 - // Add navigation elements - if (!nav) { - this.nav = nav = renderer.g().attr({ zIndex: 1 }).add(this.group); - this.up = renderer.symbol('triangle', 0, 0, arrowSize, arrowSize) - .on('click', function () { - legend.scroll(-1, animation); - }) - .add(nav); - this.pager = renderer.text('', 15, 10) - .css(navOptions.style) - .add(nav); - this.down = renderer.symbol('triangle-down', 0, 0, arrowSize, arrowSize) - .on('click', function () { - legend.scroll(1, animation); - }) - .add(nav); - } - - // Set initial position - legend.scroll(0); - - legendHeight = spaceHeight; - - } else if (nav) { - clipToHeight(chart.chartHeight); - nav.hide(); - this.scrollGroup.attr({ - translateY: 1 - }); - this.clipHeight = 0; // #1379 - } - - return legendHeight; - }, - - /** - * Scroll the legend by a number of pages - * @param {Object} scrollBy - * @param {Object} animation - */ - scroll: function (scrollBy, animation) { - var pages = this.pages, - pageCount = pages.length, - currentPage = this.currentPage + scrollBy, - clipHeight = this.clipHeight, - navOptions = this.options.navigation, - activeColor = navOptions.activeColor, - inactiveColor = navOptions.inactiveColor, - pager = this.pager, - padding = this.padding, - scrollOffset; - - // When resizing while looking at the last page - if (currentPage > pageCount) { - currentPage = pageCount; - } - - if (currentPage > 0) { - - if (animation !== UNDEFINED) { - setAnimation(animation, this.chart); - } - - this.nav.attr({ - translateX: padding, - translateY: clipHeight + this.padding + 7 + this.titleHeight, - visibility: VISIBLE - }); - this.up.attr({ - fill: currentPage === 1 ? inactiveColor : activeColor - }) - .css({ - cursor: currentPage === 1 ? 'default' : 'pointer' - }); - pager.attr({ - text: currentPage + '/' + pageCount - }); - this.down.attr({ - x: 18 + this.pager.getBBox().width, // adjust to text width - fill: currentPage === pageCount ? inactiveColor : activeColor - }) - .css({ - cursor: currentPage === pageCount ? 'default' : 'pointer' - }); - - scrollOffset = -pages[currentPage - 1] + this.initialItemY; + if (!chart.cancelClick) { - this.scrollGroup.animate({ - translateY: scrollOffset - }); - - this.currentPage = currentPage; - this.positionCheckboxes(scrollOffset); - } - - } - -}; + // On tracker click, fire the series and point events. #783, #1583 + if (hoverPoint && this.inClass(e.target, PREFIX + 'tracker')) { -/* - * LegendSymbolMixin - */ + // the series click event + fireEvent(hoverPoint.series, 'click', extend(e, { + point: hoverPoint + })); -var LegendSymbolMixin = Highcharts.LegendSymbolMixin = { + // the point click event + if (chart.hoverPoint) { // it may be destroyed (#1844) + hoverPoint.firePointEvent('click', e); + } - /** - * Get the series' symbol in the legend - * - * @param {Object} legend The legend object - * @param {Object} item The series (this) or point - */ - drawRectangle: function (legend, item) { - var symbolHeight = legend.options.symbolHeight || legend.fontMetrics.f; + // When clicking outside a tracker, fire a chart event + } else { + extend(e, this.getCoordinates(e)); - item.legendSymbol = this.chart.renderer.rect( - 0, - legend.baseline - symbolHeight + 1, // #3988 - legend.symbolWidth, - symbolHeight, - legend.options.symbolRadius || 0 - ).attr({ - zIndex: 3 - }).add(item.legendGroup); - - }, + // fire a click event in the chart + if (chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) { + fireEvent(chart, 'click', e); + } + } - /** - * Get the series' symbol in the legend. This method should be overridable to create custom - * symbols through Highcharts.seriesTypes[type].prototype.drawLegendSymbols. - * - * @param {Object} legend The legend object - */ - drawLineMarker: function (legend) { - var options = this.options, - markerOptions = options.marker, - radius, - legendSymbol, - symbolWidth = legend.symbolWidth, - renderer = this.chart.renderer, - legendItemGroup = this.legendGroup, - verticalCenter = legend.baseline - mathRound(legend.fontMetrics.b * 0.3), - attr; + } + }, - // Draw the line - if (options.lineWidth) { - attr = { - 'stroke-width': options.lineWidth - }; - if (options.dashStyle) { - attr.dashstyle = options.dashStyle; - } - this.legendLine = renderer.path([ - M, - 0, - verticalCenter, - L, - symbolWidth, - verticalCenter - ]) - .attr(attr) - .add(legendItemGroup); - } - - // Draw the marker - if (markerOptions && markerOptions.enabled !== false) { - radius = markerOptions.radius; - this.legendSymbol = legendSymbol = renderer.symbol( - this.symbol, - (symbolWidth / 2) - radius, - verticalCenter - radius, - 2 * radius, - 2 * radius - ) - .add(legendItemGroup); - legendSymbol.isMarker = true; - } - } -}; + /** + * Set the JS DOM events on the container and document. This method should contain + * a one-to-one assignment between methods and their handlers. Any advanced logic should + * be moved to the handler reflecting the event's name. + */ + setDOMEvents: function () { -// Workaround for #2030, horizontal legend items not displaying in IE11 Preview, -// and for #2580, a similar drawing flaw in Firefox 26. -// TODO: Explore if there's a general cause for this. The problem may be related -// to nested group elements, as the legend item texts are within 4 group elements. -if (/Trident\/7\.0/.test(userAgent) || isFirefox) { - wrap(Legend.prototype, 'positionItem', function (proceed, item) { - var legend = this, - runPositionItem = function () { // If chart destroyed in sync, this is undefined (#2030) - if (item._legendItemPos) { - proceed.call(legend, item); - } - }; + var pointer = this, + container = pointer.chart.container; - // Do it now, for export and to get checkbox placement - runPositionItem(); - - // Do it after to work around the core issue - setTimeout(runPositionItem); - }); -} -/** - * The chart class - * @param {Object} options - * @param {Function} callback Function to run when the chart has loaded - */ -var Chart = Highcharts.Chart = function () { - this.init.apply(this, arguments); -}; + container.onmousedown = function (e) { + pointer.onContainerMouseDown(e); + }; + container.onmousemove = function (e) { + pointer.onContainerMouseMove(e); + }; + container.onclick = function (e) { + pointer.onContainerClick(e); + }; + addEvent(container, 'mouseleave', pointer.onContainerMouseLeave); + if (chartCount === 1) { + addEvent(doc, 'mouseup', pointer.onDocumentMouseUp); + } + if (hasTouch) { + container.ontouchstart = function (e) { + pointer.onContainerTouchStart(e); + }; + container.ontouchmove = function (e) { + pointer.onContainerTouchMove(e); + }; + if (chartCount === 1) { + addEvent(doc, 'touchend', pointer.onDocumentTouchEnd); + } + } -Chart.prototype = { + }, - /** - * Hook for modules - */ - callbacks: [], + /** + * Destroys the Pointer object and disconnects DOM events. + */ + destroy: function () { + var prop; - /** - * Initialize the chart - */ - init: function (userOptions, callback) { + removeEvent(this.chart.container, 'mouseleave', this.onContainerMouseLeave); + if (!chartCount) { + removeEvent(doc, 'mouseup', this.onDocumentMouseUp); + removeEvent(doc, 'touchend', this.onDocumentTouchEnd); + } - // Handle regular options - var options, - seriesOptions = userOptions.series; // skip merging data points to increase performance + // memory and CPU leak + clearInterval(this.tooltipTimeout); - userOptions.series = null; - options = merge(defaultOptions, userOptions); // do the merge - options.series = userOptions.series = seriesOptions; // set back the series data - this.userOptions = userOptions; + for (prop in this) { + this[prop] = null; + } + } + }; - var optionsChart = options.chart; - - // Create margin & spacing array - this.margin = this.splashArray('margin', optionsChart); - this.spacing = this.splashArray('spacing', optionsChart); - var chartEvents = optionsChart.events; + /* Support for touch devices */ + extend(Highcharts.Pointer.prototype, { - //this.runChartClick = chartEvents && !!chartEvents.click; - this.bounds = { h: {}, v: {} }; // Pixel data bounds for touch zoom + /** + * Run translation operations + */ + pinchTranslate: function (pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) { + if (this.zoomHor || this.pinchHor) { + this.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); + } + if (this.zoomVert || this.pinchVert) { + this.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); + } + }, - this.callback = callback; - this.isResizing = 0; - this.options = options; - //chartTitleOptions = UNDEFINED; - //chartSubtitleOptions = UNDEFINED; + /** + * Run translation operations for each direction (horizontal and vertical) independently + */ + 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', + plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')], + selectionWH, + selectionXY, + clipXY, + scale = forcedScale || 1, + inverted = chart.inverted, + bounds = chart.bounds[horiz ? 'h' : 'v'], + singleTouch = pinchDown.length === 1, + touch0Start = pinchDown[0][sChartXY], + touch0Now = touches[0][sChartXY], + touch1Start = !singleTouch && pinchDown[1][sChartXY], + touch1Now = !singleTouch && touches[1][sChartXY], + outOfBounds, + transformScale, + scaleKey, + setScale = function () { + if (!singleTouch && mathAbs(touch0Start - touch1Start) > 20) { // Don't zoom if fingers are too close on this axis + scale = forcedScale || mathAbs(touch0Now - touch1Now) / mathAbs(touch0Start - touch1Start); + } - this.axes = []; - this.series = []; - this.hasCartesianSeries = optionsChart.showAxes; - //this.axisOffset = UNDEFINED; - //this.maxTicks = UNDEFINED; // handle the greatest amount of ticks on grouped axes - //this.inverted = UNDEFINED; - //this.loadingShown = UNDEFINED; - //this.container = UNDEFINED; - //this.chartWidth = UNDEFINED; - //this.chartHeight = UNDEFINED; - //this.marginRight = UNDEFINED; - //this.marginBottom = UNDEFINED; - //this.containerWidth = UNDEFINED; - //this.containerHeight = UNDEFINED; - //this.oldChartWidth = UNDEFINED; - //this.oldChartHeight = UNDEFINED; + clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start; + selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale; + }; - //this.renderTo = UNDEFINED; - //this.renderToClone = UNDEFINED; + // Set the scale, first pass + setScale(); - //this.spacingBox = UNDEFINED + selectionXY = clipXY; // the clip position (x or y) is altered if out of bounds, the selection position is not - //this.legend = UNDEFINED; + // Out of bounds + if (selectionXY < bounds.min) { + selectionXY = bounds.min; + outOfBounds = true; + } else if (selectionXY + selectionWH > bounds.max) { + selectionXY = bounds.max - selectionWH; + outOfBounds = true; + } - // Elements - //this.chartBackground = UNDEFINED; - //this.plotBackground = UNDEFINED; - //this.plotBGImage = UNDEFINED; - //this.plotBorder = UNDEFINED; - //this.loadingDiv = UNDEFINED; - //this.loadingSpan = UNDEFINED; + // Is the chart dragged off its bounds, determined by dataMin and dataMax? + if (outOfBounds) { - var chart = this, - eventType; + // Modify the touchNow position in order to create an elastic drag movement. This indicates + // to the user that the chart is responsive but can't be dragged further. + touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]); + if (!singleTouch) { + touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]); + } - // Add the chart to the global lookup - chart.index = charts.length; - charts.push(chart); - chartCount++; + // Set the scale, second pass to adapt to the modified touchNow positions + setScale(); - // Set up auto resize - if (optionsChart.reflow !== false) { - addEvent(chart, 'load', function () { - chart.initReflow(); - }); - } + } else { + lastValidTouch[xy] = [touch0Now, touch1Now]; + } - // Chart event handlers - if (chartEvents) { - for (eventType in chartEvents) { - addEvent(chart, eventType, chartEvents[eventType]); - } - } + // Set geometry for clipping, selection and transformation + if (!inverted) { + clip[xy] = clipXY - plotLeftTop; + clip[wh] = selectionWH; + } + scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY; + transformScale = inverted ? 1 / scale : scale; - chart.xAxis = []; - chart.yAxis = []; + selectionMarker[wh] = selectionWH; + selectionMarker[xy] = selectionXY; + transform[scaleKey] = scale; + transform['translate' + XY] = (transformScale * plotLeftTop) + (touch0Now - (transformScale * touch0Start)); + }, - // Expose methods and variables - chart.animation = useCanVG ? false : pick(optionsChart.animation, true); - chart.pointCount = chart.colorCounter = chart.symbolCounter = 0; + /** + * Handle touch events with two touches + */ + pinch: function (e) { - chart.firstRender(); - }, + var self = this, + chart = self.chart, + pinchDown = self.pinchDown, + touches = e.touches, + touchesLength = touches.length, + lastValidTouch = self.lastValidTouch, + hasZoom = self.hasZoom, + selectionMarker = self.selectionMarker, + transform = {}, + fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, PREFIX + 'tracker') && + chart.runTrackerClick) || self.runChartClick), + clip = {}; - /** - * Initialize an individual series, called internally before render time - */ - initSeries: function (options) { - var chart = this, - optionsChart = chart.options.chart, - type = options.type || optionsChart.type || optionsChart.defaultSeriesType, - series, - constr = seriesTypes[type]; + // Don't initiate panning until the user has pinched. This prevents us from + // blocking page scrolling as users scroll down a long page (#4210). + if (touchesLength > 1) { + self.initiated = true; + } - // No such series type - if (!constr) { - error(17, true); - } + // On touch devices, only proceed to trigger click if a handler is defined + if (hasZoom && self.initiated && !fireClickEvent) { + e.preventDefault(); + } - series = new constr(); - series.init(this, options); - return series; - }, + // Normalize each touch + map(touches, function (e) { + return self.normalize(e); + }); - /** - * Check whether a given point is within the plot area - * - * @param {Number} plotX Pixel x relative to the plot area - * @param {Number} plotY Pixel y relative to the plot area - * @param {Boolean} inverted Whether the chart is inverted - */ - isInsidePlot: function (plotX, plotY, inverted) { - var x = inverted ? plotY : plotX, - y = inverted ? plotX : plotY; - - return x >= 0 && - x <= this.plotWidth && - y >= 0 && - y <= this.plotHeight; - }, + // Register the touch start position + if (e.type === 'touchstart') { + each(touches, function (e, i) { + pinchDown[i] = { chartX: e.chartX, chartY: e.chartY }; + }); + lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && pinchDown[1].chartX]; + lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && pinchDown[1].chartY]; - /** - * Redraw legend, axes or series based on updated data - * - * @param {Boolean|Object} animation Whether to apply animation, and optionally animation - * configuration - */ - redraw: function (animation) { - var chart = this, - axes = chart.axes, - series = chart.series, - pointer = chart.pointer, - legend = chart.legend, - redrawLegend = chart.isDirtyLegend, - hasStackedSeries, - hasDirtyStacks, - hasCartesianSeries = chart.hasCartesianSeries, - isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed? - seriesLength = series.length, - i = seriesLength, - serie, - renderer = chart.renderer, - isHiddenChart = renderer.isHidden(), - afterRedraw = []; - - setAnimation(animation, chart); - - if (isHiddenChart) { - chart.cloneRenderTo(); - } + // Identify the data bounds in pixels + each(chart.axes, function (axis) { + if (axis.zoomEnabled) { + var bounds = chart.bounds[axis.horiz ? 'h' : 'v'], + minPixelPadding = axis.minPixelPadding, + min = axis.toPixels(pick(axis.options.min, axis.dataMin)), + max = axis.toPixels(pick(axis.options.max, axis.dataMax)), + absMin = mathMin(min, max), + absMax = mathMax(min, max); - // Adjust title layout (reflow multiline text) - chart.layOutTitles(); + // Store the bounds for use in the touchmove handler + bounds.min = mathMin(axis.pos, absMin - minPixelPadding); + bounds.max = mathMax(axis.pos + axis.len, absMax + minPixelPadding); + } + }); + self.res = true; // reset on next move - // link stacked series - while (i--) { - serie = series[i]; + // Event type is touchmove, handle panning and pinching + } else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first - if (serie.options.stacking) { - hasStackedSeries = true; - - if (serie.isDirty) { - hasDirtyStacks = true; - break; - } - } - } - if (hasDirtyStacks) { // mark others as dirty - i = seriesLength; - while (i--) { - serie = series[i]; - if (serie.options.stacking) { - serie.isDirty = true; - } - } - } - // Handle updated data in the series - each(series, function (serie) { - if (serie.isDirty) { - if (serie.options.legendType === 'point') { - if (serie.updateTotals) { - serie.updateTotals(); - } - redrawLegend = true; - } - } - }); + // Set the marker + if (!selectionMarker) { + self.selectionMarker = selectionMarker = extend({ + destroy: noop, + touch: true + }, chart.plotBox); + } - // handle added or removed series - if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed - // draw legend graphics - legend.render(); + self.pinchTranslate(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); - chart.isDirtyLegend = false; - } + self.hasPinched = hasZoom; - // reset stacks - if (hasStackedSeries) { - chart.getStacks(); - } + // 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) { + self.res = false; + this.reset(false, 0); + } + } + }, - if (hasCartesianSeries) { - if (!chart.isResizing) { + /** + * General touch handler shared by touchstart and touchmove. + */ + touch: function (e, start) { + var chart = this.chart; - // reset maxTicks - chart.maxTicks = null; + hoverChartIndex = chart.index; - // set axes scales - each(axes, function (axis) { - axis.setScale(); - }); - } - } + if (e.touches.length === 1) { - chart.getMargins(); // #3098 + e = this.normalize(e); - if (hasCartesianSeries) { - // If one axis is dirty, all axes must be redrawn (#792, #2169) - each(axes, function (axis) { - if (axis.isDirty) { - isDirtyBox = true; - } - }); + if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop) && !chart.openMenu) { - // redraw axes - each(axes, function (axis) { + // Run mouse events and display tooltip etc + if (start) { + this.runPointActions(e); + } - // Fire 'afterSetExtremes' only if extremes are set - var key = axis.min + ',' + axis.max; - if (axis.extKey !== key) { // #821, #4452 - axis.extKey = key; - afterRedraw.push(function () { // prevent a recursive call to chart.redraw() (#1119) - fireEvent(axis, 'afterSetExtremes', extend(axis.eventArgs, axis.getExtremes())); // #747, #751 - delete axis.eventArgs; - }); - } - if (isDirtyBox || hasStackedSeries) { - axis.redraw(); - } - }); - } - - // the plot areas size has changed - if (isDirtyBox) { - chart.drawChartBox(); - } + this.pinch(e); + } else if (start) { + // Hide the tooltip on touching outside the plot area (#1203) + this.reset(); + } - // redraw affected series - each(series, function (serie) { - if (serie.isDirty && serie.visible && - (!serie.isCartesian || serie.xAxis)) { // issue #153 - serie.redraw(); - } - }); + } else if (e.touches.length === 2) { + this.pinch(e); + } + }, - // move tooltip or reset - if (pointer) { - pointer.reset(true); - } + onContainerTouchStart: function (e) { + this.touch(e, true); + }, - // redraw if canvas - renderer.draw(); + onContainerTouchMove: function (e) { + this.touch(e); + }, - // fire the event - fireEvent(chart, 'redraw'); // jQuery breaks this when calling it from addEvent. Overwrites chart.redraw - - if (isHiddenChart) { - chart.cloneRenderTo(true); - } - - // Fire callbacks that are put on hold until after the redraw - each(afterRedraw, function (callback) { - callback.call(); - }); - }, + onDocumentTouchEnd: function (e) { + if (charts[hoverChartIndex]) { + charts[hoverChartIndex].pointer.drop(e); + } + } - /** - * Get an axis, series or point object by id. - * @param id {String} The id as given in the configuration options - */ - get: function (id) { - var chart = this, - axes = chart.axes, - series = chart.series; + }); + if (win.PointerEvent || win.MSPointerEvent) { - var i, - j, - points; + // The touches object keeps track of the points being touched at all times + var touches = {}, + hasPointerEvent = !!win.PointerEvent, + getWebkitTouches = function () { + var key, fake = []; + fake.item = function (i) { + return this[i]; + }; + for (key in touches) { + if (touches.hasOwnProperty(key)) { + fake.push({ + pageX: touches[key].pageX, + pageY: touches[key].pageY, + target: touches[key].target + }); + } + } + return fake; + }, + translateMSPointer = function (e, method, wktype, func) { + var p; + e = e.originalEvent || e; + if ((e.pointerType === 'touch' || e.pointerType === e.MSPOINTER_TYPE_TOUCH) && charts[hoverChartIndex]) { + func(e); + p = charts[hoverChartIndex].pointer; + p[method]({ + type: wktype, + target: e.currentTarget, + preventDefault: noop, + touches: getWebkitTouches() + }); + } + }; - // search axes - for (i = 0; i < axes.length; i++) { - if (axes[i].options.id === id) { - return axes[i]; - } - } + /** + * Extend the Pointer prototype with methods for each event handler and more + */ + extend(Pointer.prototype, { + onContainerPointerDown: function (e) { + translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function (e) { + touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY, target: e.currentTarget }; + }); + }, + onContainerPointerMove: function (e) { + translateMSPointer(e, 'onContainerTouchMove', 'touchmove', function (e) { + touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY }; + if (!touches[e.pointerId].target) { + touches[e.pointerId].target = e.currentTarget; + } + }); + }, + onDocumentPointerUp: function (e) { + translateMSPointer(e, 'onDocumentTouchEnd', 'touchend', function (e) { + delete touches[e.pointerId]; + }); + }, - // search series - for (i = 0; i < series.length; i++) { - if (series[i].options.id === id) { - return series[i]; - } - } + /** + * Add or remove the MS Pointer specific events + */ + batchMSEvents: function (fn) { + fn(this.chart.container, hasPointerEvent ? 'pointerdown' : 'MSPointerDown', this.onContainerPointerDown); + fn(this.chart.container, hasPointerEvent ? 'pointermove' : 'MSPointerMove', this.onContainerPointerMove); + fn(doc, hasPointerEvent ? 'pointerup' : 'MSPointerUp', this.onDocumentPointerUp); + } + }); - // search points - for (i = 0; i < series.length; i++) { - points = series[i].points || []; - for (j = 0; j < points.length; j++) { - if (points[j].id === id) { - return points[j]; - } - } - } - return null; - }, + // Disable default IE actions for pinch and such on chart element + wrap(Pointer.prototype, 'init', function (proceed, chart, options) { + proceed.call(this, chart, options); + if (this.hasZoom) { // #4014 + css(chart.container, { + '-ms-touch-action': NONE, + 'touch-action': NONE + }); + } + }); - /** - * Create the Axis instances based on the config options - */ - getAxes: function () { - var chart = this, - options = this.options, - xAxisOptions = options.xAxis = splat(options.xAxis || {}), - yAxisOptions = options.yAxis = splat(options.yAxis || {}), - optionsArray, - axis; + // Add IE specific touch events to chart + wrap(Pointer.prototype, 'setDOMEvents', function (proceed) { + proceed.apply(this); + if (this.hasZoom || this.followTouchMove) { + this.batchMSEvents(addEvent); + } + }); + // Destroy MS events also + wrap(Pointer.prototype, 'destroy', function (proceed) { + this.batchMSEvents(removeEvent); + proceed.call(this); + }); + } + /** + * The overview of the chart's series + */ + var Legend = Highcharts.Legend = function (chart, options) { + this.init(chart, options); + }; - // make sure the options are arrays and add some members - each(xAxisOptions, function (axis, i) { - axis.index = i; - axis.isX = true; - }); + Legend.prototype = { - each(yAxisOptions, function (axis, i) { - axis.index = i; - }); + /** + * Initialize the legend + */ + init: function (chart, options) { - // concatenate all axis options into one array - optionsArray = xAxisOptions.concat(yAxisOptions); + var legend = this, + itemStyle = options.itemStyle, + padding, + itemMarginTop = options.itemMarginTop || 0; - each(optionsArray, function (axisOptions) { - axis = new Axis(chart, axisOptions); - }); - }, + this.options = options; + if (!options.enabled) { + return; + } - /** - * Get the currently selected points from all series - */ - getSelectedPoints: function () { - var points = []; - each(this.series, function (serie) { - points = points.concat(grep(serie.points || [], function (point) { - return point.selected; - })); - }); - return points; - }, + legend.itemStyle = itemStyle; + legend.itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle); + legend.itemMarginTop = itemMarginTop; + legend.padding = padding = pick(options.padding, 8); + legend.initialItemX = padding; + legend.initialItemY = padding - 5; // 5 is the number of pixels above the text + legend.maxItemWidth = 0; + legend.chart = chart; + legend.itemHeight = 0; + legend.symbolWidth = pick(options.symbolWidth, 16); + legend.pages = []; - /** - * Get the currently selected series - */ - getSelectedSeries: function () { - return grep(this.series, function (serie) { - return serie.selected; - }); - }, - /** - * Show the title and subtitle of the chart - * - * @param titleOptions {Object} New title options - * @param subtitleOptions {Object} New subtitle options - * - */ - setTitle: function (titleOptions, subtitleOptions, redraw) { - var chart = this, - options = chart.options, - chartTitleOptions, - chartSubtitleOptions; + // Render it + legend.render(); - chartTitleOptions = options.title = merge(options.title, titleOptions); - chartSubtitleOptions = options.subtitle = merge(options.subtitle, subtitleOptions); + // move checkboxes + addEvent(legend.chart, 'endResize', function () { + legend.positionCheckboxes(); + }); - // add title and subtitle - each([ - ['title', titleOptions, chartTitleOptions], - ['subtitle', subtitleOptions, chartSubtitleOptions] - ], function (arr) { - var name = arr[0], - title = chart[name], - titleOptions = arr[1], - chartTitleOptions = arr[2]; + }, - if (title && titleOptions) { - chart[name] = title = title.destroy(); // remove old - } - - if (chartTitleOptions && chartTitleOptions.text && !title) { - chart[name] = chart.renderer.text( - chartTitleOptions.text, - 0, - 0, - chartTitleOptions.useHTML - ) - .attr({ - align: chartTitleOptions.align, - 'class': PREFIX + name, - zIndex: chartTitleOptions.zIndex || 4 - }) - .css(chartTitleOptions.style) - .add(); - } - }); - chart.layOutTitles(redraw); - }, + /** + * Set the colors for the legend item + * @param {Object} item A Series or Point instance + * @param {Object} visible Dimmed or colored + */ + colorizeItem: function (item, visible) { + var legend = this, + options = legend.options, + legendItem = item.legendItem, + legendLine = item.legendLine, + legendSymbol = item.legendSymbol, + hiddenColor = legend.itemHiddenStyle.color, + textColor = visible ? options.itemStyle.color : hiddenColor, + symbolColor = visible ? (item.legendColor || item.color || '#CCC') : hiddenColor, + markerOptions = item.options && item.options.marker, + symbolAttr = { fill: symbolColor }, + key, + val; - /** - * Lay out the chart titles and cache the full offset height for use in getMargins - */ - layOutTitles: function (redraw) { - var titleOffset = 0, - title = this.title, - subtitle = this.subtitle, - options = this.options, - titleOptions = options.title, - subtitleOptions = options.subtitle, - requiresDirtyBox, - renderer = this.renderer, - autoWidth = this.spacingBox.width - 44; // 44 makes room for default context button + if (legendItem) { + legendItem.css({ fill: textColor, color: textColor }); // color for #1553, oldIE + } + if (legendLine) { + legendLine.attr({ stroke: symbolColor }); + } - if (title) { - title - .css({ width: (titleOptions.width || autoWidth) + PX }) - .align(extend({ - y: renderer.fontMetrics(titleOptions.style.fontSize, title).b - 3 - }, titleOptions), false, 'spacingBox'); - - if (!titleOptions.floating && !titleOptions.verticalAlign) { - titleOffset = title.getBBox().height; - } - } - if (subtitle) { - subtitle - .css({ width: (subtitleOptions.width || autoWidth) + PX }) - .align(extend({ - y: titleOffset + (titleOptions.margin - 13) + renderer.fontMetrics(subtitleOptions.style.fontSize, title).b - }, subtitleOptions), false, 'spacingBox'); - - if (!subtitleOptions.floating && !subtitleOptions.verticalAlign) { - titleOffset = mathCeil(titleOffset + subtitle.getBBox().height); - } - } + if (legendSymbol) { - requiresDirtyBox = this.titleOffset !== titleOffset; - this.titleOffset = titleOffset; // used in getMargins + // Apply marker options + if (markerOptions && legendSymbol.isMarker) { // #585 + symbolAttr.stroke = symbolColor; + markerOptions = item.convertAttribs(markerOptions); + for (key in markerOptions) { + val = markerOptions[key]; + if (val !== UNDEFINED) { + symbolAttr[key] = val; + } + } + } - if (!this.isDirtyBox && requiresDirtyBox) { - this.isDirtyBox = requiresDirtyBox; - // Redraw if necessary (#2719, #2744) - if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) { - this.redraw(); - } - } - }, + legendSymbol.attr(symbolAttr); + } + }, - /** - * Get chart width and height according to options and container size - */ - getChartSize: function () { - var chart = this, - optionsChart = chart.options.chart, - widthOption = optionsChart.width, - heightOption = optionsChart.height, - renderTo = chart.renderToClone || chart.renderTo; + /** + * Position the legend item + * @param {Object} item A Series or Point instance + */ + positionItem: function (item) { + var legend = this, + options = legend.options, + symbolPadding = options.symbolPadding, + ltr = !options.rtl, + legendItemPos = item._legendItemPos, + itemX = legendItemPos[0], + itemY = legendItemPos[1], + checkbox = item.checkbox, + legendGroup = item.legendGroup; - // get inner width and height from jQuery (#824) - if (!defined(widthOption)) { - chart.containerWidth = adapterRun(renderTo, 'width'); - } - if (!defined(heightOption)) { - chart.containerHeight = adapterRun(renderTo, 'height'); - } - - chart.chartWidth = mathMax(0, widthOption || chart.containerWidth || 600); // #1393, 1460 - chart.chartHeight = mathMax(0, pick(heightOption, - // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7: - chart.containerHeight > 19 ? chart.containerHeight : 400)); - }, + if (legendGroup && legendGroup.element) { + legendGroup.translate( + ltr ? itemX : legend.legendWidth - itemX - 2 * symbolPadding - 4, + itemY + ); + } - /** - * Create a clone of the chart's renderTo div and place it outside the viewport to allow - * size computation on chart.render and chart.redraw - */ - cloneRenderTo: function (revert) { - var clone = this.renderToClone, - container = this.container; - - // Destroy the clone and bring the container back to the real renderTo div - if (revert) { - if (clone) { - this.renderTo.appendChild(container); - discardElement(clone); - delete this.renderToClone; - } - - // Set up the clone - } else { - if (container && container.parentNode === this.renderTo) { - this.renderTo.removeChild(container); // do not clone this - } - this.renderToClone = clone = this.renderTo.cloneNode(0); - css(clone, { - position: ABSOLUTE, - top: '-9999px', - display: 'block' // #833 - }); - if (clone.style.setProperty) { // #2631 - clone.style.setProperty('display', 'block', 'important'); - } - doc.body.appendChild(clone); - if (container) { - clone.appendChild(container); - } - } - }, + if (checkbox) { + checkbox.x = itemX; + checkbox.y = itemY; + } + }, - /** - * Get the containing element, determine the size and create the inner container - * div to hold the chart - */ - getContainer: function () { - var chart = this, - container, - options = chart.options, - optionsChart = options.chart, - chartWidth, - chartHeight, - renderTo, - indexAttrName = 'data-highcharts-chart', - oldChartIndex, - Ren, - containerId; + /** + * Destroy a single legend item + * @param {Object} item The series or point + */ + destroyItem: function (item) { + var checkbox = item.checkbox; - chart.renderTo = renderTo = optionsChart.renderTo; - containerId = PREFIX + idCounter++; + // destroy SVG elements + each(['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], function (key) { + if (item[key]) { + item[key] = item[key].destroy(); + } + }); - if (isString(renderTo)) { - chart.renderTo = renderTo = doc.getElementById(renderTo); - } - - // Display an error if the renderTo is wrong - if (!renderTo) { - error(13, true); - } - - // If the container already holds a chart, destroy it. The check for hasRendered is there - // because web pages that are saved to disk from the browser, will preserve the data-highcharts-chart - // attribute and the SVG contents, but not an interactive chart. So in this case, - // charts[oldChartIndex] will point to the wrong chart if any (#2609). - oldChartIndex = pInt(attr(renderTo, indexAttrName)); - if (!isNaN(oldChartIndex) && charts[oldChartIndex] && charts[oldChartIndex].hasRendered) { - charts[oldChartIndex].destroy(); - } - - // Make a reference to the chart from the div - attr(renderTo, indexAttrName, chart.index); + if (checkbox) { + discardElement(item.checkbox); + } + }, - // remove previous chart - renderTo.innerHTML = ''; + /** + * Destroys the legend. + */ + destroy: function () { + var legend = this, + legendGroup = legend.group, + box = legend.box; - // 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 (!optionsChart.skipClone && !renderTo.offsetWidth) { - chart.cloneRenderTo(); - } + if (box) { + legend.box = box.destroy(); + } - // get the width and height - chart.getChartSize(); - chartWidth = chart.chartWidth; - chartHeight = chart.chartHeight; + if (legendGroup) { + legend.group = legendGroup.destroy(); + } + }, - // create the inner container - chart.container = container = createElement(DIV, { - className: PREFIX + 'container' + - (optionsChart.className ? ' ' + optionsChart.className : ''), - id: containerId - }, extend({ - position: RELATIVE, - overflow: HIDDEN, // needed for context menu (avoid scrollbars) and - // content overflow in IE - width: chartWidth + PX, - height: chartHeight + PX, - textAlign: 'left', - lineHeight: 'normal', // #427 - zIndex: 0, // #1072 - '-webkit-tap-highlight-color': 'rgba(0,0,0,0)' - }, optionsChart.style), - chart.renderToClone || renderTo - ); + /** + * Position the checkboxes after the width is determined + */ + positionCheckboxes: function (scrollOffset) { + var alignAttr = this.group.alignAttr, + translateY, + clipHeight = this.clipHeight || this.legendHeight; - // cache the cursor (#1650) - chart._cursor = container.style.cursor; + if (alignAttr) { + translateY = alignAttr.translateY; + each(this.allItems, function (item) { + var checkbox = item.checkbox, + top; - // Initialize the renderer - Ren = Highcharts[optionsChart.renderer] || Renderer; - chart.renderer = new Ren( - container, - chartWidth, - chartHeight, - optionsChart.style, - optionsChart.forExport, - options.exporting && options.exporting.allowHTML - ); + if (checkbox) { + top = (translateY + checkbox.y + (scrollOffset || 0) + 3); + css(checkbox, { + left: (alignAttr.translateX + item.checkboxOffset + checkbox.x - 20) + PX, + top: top + PX, + display: top > translateY - 6 && top < translateY + clipHeight - 6 ? '' : NONE + }); + } + }); + } + }, - if (useCanVG) { - // If we need canvg library, extend and configure the renderer - // to get the tracker for translating mouse events - chart.renderer.create(chart, container, chartWidth, chartHeight); - } - // Add a reference to the charts index - chart.renderer.chartIndex = chart.index; - }, + /** + * Render the legend title on top of the legend + */ + renderTitle: function () { + var options = this.options, + padding = this.padding, + titleOptions = options.title, + titleHeight = 0, + bBox; - /** - * Calculate margins by rendering axis labels in a preliminary position. Title, - * subtitle and legend have already been rendered at this stage, but will be - * moved into their final positions - */ - getMargins: function (skipAxes) { - var chart = this, - spacing = chart.spacing, - margin = chart.margin, - titleOffset = chart.titleOffset; + if (titleOptions.text) { + if (!this.title) { + this.title = this.chart.renderer.label(titleOptions.text, padding - 3, padding - 4, null, null, null, null, null, 'legend-title') + .attr({ zIndex: 1 }) + .css(titleOptions.style) + .add(this.group); + } + bBox = this.title.getBBox(); + titleHeight = bBox.height; + this.offsetWidth = bBox.width; // #1717 + this.contentGroup.attr({ translateY: titleHeight }); + } + this.titleHeight = titleHeight; + }, - chart.resetMargins(); + /** + * Set the legend item text + */ + setText: function (item) { + var options = this.options; + item.legendItem.attr({ + text: options.labelFormat ? format(options.labelFormat, item) : options.labelFormatter.call(item) + }); + }, - // Adjust for title and subtitle - if (titleOffset && !defined(margin[0])) { - chart.plotTop = mathMax(chart.plotTop, titleOffset + chart.options.title.margin + spacing[0]); - } - - // Adjust for legend - chart.legend.adjustMargins(margin, spacing); + /** + * Render a single specific legend item + * @param {Object} item A series or point + */ + renderItem: function (item) { + var legend = this, + chart = legend.chart, + renderer = chart.renderer, + options = legend.options, + horizontal = options.layout === 'horizontal', + symbolWidth = legend.symbolWidth, + symbolPadding = options.symbolPadding, + itemStyle = legend.itemStyle, + itemHiddenStyle = legend.itemHiddenStyle, + padding = legend.padding, + itemDistance = horizontal ? pick(options.itemDistance, 20) : 0, + ltr = !options.rtl, + itemHeight, + widthOption = options.width, + itemMarginBottom = options.itemMarginBottom || 0, + itemMarginTop = legend.itemMarginTop, + initialItemX = legend.initialItemX, + bBox, + itemWidth, + li = item.legendItem, + series = item.series && item.series.drawLegendSymbol ? item.series : item, + seriesOptions = series.options, + showCheckbox = legend.createCheckboxForItem && seriesOptions && seriesOptions.showCheckbox, + useHTML = options.useHTML; - // adjust for scroller - if (chart.extraBottomMargin) { - chart.marginBottom += chart.extraBottomMargin; - } - if (chart.extraTopMargin) { - chart.plotTop += chart.extraTopMargin; - } - if (!skipAxes) { - this.getAxisMargins(); - } - }, + if (!li) { // generate it once, later move it - getAxisMargins: function () { + // 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') + .attr({ zIndex: 1 }) + .add(legend.scrollGroup); - var chart = this, - axisOffset = chart.axisOffset = [0, 0, 0, 0], // top, right, bottom, left - margin = chart.margin; - - // pre-render axes to get labels offset width - if (chart.hasCartesianSeries) { - each(chart.axes, function (axis) { - if (axis.visible) { - axis.getOffset(); - } - }); - } + // Generate the list item text and add it to the group + item.legendItem = li = renderer.text( + '', + ltr ? symbolWidth + symbolPadding : -symbolPadding, + legend.baseline || 0, + useHTML + ) + .css(merge(item.visible ? itemStyle : itemHiddenStyle)) // merge to prevent modifying original (#1021) + .attr({ + align: ltr ? 'left' : 'right', + zIndex: 2 + }) + .add(item.legendGroup); - // Add the axis offsets - each(marginNames, function (m, side) { - if (!defined(margin[side])) { - chart[m] += axisOffset[side]; - } - }); + // Get the baseline for the first item - the font size is equal for all + if (!legend.baseline) { + legend.fontMetrics = renderer.fontMetrics(itemStyle.fontSize, li); + legend.baseline = legend.fontMetrics.f + 3 + itemMarginTop; + li.attr('y', legend.baseline); + } - chart.setChartSize(); + // Draw the legend symbol inside the group box + series.drawLegendSymbol(legend, item); - }, + if (legend.setItemEvents) { + legend.setItemEvents(item, li, useHTML, itemStyle, itemHiddenStyle); + } - /** - * Resize the chart to its container if size is not explicitly set - */ - reflow: function (e) { - var chart = this, - optionsChart = chart.options.chart, - renderTo = chart.renderTo, - width = optionsChart.width || adapterRun(renderTo, 'width'), - height = optionsChart.height || adapterRun(renderTo, 'height'), - target = e ? e.target : win, // #805 - MooTools doesn't supply e - doReflow = function () { - if (chart.container) { // It may have been destroyed in the meantime (#1257) - chart.setSize(width, height, false); - chart.hasUserSize = null; - } - }; - - // Width and height checks for display:none. Target is doc in IE8 and Opera, - // win in Firefox, Chrome and IE9. - if (!chart.hasUserSize && !chart.isPrinting && width && height && (target === win || target === doc)) { // #1093 - if (width !== chart.containerWidth || height !== chart.containerHeight) { - clearTimeout(chart.reflowTimeout); - if (e) { // Called from window.resize - chart.reflowTimeout = setTimeout(doReflow, 100); - } else { // Called directly (#2224) - doReflow(); - } - } - chart.containerWidth = width; - chart.containerHeight = height; - } - }, + // Colorize the items + legend.colorizeItem(item, item.visible); - /** - * Add the event handlers necessary for auto resizing - */ - initReflow: function () { - var chart = this, - reflow = function (e) { - chart.reflow(e); - }; - - - addEvent(win, 'resize', reflow); - addEvent(chart, 'destroy', function () { - removeEvent(win, 'resize', reflow); - }); - }, + // add the HTML checkbox on top + if (showCheckbox) { + legend.createCheckboxForItem(item); + } + } - /** - * Resize the chart to a given width and height - * @param {Number} width - * @param {Number} height - * @param {Object|Boolean} animation - */ - setSize: function (width, height, animation) { - var chart = this, - chartWidth, - chartHeight, - fireEndResize, - renderer = chart.renderer, - globalAnimation; + // Always update the text + legend.setText(item); - // Handle the isResizing counter - chart.isResizing += 1; - fireEndResize = function () { - if (chart) { - fireEvent(chart, 'endResize', null, function () { - chart.isResizing -= 1; - }); - } - }; + // calculate the positions for the next line + bBox = li.getBBox(); - // set the animation for the current process - setAnimation(animation, chart); + itemWidth = item.checkboxOffset = + options.itemWidth || + item.legendItemWidth || + symbolWidth + symbolPadding + bBox.width + itemDistance + (showCheckbox ? 20 : 0); + legend.itemHeight = itemHeight = mathRound(item.legendItemHeight || bBox.height); - chart.oldChartHeight = chart.chartHeight; - chart.oldChartWidth = chart.chartWidth; - if (defined(width)) { - chart.chartWidth = chartWidth = mathMax(0, mathRound(width)); - chart.hasUserSize = !!chartWidth; - } - if (defined(height)) { - chart.chartHeight = chartHeight = mathMax(0, mathRound(height)); - } + // if the item exceeds the width, start a new line + if (horizontal && legend.itemX - initialItemX + itemWidth > + (widthOption || (chart.chartWidth - 2 * padding - initialItemX - options.x))) { + legend.itemX = initialItemX; + legend.itemY += itemMarginTop + legend.lastLineHeight + itemMarginBottom; + legend.lastLineHeight = 0; // reset for next line (#915, #3976) + } - // Resize the container with the global animation applied if enabled (#2503) - globalAnimation = renderer.globalAnimation; - (globalAnimation ? animate : css)(chart.container, { - width: chartWidth + PX, - height: chartHeight + PX - }, globalAnimation); + // If the item exceeds the height, start a new column + /*if (!horizontal && legend.itemY + options.y + itemHeight > chart.chartHeight - spacingTop - spacingBottom) { + legend.itemY = legend.initialItemY; + legend.itemX += legend.maxItemWidth; + legend.maxItemWidth = 0; + }*/ - chart.setChartSize(true); - renderer.setSize(chartWidth, chartHeight, animation); + // Set the edge positions + legend.maxItemWidth = mathMax(legend.maxItemWidth, itemWidth); + legend.lastItemY = itemMarginTop + legend.itemY + itemMarginBottom; + legend.lastLineHeight = mathMax(itemHeight, legend.lastLineHeight); // #915 - // handle axes - chart.maxTicks = null; - each(chart.axes, function (axis) { - axis.isDirty = true; - axis.setScale(); - }); + // cache the position of the newly generated or reordered items + item._legendItemPos = [legend.itemX, legend.itemY]; - // make sure non-cartesian series are also handled - each(chart.series, function (serie) { - serie.isDirty = true; - }); + // advance + if (horizontal) { + legend.itemX += itemWidth; - chart.isDirtyLegend = true; // force legend redraw - chart.isDirtyBox = true; // force redraw of plot and chart border + } else { + legend.itemY += itemMarginTop + itemHeight + itemMarginBottom; + legend.lastLineHeight = itemHeight; + } - chart.layOutTitles(); // #2857 - chart.getMargins(); + // the width of the widest item + legend.offsetWidth = widthOption || mathMax( + (horizontal ? legend.itemX - initialItemX - itemDistance : itemWidth) + padding, + legend.offsetWidth + ); + }, - chart.redraw(animation); + /** + * Get all items, which is one item per series for normal series and one item per point + * for pie series. + */ + getAllItems: function () { + var allItems = []; + each(this.chart.series, function (series) { + var seriesOptions = series.options; + // Handle showInLegend. If the series is linked to another series, defaults to false. + if (!pick(seriesOptions.showInLegend, !defined(seriesOptions.linkedTo) ? UNDEFINED : false, true)) { + return; + } - chart.oldChartHeight = null; - fireEvent(chart, 'resize'); + // use points or series for the legend item depending on legendType + allItems = allItems.concat( + series.legendItems || + (seriesOptions.legendType === 'point' ? + series.data : + series) + ); + }); + return allItems; + }, - // Fire endResize and set isResizing back. If animation is disabled, fire without delay - globalAnimation = renderer.globalAnimation; // Reassign it before using it, it may have changed since the top of this function. - if (globalAnimation === false) { - fireEndResize(); - } else { // else set a timeout with the animation duration - setTimeout(fireEndResize, (globalAnimation && globalAnimation.duration) || 500); - } - }, + /** + * Adjust the chart margins by reserving space for the legend on only one side + * of the chart. If the position is set to a corner, top or bottom is reserved + * for horizontal legends and left or right for vertical ones. + */ + adjustMargins: function (margin, spacing) { + var chart = this.chart, + options = this.options, + // Use the first letter of each alignment option in order to detect the side + alignment = options.align.charAt(0) + options.verticalAlign.charAt(0) + options.layout.charAt(0); // #4189 - use charAt(x) notation instead of [x] for IE7 - /** - * Set the public chart properties. This is done before and after the pre-render - * to determine margin sizes - */ - setChartSize: function (skipAxes) { - var chart = this, - inverted = chart.inverted, - renderer = chart.renderer, - chartWidth = chart.chartWidth, - chartHeight = chart.chartHeight, - optionsChart = chart.options.chart, - spacing = chart.spacing, - clipOffset = chart.clipOffset, - clipX, - clipY, - plotLeft, - plotTop, - plotWidth, - plotHeight, - plotBorderWidth; + if (this.display && !options.floating) { - chart.plotLeft = plotLeft = mathRound(chart.plotLeft); - chart.plotTop = plotTop = mathRound(chart.plotTop); - chart.plotWidth = plotWidth = mathMax(0, mathRound(chartWidth - plotLeft - chart.marginRight)); - chart.plotHeight = plotHeight = mathMax(0, mathRound(chartHeight - plotTop - chart.marginBottom)); + each([ + /(lth|ct|rth)/, + /(rtv|rm|rbv)/, + /(rbh|cb|lbh)/, + /(lbv|lm|ltv)/ + ], function (alignments, side) { + if (alignments.test(alignment) && !defined(margin[side])) { + // Now we have detected on which side of the chart we should reserve space for the legend + chart[marginNames[side]] = mathMax( + chart[marginNames[side]], + chart.legend[(side + 1) % 2 ? 'legendHeight' : 'legendWidth'] + + [1, -1, -1, 1][side] * options[(side % 2) ? 'x' : 'y'] + + pick(options.margin, 12) + + spacing[side] + ); + } + }); + } + }, - chart.plotSizeX = inverted ? plotHeight : plotWidth; - chart.plotSizeY = inverted ? plotWidth : plotHeight; - - chart.plotBorderWidth = optionsChart.plotBorderWidth || 0; + /** + * Render the legend. This method can be called both before and after + * chart.render. If called after, it will only rearrange items instead + * of creating new ones. + */ + render: function () { + var legend = this, + chart = legend.chart, + renderer = chart.renderer, + legendGroup = legend.group, + allItems, + display, + legendWidth, + legendHeight, + box = legend.box, + options = legend.options, + padding = legend.padding, + legendBorderWidth = options.borderWidth, + legendBackgroundColor = options.backgroundColor; - // Set boxes used for alignment - chart.spacingBox = renderer.spacingBox = { - x: spacing[3], - y: spacing[0], - width: chartWidth - spacing[3] - spacing[1], - height: chartHeight - spacing[0] - spacing[2] - }; - chart.plotBox = renderer.plotBox = { - x: plotLeft, - y: plotTop, - width: plotWidth, - height: plotHeight - }; + legend.itemX = legend.initialItemX; + legend.itemY = legend.initialItemY; + legend.offsetWidth = 0; + legend.lastItemY = 0; - plotBorderWidth = 2 * mathFloor(chart.plotBorderWidth / 2); - clipX = mathCeil(mathMax(plotBorderWidth, clipOffset[3]) / 2); - clipY = mathCeil(mathMax(plotBorderWidth, clipOffset[0]) / 2); - chart.clipBox = { - x: clipX, - y: clipY, - width: mathFloor(chart.plotSizeX - mathMax(plotBorderWidth, clipOffset[1]) / 2 - clipX), - height: mathMax(0, mathFloor(chart.plotSizeY - mathMax(plotBorderWidth, clipOffset[2]) / 2 - clipY)) - }; + if (!legendGroup) { + legend.group = legendGroup = renderer.g('legend') + .attr({ zIndex: 7 }) + .add(); + legend.contentGroup = renderer.g() + .attr({ zIndex: 1 }) // above background + .add(legendGroup); + legend.scrollGroup = renderer.g() + .add(legend.contentGroup); + } - if (!skipAxes) { - each(chart.axes, function (axis) { - axis.setAxisSize(); - axis.setAxisTranslation(); - }); - } - }, + legend.renderTitle(); - /** - * Initial margins before auto size margins are applied - */ - resetMargins: function () { - var chart = this; + // add each series or point + allItems = legend.getAllItems(); - each(marginNames, function (m, side) { - chart[m] = pick(chart.margin[side], chart.spacing[side]); - }); - chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left - chart.clipOffset = [0, 0, 0, 0]; - }, + // sort by legendIndex + stableSort(allItems, function (a, b) { + return ((a.options && a.options.legendIndex) || 0) - ((b.options && b.options.legendIndex) || 0); + }); - /** - * Draw the borders and backgrounds for chart and plot area - */ - drawChartBox: function () { - var chart = this, - optionsChart = chart.options.chart, - renderer = chart.renderer, - chartWidth = chart.chartWidth, - chartHeight = chart.chartHeight, - chartBackground = chart.chartBackground, - plotBackground = chart.plotBackground, - plotBorder = chart.plotBorder, - plotBGImage = chart.plotBGImage, - chartBorderWidth = optionsChart.borderWidth || 0, - chartBackgroundColor = optionsChart.backgroundColor, - plotBackgroundColor = optionsChart.plotBackgroundColor, - plotBackgroundImage = optionsChart.plotBackgroundImage, - plotBorderWidth = optionsChart.plotBorderWidth || 0, - mgn, - bgAttr, - plotLeft = chart.plotLeft, - plotTop = chart.plotTop, - plotWidth = chart.plotWidth, - plotHeight = chart.plotHeight, - plotBox = chart.plotBox, - clipRect = chart.clipRect, - clipBox = chart.clipBox; + // reversed legend + if (options.reversed) { + allItems.reverse(); + } - // Chart area - mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0); + legend.allItems = allItems; + legend.display = display = !!allItems.length; - if (chartBorderWidth || chartBackgroundColor) { - if (!chartBackground) { - - bgAttr = { - fill: chartBackgroundColor || NONE - }; - if (chartBorderWidth) { // #980 - bgAttr.stroke = optionsChart.borderColor; - bgAttr['stroke-width'] = chartBorderWidth; - } - chart.chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn, - optionsChart.borderRadius, chartBorderWidth) - .attr(bgAttr) - .addClass(PREFIX + 'background') - .add() - .shadow(optionsChart.shadow); + // render the items + legend.lastLineHeight = 0; + each(allItems, function (item) { + legend.renderItem(item); + }); - } else { // resize - chartBackground.animate( - chartBackground.crisp({ width: chartWidth - mgn, height: chartHeight - mgn }) - ); - } - } + // Get the box + legendWidth = (options.width || legend.offsetWidth) + padding; + legendHeight = legend.lastItemY + legend.lastLineHeight + legend.titleHeight; + legendHeight = legend.handleOverflow(legendHeight); + legendHeight += padding; + // Draw the border and/or background + if (legendBorderWidth || legendBackgroundColor) { - // Plot background - if (plotBackgroundColor) { - if (!plotBackground) { - chart.plotBackground = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0) - .attr({ - fill: plotBackgroundColor - }) - .add() - .shadow(optionsChart.plotShadow); - } else { - plotBackground.animate(plotBox); - } - } - if (plotBackgroundImage) { - if (!plotBGImage) { - chart.plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight) - .add(); - } else { - plotBGImage.animate(plotBox); - } - } - - // Plot clip - if (!clipRect) { - chart.clipRect = renderer.clipRect(clipBox); - } else { - clipRect.animate({ - width: clipBox.width, - height: clipBox.height - }); - } + if (!box) { + legend.box = box = renderer.rect( + 0, + 0, + legendWidth, + legendHeight, + options.borderRadius, + legendBorderWidth || 0 + ).attr({ + stroke: options.borderColor, + 'stroke-width': legendBorderWidth || 0, + fill: legendBackgroundColor || NONE + }) + .add(legendGroup) + .shadow(options.shadow); + box.isNew = true; - // Plot area border - if (plotBorderWidth) { - if (!plotBorder) { - chart.plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, -plotBorderWidth) - .attr({ - stroke: optionsChart.plotBorderColor, - 'stroke-width': plotBorderWidth, - fill: NONE, - zIndex: 1 - }) - .add(); - } else { - plotBorder.animate( - plotBorder.crisp({ x: plotLeft, y: plotTop, width: plotWidth, height: plotHeight, strokeWidth: -plotBorderWidth }) //#3282 plotBorder should be negative - ); - } - } + } else if (legendWidth > 0 && legendHeight > 0) { + box[box.isNew ? 'attr' : 'animate']( + box.crisp({ width: legendWidth, height: legendHeight }) + ); + box.isNew = false; + } - // reset - chart.isDirtyBox = false; - }, + // hide the border if no items + box[display ? 'show' : 'hide'](); + } - /** - * Detect whether a certain chart property is needed based on inspecting its options - * and series. This mainly applies to the chart.invert property, and in extensions to - * the chart.angular and chart.polar properties. - */ - propFromSeries: function () { - var chart = this, - optionsChart = chart.options.chart, - klass, - seriesOptions = chart.options.series, - i, - value; - - - each(['inverted', 'angular', 'polar'], function (key) { - - // The default series type's class - klass = seriesTypes[optionsChart.type || optionsChart.defaultSeriesType]; - - // Get the value from available chart-wide properties - value = ( - chart[key] || // 1. it is set before - optionsChart[key] || // 2. it is set in the options - (klass && klass.prototype[key]) // 3. it's default series class requires it - ); - - // 4. Check if any the chart's series require it - i = seriesOptions && seriesOptions.length; - while (!value && i--) { - klass = seriesTypes[seriesOptions[i].type]; - if (klass && klass.prototype[key]) { - value = true; - } - } - - // Set the chart property - chart[key] = value; - }); - - }, + legend.legendWidth = legendWidth; + legend.legendHeight = legendHeight; - /** - * Link two or more series together. This is done initially from Chart.render, - * and after Chart.addSeries and Series.remove. - */ - linkSeries: function () { - var chart = this, - chartSeries = chart.series; + // Now that the legend width and height are established, put the items in the + // final position + each(allItems, function (item) { + legend.positionItem(item); + }); - // Reset links - each(chartSeries, function (series) { - series.linkedSeries.length = 0; - }); + // 1.x compatibility: positioning based on style + /*var props = ['left', 'right', 'top', 'bottom'], + prop, + i = 4; + while (i--) { + prop = props[i]; + if (options.style[prop] && options.style[prop] !== 'auto') { + options[i < 2 ? 'align' : 'verticalAlign'] = prop; + options[i < 2 ? 'x' : 'y'] = pInt(options.style[prop]) * (i % 2 ? -1 : 1); + } + }*/ - // Apply new links - each(chartSeries, function (series) { - var linkedTo = series.options.linkedTo; - if (isString(linkedTo)) { - if (linkedTo === ':previous') { - linkedTo = chart.series[series.index - 1]; - } else { - linkedTo = chart.get(linkedTo); - } - if (linkedTo) { - linkedTo.linkedSeries.push(series); - series.linkedParent = linkedTo; - series.visible = pick(series.options.visible, linkedTo.options.visible, series.visible); // #3879 - } - } - }); - }, + if (display) { + legendGroup.align(extend({ + width: legendWidth, + height: legendHeight + }, options), true, 'spacingBox'); + } - /** - * Render series for the chart - */ - renderSeries: function () { - each(this.series, function (serie) { - serie.translate(); - serie.render(); - }); - }, - - /** - * Render labels for the chart - */ - renderLabels: function () { - var chart = this, - labels = chart.options.labels; - if (labels.items) { - each(labels.items, function (label) { - var style = extend(labels.style, label.style), - x = pInt(style.left) + chart.plotLeft, - y = pInt(style.top) + chart.plotTop + 12; + if (!chart.isResizing) { + this.positionCheckboxes(); + } + }, - // delete to prevent rewriting in IE - delete style.left; - delete style.top; + /** + * Set up the overflow handling by adding navigation with up and down arrows below the + * legend. + */ + handleOverflow: function (legendHeight) { + var legend = this, + chart = this.chart, + renderer = chart.renderer, + options = this.options, + optionsY = options.y, + alignTop = options.verticalAlign === 'top', + spaceHeight = chart.spacingBox.height + (alignTop ? -optionsY : optionsY) - this.padding, + maxHeight = options.maxHeight, + clipHeight, + clipRect = this.clipRect, + navOptions = options.navigation, + animation = pick(navOptions.animation, true), + arrowSize = navOptions.arrowSize || 12, + nav = this.nav, + pages = this.pages, + padding = this.padding, + lastY, + allItems = this.allItems, + clipToHeight = function (height) { + clipRect.attr({ + height: height + }); - chart.renderer.text( - label.html, - x, - y - ) - .attr({ zIndex: 2 }) - .css(style) - .add(); + // useHTML + if (legend.contentGroup.div) { + legend.contentGroup.div.style.clip = 'rect(' + padding + 'px,9999px,' + (padding + height) + 'px,0)'; + } + }; - }); - } - }, - /** - * Render all graphics for the chart - */ - render: function () { - var chart = this, - axes = chart.axes, - renderer = chart.renderer, - options = chart.options, - tempWidth, - tempHeight, - redoHorizontal, - redoVertical; + // Adjust the height + if (options.layout === 'horizontal') { + spaceHeight /= 2; + } + if (maxHeight) { + spaceHeight = mathMin(spaceHeight, maxHeight); + } - // Title - chart.setTitle(); + // Reset the legend height and adjust the clipping rectangle + pages.length = 0; + if (legendHeight > spaceHeight) { + this.clipHeight = clipHeight = mathMax(spaceHeight - 20 - this.titleHeight - padding, 0); + this.currentPage = pick(this.currentPage, 1); + this.fullHeight = legendHeight; - // Legend - chart.legend = new Legend(chart, options.legend); + // Fill pages with Y positions so that the top of each a legend item defines + // the scroll top for each page (#2098) + each(allItems, function (item, i) { + var y = item._legendItemPos[1], + h = mathRound(item.legendItem.getBBox().height), + len = pages.length; - // Get stacks - if (chart.getStacks) { - chart.getStacks(); - } + if (!len || (y - pages[len - 1] > clipHeight && (lastY || y) !== pages[len - 1])) { + pages.push(lastY || y); + len++; + } - // Get chart margins - chart.getMargins(true); - chart.setChartSize(); + if (i === allItems.length - 1 && y + h - pages[len - 1] > clipHeight) { + pages.push(y); + } + if (y !== lastY) { + lastY = y; + } + }); - // Record preliminary dimensions for later comparison - tempWidth = chart.plotWidth; - tempHeight = chart.plotHeight = chart.plotHeight - 13; // 13 is the most common height of X axis labels + // Only apply clipping if needed. Clipping causes blurred legend in PDF export (#1787) + if (!clipRect) { + clipRect = legend.clipRect = renderer.clipRect(0, padding, 9999, 0); + legend.contentGroup.clip(clipRect); + } - // Get margins by pre-rendering axes - each(axes, function (axis) { - axis.setScale(); - }); - chart.getAxisMargins(); + clipToHeight(clipHeight); - // If the plot area size has changed significantly, calculate tick positions again - redoHorizontal = tempWidth / chart.plotWidth > 1.1; - redoVertical = tempHeight / chart.plotHeight > 1.1; + // Add navigation elements + if (!nav) { + this.nav = nav = renderer.g().attr({ zIndex: 1 }).add(this.group); + this.up = renderer.symbol('triangle', 0, 0, arrowSize, arrowSize) + .on('click', function () { + legend.scroll(-1, animation); + }) + .add(nav); + this.pager = renderer.text('', 15, 10) + .css(navOptions.style) + .add(nav); + this.down = renderer.symbol('triangle-down', 0, 0, arrowSize, arrowSize) + .on('click', function () { + legend.scroll(1, animation); + }) + .add(nav); + } - if (redoHorizontal || redoVertical) { + // Set initial position + legend.scroll(0); - chart.maxTicks = null; // reset for second pass - each(axes, function (axis) { - if ((axis.horiz && redoHorizontal) || (!axis.horiz && redoVertical)) { - axis.setTickInterval(true); // update to reflect the new margins - } - }); - chart.getMargins(); // second pass to check for new labels - } + legendHeight = spaceHeight; - // Draw the borders and backgrounds - chart.drawChartBox(); + } else if (nav) { + clipToHeight(chart.chartHeight); + nav.hide(); + this.scrollGroup.attr({ + translateY: 1 + }); + this.clipHeight = 0; // #1379 + } + return legendHeight; + }, - // Axes - if (chart.hasCartesianSeries) { - each(axes, function (axis) { - if (axis.visible) { - axis.render(); - } - }); - } + /** + * Scroll the legend by a number of pages + * @param {Object} scrollBy + * @param {Object} animation + */ + scroll: function (scrollBy, animation) { + var pages = this.pages, + pageCount = pages.length, + currentPage = this.currentPage + scrollBy, + clipHeight = this.clipHeight, + navOptions = this.options.navigation, + activeColor = navOptions.activeColor, + inactiveColor = navOptions.inactiveColor, + pager = this.pager, + padding = this.padding, + scrollOffset; - // The series - if (!chart.seriesGroup) { - chart.seriesGroup = renderer.g('series-group') - .attr({ zIndex: 3 }) - .add(); - } - chart.renderSeries(); + // When resizing while looking at the last page + if (currentPage > pageCount) { + currentPage = pageCount; + } - // Labels - chart.renderLabels(); + if (currentPage > 0) { - // Credits - chart.showCredits(options.credits); + if (animation !== UNDEFINED) { + setAnimation(animation, this.chart); + } - // Set flag - chart.hasRendered = true; + this.nav.attr({ + translateX: padding, + translateY: clipHeight + this.padding + 7 + this.titleHeight, + visibility: VISIBLE + }); + this.up.attr({ + fill: currentPage === 1 ? inactiveColor : activeColor + }) + .css({ + cursor: currentPage === 1 ? 'default' : 'pointer' + }); + pager.attr({ + text: currentPage + '/' + pageCount + }); + this.down.attr({ + x: 18 + this.pager.getBBox().width, // adjust to text width + fill: currentPage === pageCount ? inactiveColor : activeColor + }) + .css({ + cursor: currentPage === pageCount ? 'default' : 'pointer' + }); - }, + scrollOffset = -pages[currentPage - 1] + this.initialItemY; - /** - * Show chart credits based on config options - */ - showCredits: function (credits) { - if (credits.enabled && !this.credits) { - this.credits = this.renderer.text( - credits.text, - 0, - 0 - ) - .on('click', function () { - if (credits.href) { - location.href = credits.href; - } - }) - .attr({ - align: credits.position.align, - zIndex: 8 - }) - .css(credits.style) - .add() - .align(credits.position); - } - }, + this.scrollGroup.animate({ + translateY: scrollOffset + }); - /** - * Clean up memory usage - */ - destroy: function () { - var chart = this, - axes = chart.axes, - series = chart.series, - container = chart.container, - i, - parentNode = container && container.parentNode; - - // fire the chart.destoy event - fireEvent(chart, 'destroy'); - - // Delete the chart from charts lookup array - charts[chart.index] = UNDEFINED; - chartCount--; - chart.renderTo.removeAttribute('data-highcharts-chart'); + this.currentPage = currentPage; + this.positionCheckboxes(scrollOffset); + } - // remove events - removeEvent(chart); + } - // ==== Destroy collections: - // Destroy axes - i = axes.length; - while (i--) { - axes[i] = axes[i].destroy(); - } + }; - // Destroy each series - i = series.length; - while (i--) { - series[i] = series[i].destroy(); - } + /* + * LegendSymbolMixin + */ - // ==== Destroy chart properties: - each(['title', 'subtitle', 'chartBackground', 'plotBackground', 'plotBGImage', - 'plotBorder', 'seriesGroup', 'clipRect', 'credits', 'pointer', 'scroller', - 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', 'renderer'], function (name) { - var prop = chart[name]; + var LegendSymbolMixin = Highcharts.LegendSymbolMixin = { - if (prop && prop.destroy) { - chart[name] = prop.destroy(); - } - }); + /** + * Get the series' symbol in the legend + * + * @param {Object} legend The legend object + * @param {Object} item The series (this) or point + */ + drawRectangle: function (legend, item) { + var symbolHeight = legend.options.symbolHeight || legend.fontMetrics.f; - // remove container and all SVG - if (container) { // can break in IE when destroyed before finished loading - container.innerHTML = ''; - removeEvent(container); - if (parentNode) { - discardElement(container); - } + item.legendSymbol = this.chart.renderer.rect( + 0, + legend.baseline - symbolHeight + 1, // #3988 + legend.symbolWidth, + symbolHeight, + legend.options.symbolRadius || 0 + ).attr({ + zIndex: 3 + }).add(item.legendGroup); - } + }, - // clean it all up - for (i in chart) { - delete chart[i]; - } + /** + * Get the series' symbol in the legend. This method should be overridable to create custom + * symbols through Highcharts.seriesTypes[type].prototype.drawLegendSymbols. + * + * @param {Object} legend The legend object + */ + drawLineMarker: function (legend) { - }, + var options = this.options, + markerOptions = options.marker, + radius, + legendSymbol, + symbolWidth = legend.symbolWidth, + renderer = this.chart.renderer, + legendItemGroup = this.legendGroup, + verticalCenter = legend.baseline - mathRound(legend.fontMetrics.b * 0.3), + attr; + // Draw the line + if (options.lineWidth) { + attr = { + 'stroke-width': options.lineWidth + }; + if (options.dashStyle) { + attr.dashstyle = options.dashStyle; + } + this.legendLine = renderer.path([ + M, + 0, + verticalCenter, + L, + symbolWidth, + verticalCenter + ]) + .attr(attr) + .add(legendItemGroup); + } - /** - * VML namespaces can't be added until after complete. Listening - * for Perini's doScroll hack is not enough. - */ - isReadyToRender: function () { - var chart = this; + // Draw the marker + if (markerOptions && markerOptions.enabled !== false) { + radius = markerOptions.radius; + this.legendSymbol = legendSymbol = renderer.symbol( + this.symbol, + (symbolWidth / 2) - radius, + verticalCenter - radius, + 2 * radius, + 2 * radius + ) + .add(legendItemGroup); + legendSymbol.isMarker = true; + } + } + }; - // Note: in spite of JSLint's complaints, win == win.top is required - /*jslint eqeq: true*/ - if ((!hasSVG && (win == win.top && doc.readyState !== 'complete')) || (useCanVG && !win.canvg)) { - /*jslint eqeq: false*/ - if (useCanVG) { - // Delay rendering until canvg library is downloaded and ready - CanVGController.push(function () { chart.firstRender(); }, chart.options.global.canvasToolsURL); - } else { - doc.attachEvent('onreadystatechange', function () { - doc.detachEvent('onreadystatechange', chart.firstRender); - if (doc.readyState === 'complete') { - chart.firstRender(); - } - }); - } - return false; - } - return true; - }, + // Workaround for #2030, horizontal legend items not displaying in IE11 Preview, + // and for #2580, a similar drawing flaw in Firefox 26. + // Explore if there's a general cause for this. The problem may be related + // to nested group elements, as the legend item texts are within 4 group elements. + if (/Trident\/7\.0/.test(userAgent) || isFirefox) { + wrap(Legend.prototype, 'positionItem', function (proceed, item) { + var legend = this, + runPositionItem = function () { // If chart destroyed in sync, this is undefined (#2030) + if (item._legendItemPos) { + proceed.call(legend, item); + } + }; - /** - * Prepare for first rendering after all data are loaded - */ - firstRender: function () { - var chart = this, - options = chart.options, - callback = chart.callback; + // Do it now, for export and to get checkbox placement + runPositionItem(); - // Check whether the chart is ready to render - if (!chart.isReadyToRender()) { - return; - } + // Do it after to work around the core issue + setTimeout(runPositionItem); + }); + } + /** + * The chart class + * @param {Object} options + * @param {Function} callback Function to run when the chart has loaded + */ + var Chart = Highcharts.Chart = function () { + this.init.apply(this, arguments); + }; - // Create the container - chart.getContainer(); + Chart.prototype = { - // Run an early event after the container and renderer are established - fireEvent(chart, 'init'); + /** + * Hook for modules + */ + callbacks: [], - - chart.resetMargins(); - chart.setChartSize(); + /** + * Initialize the chart + */ + init: function (userOptions, callback) { - // Set the common chart properties (mainly invert) from the given series - chart.propFromSeries(); + // Handle regular options + var options, + seriesOptions = userOptions.series; // skip merging data points to increase performance - // get axes - chart.getAxes(); + userOptions.series = null; + options = merge(defaultOptions, userOptions); // do the merge + options.series = userOptions.series = seriesOptions; // set back the series data + this.userOptions = userOptions; - // Initialize the series - each(options.series || [], function (serieOptions) { - chart.initSeries(serieOptions); - }); + var optionsChart = options.chart; - chart.linkSeries(); + // Create margin & spacing array + this.margin = this.splashArray('margin', optionsChart); + this.spacing = this.splashArray('spacing', optionsChart); - // Run an event after axes and series are initialized, but before render. At this stage, - // the series data is indexed and cached in the xData and yData arrays, so we can access - // those before rendering. Used in Highstock. - fireEvent(chart, 'beforeRender'); + var chartEvents = optionsChart.events; - // depends on inverted and on margins being set - if (Highcharts.Pointer) { - chart.pointer = new Pointer(chart, options); - } + //this.runChartClick = chartEvents && !!chartEvents.click; + this.bounds = { h: {}, v: {} }; // Pixel data bounds for touch zoom - chart.render(); + this.callback = callback; + this.isResizing = 0; + this.options = options; + //chartTitleOptions = UNDEFINED; + //chartSubtitleOptions = UNDEFINED; - // add canvas - chart.renderer.draw(); - // run callbacks - if (callback) { - callback.apply(chart, [chart]); - } - each(chart.callbacks, function (fn) { - if (chart.index !== UNDEFINED) { // Chart destroyed in its own callback (#3600) - fn.apply(chart, [chart]); - } - }); - - // Fire the load event - fireEvent(chart, 'load'); - - // If the chart was rendered outside the top container, put it back in (#3679) - chart.cloneRenderTo(true); + this.axes = []; + this.series = []; + this.hasCartesianSeries = optionsChart.showAxes; + //this.axisOffset = UNDEFINED; + //this.maxTicks = UNDEFINED; // handle the greatest amount of ticks on grouped axes + //this.inverted = UNDEFINED; + //this.loadingShown = UNDEFINED; + //this.container = UNDEFINED; + //this.chartWidth = UNDEFINED; + //this.chartHeight = UNDEFINED; + //this.marginRight = UNDEFINED; + //this.marginBottom = UNDEFINED; + //this.containerWidth = UNDEFINED; + //this.containerHeight = UNDEFINED; + //this.oldChartWidth = UNDEFINED; + //this.oldChartHeight = UNDEFINED; - }, + //this.renderTo = UNDEFINED; + //this.renderToClone = UNDEFINED; - /** - * Creates arrays for spacing and margin from given options. - */ - splashArray: function (target, options) { - var oVar = options[target], - tArray = isObject(oVar) ? oVar : [oVar, oVar, oVar, oVar]; + //this.spacingBox = UNDEFINED - return [pick(options[target + 'Top'], tArray[0]), - pick(options[target + 'Right'], tArray[1]), - pick(options[target + 'Bottom'], tArray[2]), - pick(options[target + 'Left'], tArray[3])]; - } -}; // end Chart + //this.legend = UNDEFINED; -var CenteredSeriesMixin = Highcharts.CenteredSeriesMixin = { - /** - * Get the center of the pie based on the size and center options relative to the - * plot area. Borrowed by the polar and gauge series types. - */ - getCenter: function () { - - var options = this.options, - chart = this.chart, - slicingRoom = 2 * (options.slicedOffset || 0), - handleSlicingRoom, - plotWidth = chart.plotWidth - 2 * slicingRoom, - plotHeight = chart.plotHeight - 2 * slicingRoom, - centerOption = options.center, - positions = [pick(centerOption[0], '50%'), pick(centerOption[1], '50%'), options.size || '100%', options.innerSize || 0], - smallestSize = mathMin(plotWidth, plotHeight), - i, - value; + // Elements + //this.chartBackground = UNDEFINED; + //this.plotBackground = UNDEFINED; + //this.plotBGImage = UNDEFINED; + //this.plotBorder = UNDEFINED; + //this.loadingDiv = UNDEFINED; + //this.loadingSpan = UNDEFINED; - for (i = 0; i < 4; ++i) { - value = positions[i]; - handleSlicingRoom = i < 2 || (i === 2 && /%$/.test(value)); - - // i == 0: centerX, relative to width - // i == 1: centerY, relative to height - // i == 2: size, relative to smallestSize - // i == 3: innerSize, relative to size - positions[i] = relativeLength(value, [plotWidth, plotHeight, smallestSize, positions[2]][i]) + - (handleSlicingRoom ? slicingRoom : 0); + var chart = this, + eventType; - } - // innerSize cannot be larger than size (#3632) - if (positions[3] > positions[2]) { - positions[3] = positions[2]; - } - return positions; - } -}; + // Add the chart to the global lookup + chart.index = charts.length; + charts.push(chart); + chartCount++; -/** - * The Point object and prototype. Inheritable and used as base for PiePoint - */ -var Point = function () {}; -Point.prototype = { + // Set up auto resize + if (optionsChart.reflow !== false) { + addEvent(chart, 'load', function () { + chart.initReflow(); + }); + } - /** - * Initialize the point - * @param {Object} series The series object containing this point - * @param {Object} options The data in either number, array or object format - */ - init: function (series, options, x) { + // Chart event handlers + if (chartEvents) { + for (eventType in chartEvents) { + addEvent(chart, eventType, chartEvents[eventType]); + } + } - var point = this, - colors; - point.series = series; - point.color = series.color; // #3445 - point.applyOptions(options, x); - point.pointAttr = {}; + chart.xAxis = []; + chart.yAxis = []; - if (series.options.colorByPoint) { - colors = series.options.colors || series.chart.options.colors; - point.color = point.color || colors[series.colorCounter++]; - // loop back to zero - if (series.colorCounter === colors.length) { - series.colorCounter = 0; - } - } + // Expose methods and variables + chart.animation = useCanVG ? false : pick(optionsChart.animation, true); + chart.pointCount = chart.colorCounter = chart.symbolCounter = 0; - 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. - * - * @param {Object} options - */ - applyOptions: function (options, x) { - var point = this, - series = point.series, - pointValKey = series.options.pointValKey || series.pointValKey; + chart.firstRender(); + }, - options = Point.prototype.optionsToObject.call(this, options); + /** + * Initialize an individual series, called internally before render time + */ + initSeries: function (options) { + var chart = this, + optionsChart = chart.options.chart, + type = options.type || optionsChart.type || optionsChart.defaultSeriesType, + series, + constr = seriesTypes[type]; - // copy options directly to point - extend(point, options); - point.options = point.options ? extend(point.options, options) : options; + // No such series type + if (!constr) { + error(17, true); + } - // For higher dimension series types. For instance, for ranges, point.y is mapped to point.low. - if (pointValKey) { - point.y = point[pointValKey]; - } + series = new constr(); + series.init(this, options); + return series; + }, - // 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 (point.x === UNDEFINED && series) { - point.x = x === UNDEFINED ? series.autoIncrement() : x; - } + /** + * Check whether a given point is within the plot area + * + * @param {Number} plotX Pixel x relative to the plot area + * @param {Number} plotY Pixel y relative to the plot area + * @param {Boolean} inverted Whether the chart is inverted + */ + isInsidePlot: function (plotX, plotY, inverted) { + var x = inverted ? plotY : plotX, + y = inverted ? plotX : plotY; - return point; - }, + return x >= 0 && + x <= this.plotWidth && + y >= 0 && + y <= this.plotHeight; + }, - /** - * Transform number or array configs into objects - */ - optionsToObject: function (options) { - var ret = {}, - series = this.series, - keys = series.options.keys, - pointArrayMap = keys || series.pointArrayMap || ['y'], - valueCount = pointArrayMap.length, - firstItemType, - i = 0, - j = 0; + /** + * Redraw legend, axes or series based on updated data + * + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + redraw: function (animation) { + var chart = this, + axes = chart.axes, + series = chart.series, + pointer = chart.pointer, + legend = chart.legend, + redrawLegend = chart.isDirtyLegend, + hasStackedSeries, + hasDirtyStacks, + hasCartesianSeries = chart.hasCartesianSeries, + isDirtyBox = chart.isDirtyBox, + seriesLength = series.length, + i = seriesLength, + serie, + renderer = chart.renderer, + isHiddenChart = renderer.isHidden(), + afterRedraw = []; - if (typeof options === 'number' || options === null) { - ret[pointArrayMap[0]] = options; + setAnimation(animation, chart); - } else if (isArray(options)) { - // with leading x value - if (!keys && options.length > valueCount) { - firstItemType = typeof options[0]; - if (firstItemType === 'string') { - ret.name = options[0]; - } else if (firstItemType === 'number') { - ret.x = options[0]; - } - i++; - } - while (j < valueCount) { - if (!keys || options[i] !== undefined) { // Skip undefined positions for keys - ret[pointArrayMap[j]] = options[i]; - } - i++; - j++; - } - } else if (typeof options === 'object') { - ret = options; + if (isHiddenChart) { + chart.cloneRenderTo(); + } - // This is the fastest way to detect if there are individual point dataLabels that need - // to be considered in drawDataLabels. These can only occur in object configs. - if (options.dataLabels) { - series._hasPointLabels = true; - } + // Adjust title layout (reflow multiline text) + chart.layOutTitles(); - // Same approach as above for markers - if (options.marker) { - series._hasPointMarkers = true; - } - } - return ret; - }, + // link stacked series + while (i--) { + serie = series[i]; - /** - * Destroy a point to clear memory. Its reference still stays in series.data. - */ - destroy: function () { - var point = this, - series = point.series, - chart = series.chart, - hoverPoints = chart.hoverPoints, - prop; + if (serie.options.stacking) { + hasStackedSeries = true; - chart.pointCount--; + if (serie.isDirty) { + hasDirtyStacks = true; + break; + } + } + } + if (hasDirtyStacks) { // mark others as dirty + i = seriesLength; + while (i--) { + serie = series[i]; + if (serie.options.stacking) { + serie.isDirty = true; + } + } + } - if (hoverPoints) { - point.setState(); - erase(hoverPoints, point); - if (!hoverPoints.length) { - chart.hoverPoints = null; - } + // Handle updated data in the series + each(series, function (serie) { + if (serie.isDirty) { + if (serie.options.legendType === 'point') { + if (serie.updateTotals) { + serie.updateTotals(); + } + redrawLegend = true; + } + } + }); - } - if (point === chart.hoverPoint) { - point.onMouseOut(); - } + // handle added or removed series + if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed + // draw legend graphics + legend.render(); - // remove all events - if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive - removeEvent(point); - point.destroyElements(); - } + chart.isDirtyLegend = false; + } - if (point.legendItem) { // pies have legend items - chart.legend.destroyItem(point); - } + // reset stacks + if (hasStackedSeries) { + chart.getStacks(); + } - for (prop in point) { - point[prop] = null; - } + if (hasCartesianSeries) { + if (!chart.isResizing) { - }, + // reset maxTicks + chart.maxTicks = null; - /** - * Destroy SVG elements associated with the point - */ - destroyElements: function () { - var point = this, - props = ['graphic', 'dataLabel', 'dataLabelUpper', 'connector', 'shadowGroup'], - prop, - i = 6; - while (i--) { - prop = props[i]; - if (point[prop]) { - point[prop] = point[prop].destroy(); - } - } - }, + // set axes scales + each(axes, function (axis) { + axis.setScale(); + }); + } + } - /** - * Return the configuration hash needed for the data label and tooltip formatters - */ - getLabelConfig: function () { - return { - x: this.category, - y: this.y, - color: this.color, - key: this.name || this.category, - series: this.series, - point: this, - percentage: this.percentage, - total: this.total || this.stackTotal - }; - }, + chart.getMargins(); // #3098 - /** - * Extendable method for formatting each point's tooltip line - * - * @return {String} A string to be concatenated in to the common tooltip text - */ - tooltipFormatter: function (pointFormat) { + if (hasCartesianSeries) { + // If one axis is dirty, all axes must be redrawn (#792, #2169) + each(axes, function (axis) { + if (axis.isDirty) { + isDirtyBox = true; + } + }); - // Insert options for valueDecimals, valuePrefix, and valueSuffix - var series = this.series, - seriesTooltipOptions = series.tooltipOptions, - valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''), - valuePrefix = seriesTooltipOptions.valuePrefix || '', - valueSuffix = seriesTooltipOptions.valueSuffix || ''; + // redraw axes + each(axes, function (axis) { - // Loop over the point array map and replace unformatted values with sprintf formatting markup - each(series.pointArrayMap || ['y'], function (key) { - key = '{point.' + key; // without the closing bracket - if (valuePrefix || valueSuffix) { - pointFormat = pointFormat.replace(key + '}', valuePrefix + key + '}' + valueSuffix); - } - pointFormat = pointFormat.replace(key + '}', key + ':,.' + valueDecimals + 'f}'); - }); + // Fire 'afterSetExtremes' only if extremes are set + var key = axis.min + ',' + axis.max; + if (axis.extKey !== key) { // #821, #4452 + axis.extKey = key; + afterRedraw.push(function () { // prevent a recursive call to chart.redraw() (#1119) + fireEvent(axis, 'afterSetExtremes', extend(axis.eventArgs, axis.getExtremes())); // #747, #751 + delete axis.eventArgs; + }); + } + if (isDirtyBox || hasStackedSeries) { + axis.redraw(); + } + }); + } - return format(pointFormat, { - point: this, - series: this.series - }); - }, + // the plot areas size has changed + if (isDirtyBox) { + chart.drawChartBox(); + } - /** - * Fire an event on the Point object. Must not be renamed to fireEvent, as this - * causes a name clash in MooTools - * @param {String} eventType - * @param {Object} eventArgs Additional event arguments - * @param {Function} defaultFunction Default event handler - */ - firePointEvent: function (eventType, eventArgs, defaultFunction) { - var point = this, - series = this.series, - seriesOptions = series.options; - // load event handlers on demand to save time on mouseover/out - if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) { - this.importEvents(); - } + // redraw affected series + each(series, function (serie) { + if (serie.isDirty && serie.visible && + (!serie.isCartesian || serie.xAxis)) { // issue #153 + serie.redraw(); + } + }); - // add default handler if in selection mode - if (eventType === 'click' && seriesOptions.allowPointSelect) { - defaultFunction = function (event) { - // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera - if (point.select) { // Could be destroyed by prior event handlers (#2911) - point.select(null, event.ctrlKey || event.metaKey || event.shiftKey); - } - }; - } + // move tooltip or reset + if (pointer) { + pointer.reset(true); + } - fireEvent(this, eventType, eventArgs, defaultFunction); - }, - visible: true -};/** - * @classDescription 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 - * exceeds the cropThreshold, or if the data is grouped, series.data doesn't contain all the points. It - * only contains the points that have been created on demand. - * - 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. - * - * @param {Object} chart - * @param {Object} options - */ -var Series = Highcharts.Series = function () {}; + // redraw if canvas + renderer.draw(); -Series.prototype = { + // fire the event + fireEvent(chart, 'redraw'); // jQuery breaks this when calling it from addEvent. Overwrites chart.redraw - isCartesian: true, - type: 'line', - pointClass: Point, - sorted: true, // requires the data to be sorted - requireSorting: true, - pointAttrToOptions: { // mapping between SVG attributes and the corresponding options - stroke: 'lineColor', - 'stroke-width': 'lineWidth', - fill: 'fillColor', - r: 'radius' - }, - 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 - 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); - }; + if (isHiddenChart) { + chart.cloneRenderTo(true); + } - series.chart = chart; - series.options = options = series.setOptions(options); // merge with plotOptions - series.linkedSeries = []; + // Fire callbacks that are put on hold until after the redraw + each(afterRedraw, function (callback) { + callback.call(); + }); + }, - // bind the axes - series.bindAxes(); + /** + * Get an axis, series or point object by id. + * @param id {String} The id as given in the configuration options + */ + get: function (id) { + var chart = this, + axes = chart.axes, + series = chart.series; - // set some variables - extend(series, { - name: options.name, - state: NORMAL_STATE, - pointAttr: {}, - visible: options.visible !== false, // true by default - selected: options.selected === true // false by default - }); + var i, + j, + points; - // special - if (useCanVG) { - options.animation = false; - } + // search axes + for (i = 0; i < axes.length; i++) { + if (axes[i].options.id === id) { + return axes[i]; + } + } - // 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; - } + // search series + for (i = 0; i < series.length; i++) { + if (series[i].options.id === id) { + return series[i]; + } + } - series.getColor(); - series.getSymbol(); + // search points + for (i = 0; i < series.length; i++) { + points = series[i].points || []; + for (j = 0; j < points.length; j++) { + if (points[j].id === id) { + return points[j]; + } + } + } + return null; + }, - // Set the data - each(series.parallelArrays, function (key) { - series[key + 'Data'] = []; - }); - series.setData(options.data, false); + /** + * Create the Axis instances based on the config options + */ + getAxes: function () { + var chart = this, + options = this.options, + xAxisOptions = options.xAxis = splat(options.xAxis || {}), + yAxisOptions = options.yAxis = splat(options.yAxis || {}), + optionsArray; - // Mark cartesian - if (series.isCartesian) { - chart.hasCartesianSeries = true; - } + // make sure the options are arrays and add some members + each(xAxisOptions, function (axis, i) { + axis.index = i; + axis.isX = true; + }); - // Register it in the chart - chartSeries.push(series); - series._i = chartSeries.length - 1; + each(yAxisOptions, function (axis, i) { + axis.index = i; + }); - // Sort series according to index option (#248, #1123, #2456) - stableSort(chartSeries, sortByIndex); - if (this.yAxis) { - stableSort(this.yAxis.series, sortByIndex); - } + // concatenate all axis options into one array + optionsArray = xAxisOptions.concat(yAxisOptions); - each(chartSeries, function (series, i) { - series.index = i; - series.name = series.name || 'Series ' + (i + 1); - }); + each(optionsArray, function (axisOptions) { + new Axis(chart, axisOptions); // eslint-disable-line no-new + }); + }, - }, - /** - * 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; + /** + * Get the currently selected points from all series + */ + getSelectedPoints: function () { + var points = []; + each(this.series, function (serie) { + points = points.concat(grep(serie.points || [], function (point) { + return point.selected; + })); + }); + return points; + }, - each(series.axisTypes || [], function (AXIS) { // repeat for xAxis and yAxis + /** + * Get the currently selected series + */ + getSelectedSeries: function () { + return grep(this.series, function (serie) { + return serie.selected; + }); + }, - each(chart[AXIS], function (axis) { // loop through the chart's axis objects - axisOptions = axis.options; + /** + * Show the title and subtitle of the chart + * + * @param titleOptions {Object} New title options + * @param subtitleOptions {Object} New subtitle options + * + */ + setTitle: function (titleOptions, subtitleOptions, redraw) { + var chart = this, + options = chart.options, + chartTitleOptions, + chartSubtitleOptions; - // 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)) { + chartTitleOptions = options.title = merge(options.title, titleOptions); + chartSubtitleOptions = options.subtitle = merge(options.subtitle, subtitleOptions); - // register this series in the axis.series lookup - axis.series.push(series); + // add title and subtitle + each([ + ['title', titleOptions, chartTitleOptions], + ['subtitle', subtitleOptions, chartSubtitleOptions] + ], function (arr) { + var name = arr[0], + title = chart[name], + titleOptions = arr[1], + chartTitleOptions = arr[2]; - // set this series.xAxis or series.yAxis reference - series[AXIS] = axis; + if (title && titleOptions) { + chart[name] = title = title.destroy(); // remove old + } - // mark dirty for redraw - axis.isDirty = true; - } - }); + if (chartTitleOptions && chartTitleOptions.text && !title) { + chart[name] = chart.renderer.text( + chartTitleOptions.text, + 0, + 0, + chartTitleOptions.useHTML + ) + .attr({ + align: chartTitleOptions.align, + 'class': PREFIX + name, + zIndex: chartTitleOptions.zIndex || 4 + }) + .css(chartTitleOptions.style) + .add(); + } + }); + chart.layOutTitles(redraw); + }, - // The series needs an X and an Y axis - if (!series[AXIS] && series.optionalAxis !== AXIS) { - error(18, true); - } + /** + * Lay out the chart titles and cache the full offset height for use in getMargins + */ + layOutTitles: function (redraw) { + var titleOffset = 0, + title = this.title, + subtitle = this.subtitle, + options = this.options, + titleOptions = options.title, + subtitleOptions = options.subtitle, + requiresDirtyBox, + renderer = this.renderer, + autoWidth = this.spacingBox.width - 44; // 44 makes room for default context button - }); - }, + if (title) { + title + .css({ width: (titleOptions.width || autoWidth) + PX }) + .align(extend({ + y: renderer.fontMetrics(titleOptions.style.fontSize, title).b - 3 + }, titleOptions), false, 'spacingBox'); - /** - * 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 = typeof i === 'number' ? - // 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)); - }; + if (!titleOptions.floating && !titleOptions.verticalAlign) { + titleOffset = title.getBBox().height; + } + } + if (subtitle) { + subtitle + .css({ width: (subtitleOptions.width || autoWidth) + PX }) + .align(extend({ + y: titleOffset + (titleOptions.margin - 13) + renderer.fontMetrics(subtitleOptions.style.fontSize, title).b + }, subtitleOptions), false, 'spacingBox'); - each(series.parallelArrays, fn); - }, + if (!subtitleOptions.floating && !subtitleOptions.verticalAlign) { + titleOffset = mathCeil(titleOffset + subtitle.getBBox().height); + } + } - /** - * 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 () { + requiresDirtyBox = this.titleOffset !== titleOffset; + this.titleOffset = titleOffset; // used in getMargins - var options = this.options, - xIncrement = this.xIncrement, - date, - pointInterval, - pointIntervalUnit = options.pointIntervalUnit; - - xIncrement = pick(xIncrement, options.pointStart, 0); - - this.pointInterval = pointInterval = pick(this.pointInterval, options.pointInterval, 1); - - // Added code for pointInterval strings - if (pointIntervalUnit === 'month' || pointIntervalUnit === 'year') { - date = new Date(xIncrement); - date = (pointIntervalUnit === 'month') ? - +date[setMonth](date[getMonth]() + pointInterval) : - +date[setFullYear](date[getFullYear]() + pointInterval); - pointInterval = date - xIncrement; - } - - this.xIncrement = xIncrement + pointInterval; - return xIncrement; - }, + if (!this.isDirtyBox && requiresDirtyBox) { + this.isDirtyBox = requiresDirtyBox; + // Redraw if necessary (#2719, #2744) + if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) { + this.redraw(); + } + } + }, - /** - * Divide the series data into segments divided by null values. - */ - getSegments: function () { - var series = this, - lastNull = -1, - segments = [], - i, - points = series.points, - pointsLength = points.length; + /** + * Get chart width and height according to options and container size + */ + getChartSize: function () { + var chart = this, + optionsChart = chart.options.chart, + widthOption = optionsChart.width, + heightOption = optionsChart.height, + renderTo = chart.renderToClone || chart.renderTo; - if (pointsLength) { // no action required for [] + // get inner width and height from jQuery (#824) + if (!defined(widthOption)) { + chart.containerWidth = adapterRun(renderTo, 'width'); + } + if (!defined(heightOption)) { + chart.containerHeight = adapterRun(renderTo, 'height'); + } - // if connect nulls, just remove null points - if (series.options.connectNulls) { - i = pointsLength; - while (i--) { - if (points[i].y === null) { - points.splice(i, 1); - } - } - if (points.length) { - segments = [points]; - } + chart.chartWidth = mathMax(0, widthOption || chart.containerWidth || 600); // #1393, 1460 + chart.chartHeight = mathMax(0, pick(heightOption, + // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7: + chart.containerHeight > 19 ? chart.containerHeight : 400)); + }, - // else, split on null points - } else { - each(points, function (point, i) { - if (point.y === null) { - if (i > lastNull + 1) { - segments.push(points.slice(lastNull + 1, i)); - } - lastNull = i; - } else if (i === pointsLength - 1) { // last value - segments.push(points.slice(lastNull + 1, i + 1)); - } - }); - } - } + /** + * Create a clone of the chart's renderTo div and place it outside the viewport to allow + * size computation on chart.render and chart.redraw + */ + cloneRenderTo: function (revert) { + var clone = this.renderToClone, + container = this.container; - // register it - series.segments = segments; - }, + // Destroy the clone and bring the container back to the real renderTo div + if (revert) { + if (clone) { + this.renderTo.appendChild(container); + discardElement(clone); + delete this.renderToClone; + } - /** - * 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; + // Set up the clone + } else { + if (container && container.parentNode === this.renderTo) { + this.renderTo.removeChild(container); // do not clone this + } + this.renderToClone = clone = this.renderTo.cloneNode(0); + css(clone, { + position: ABSOLUTE, + top: '-9999px', + display: 'block' // #833 + }); + if (clone.style.setProperty) { // #2631 + clone.style.setProperty('display', 'block', 'important'); + } + doc.body.appendChild(clone); + if (container) { + clone.appendChild(container); + } + } + }, - this.userOptions = itemOptions; + /** + * Get the containing element, determine the size and create the inner container + * div to hold the chart + */ + getContainer: function () { + var chart = this, + container, + options = chart.options, + optionsChart = options.chart, + chartWidth, + chartHeight, + renderTo, + indexAttrName = 'data-highcharts-chart', + oldChartIndex, + Ren, + containerId; - // 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 - ); + chart.renderTo = renderTo = optionsChart.renderTo; + containerId = PREFIX + idCounter++; - // 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 - ); + if (isString(renderTo)) { + chart.renderTo = renderTo = doc.getElementById(renderTo); + } - // Delete marker object if not allowed (#1125) - if (typeOptions.marker === null) { - delete options.marker; - } + // Display an error if the renderTo is wrong + if (!renderTo) { + error(13, true); + } - // 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, - color: options.negativeColor, - fillColor: options.negativeFillColor - }); - } - if (zones.length) { // Push one extra zone for the rest - if (defined(zones[zones.length - 1].value)) { - zones.push({ - color: this.color, - fillColor: this.fillColor - }); - } - } - return options; - }, + // If the container already holds a chart, destroy it. The check for hasRendered is there + // because web pages that are saved to disk from the browser, will preserve the data-highcharts-chart + // attribute and the SVG contents, but not an interactive chart. So in this case, + // charts[oldChartIndex] will point to the wrong chart if any (#2609). + oldChartIndex = pInt(attr(renderTo, indexAttrName)); + if (!isNaN(oldChartIndex) && charts[oldChartIndex] && charts[oldChartIndex].hasRendered) { + charts[oldChartIndex].destroy(); + } - getCyclic: function (prop, value, defaults) { - var i, - userOptions = this.userOptions, - indexName = '_' + prop + 'Index', - counterName = prop + 'Counter'; + // Make a reference to the chart from the div + attr(renderTo, indexAttrName, chart.index); - if (!value) { - if (defined(userOptions[indexName])) { // after Series.update() - i = userOptions[indexName]; - } else { - userOptions[indexName] = i = this.chart[counterName] % defaults.length; - this.chart[counterName] += 1; - } - value = defaults[i]; - } - this[prop] = value; - }, + // remove previous chart + renderTo.innerHTML = ''; - /** - * Get the series' color - */ - 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); - } - }, - /** - * Get the series' symbol - */ - getSymbol: function () { - var seriesMarkerOption = this.options.marker; + // 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 (!optionsChart.skipClone && !renderTo.offsetWidth) { + chart.cloneRenderTo(); + } - this.getCyclic('symbol', seriesMarkerOption.symbol, this.chart.options.symbols); + // get the width and height + chart.getChartSize(); + chartWidth = chart.chartWidth; + chartHeight = chart.chartHeight; - // don't substract radius in image symbols (#604) - if (/^url/.test(this.symbol)) { - seriesMarkerOption.radius = 0; - } - }, + // create the inner container + chart.container = container = createElement(DIV, { + className: PREFIX + 'container' + + (optionsChart.className ? ' ' + optionsChart.className : ''), + id: containerId + }, extend({ + position: RELATIVE, + overflow: HIDDEN, // needed for context menu (avoid scrollbars) and + // content overflow in IE + width: chartWidth + PX, + height: chartHeight + PX, + textAlign: 'left', + lineHeight: 'normal', // #427 + zIndex: 0, // #1072 + '-webkit-tap-highlight-color': 'rgba(0,0,0,0)' + }, optionsChart.style), + chart.renderToClone || renderTo + ); - drawLegendSymbol: LegendSymbolMixin.drawLineMarker, + // cache the cursor (#1650) + chart._cursor = container.style.cursor; - /** - * 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, - hasCategories = xAxis && !!xAxis.categories, - i, - turboThreshold = options.turboThreshold, - pt, - xData = this.xData, - yData = this.yData, - pointArrayMap = series.pointArrayMap, - valueCount = pointArrayMap && pointArrayMap.length; + // Initialize the renderer + Ren = Highcharts[optionsChart.renderer] || Renderer; + chart.renderer = new Ren( + container, + chartWidth, + chartHeight, + optionsChart.style, + optionsChart.forExport, + options.exporting && options.exporting.allowHTML + ); - data = data || []; - dataLength = data.length; - redraw = pick(redraw, true); + if (useCanVG) { + // If we need canvg library, extend and configure the renderer + // to get the tracker for translating mouse events + chart.renderer.create(chart, container, chartWidth, chartHeight); + } + // Add a reference to the charts index + chart.renderer.chartIndex = chart.index; + }, - // 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) { - if (oldData[i].update) { // Linked, previously hidden series (#3709) - oldData[i].update(point, false, null, false); - } - }); + /** + * Calculate margins by rendering axis labels in a preliminary position. Title, + * subtitle and legend have already been rendered at this stage, but will be + * moved into their final positions + */ + getMargins: function (skipAxes) { + var chart = this, + spacing = chart.spacing, + margin = chart.margin, + titleOffset = chart.titleOffset; - } else { + chart.resetMargins(); - // Reset properties - series.xIncrement = null; - series.pointRange = hasCategories ? 1 : options.pointRange; + // Adjust for title and subtitle + if (titleOffset && !defined(margin[0])) { + chart.plotTop = mathMax(chart.plotTop, titleOffset + chart.options.title.margin + spacing[0]); + } - series.colorCounter = 0; // for series with colorByPoint (#1547) - - // Update parallel arrays - each(this.parallelArrays, function (key) { - series[key + 'Data'].length = 0; - }); + // Adjust for legend + chart.legend.adjustMargins(margin, spacing); - // 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) { + // adjust for scroller + if (chart.extraBottomMargin) { + chart.marginBottom += chart.extraBottomMargin; + } + if (chart.extraTopMargin) { + chart.plotTop += chart.extraTopMargin; + } + if (!skipAxes) { + this.getAxisMargins(); + } + }, - // find the first non-null point - i = 0; - while (firstPoint === null && i < dataLength) { - firstPoint = data[i]; - i++; - } + getAxisMargins: function () { + var chart = this, + axisOffset = chart.axisOffset = [0, 0, 0, 0], // top, right, bottom, left + margin = chart.margin; - if (isNumber(firstPoint)) { // assume all points are numbers - var x = pick(options.pointStart, 0), - pointInterval = pick(options.pointInterval, 1); + // pre-render axes to get labels offset width + if (chart.hasCartesianSeries) { + each(chart.axes, function (axis) { + if (axis.visible) { + axis.getOffset(); + } + }); + } - for (i = 0; i < dataLength; i++) { - xData[i] = x; - yData[i] = data[i]; - x += pointInterval; - } - series.xIncrement = x; - } 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 { - 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); - if (hasCategories && defined(pt.name)) { // #4401 - xAxis.names[pt.x] = pt.name; // #2046 - } - } - } - } + // Add the axis offsets + each(marginNames, function (m, side) { + if (!defined(margin[side])) { + chart[m] += axisOffset[side]; + } + }); - // Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON - if (isString(yData[0])) { - error(14, true); - } + chart.setChartSize(); - series.data = []; - series.options.data = data; - //series.zData = zData; + }, - // destroy old points - i = oldDataLength; - while (i--) { - if (oldData[i] && oldData[i].destroy) { - oldData[i].destroy(); - } - } + /** + * Resize the chart to its container if size is not explicitly set + */ + reflow: function (e) { + var chart = this, + optionsChart = chart.options.chart, + renderTo = chart.renderTo, + width = optionsChart.width || adapterRun(renderTo, 'width'), + height = optionsChart.height || adapterRun(renderTo, 'height'), + target = e ? e.target : win; // #805 - MooTools doesn't supply e - // reset minRange (#878) - if (xAxis) { - xAxis.minRange = xAxis.userMinRange; - } + // Width and height checks for display:none. Target is doc in IE8 and Opera, + // win in Firefox, Chrome and IE9. + if (!chart.hasUserSize && !chart.isPrinting && width && height && (target === win || target === doc)) { // #1093 + if (width !== chart.containerWidth || height !== chart.containerHeight) { + clearTimeout(chart.reflowTimeout); + // When called from window.resize, e is set, else it's called directly (#2224) + chart.reflowTimeout = syncTimeout(function () { + if (chart.container) { // It may have been destroyed in the meantime (#1257) + chart.setSize(width, height, false); + chart.hasUserSize = null; + } + }, e ? 100 : 0); + } + chart.containerWidth = width; + chart.containerHeight = height; + } + }, - // redraw - series.isDirty = series.isDirtyData = chart.isDirtyBox = true; - animation = false; - } + /** + * Add the event handlers necessary for auto resizing + */ + initReflow: function () { + var chart = this, + reflow = function (e) { + chart.reflow(e); + }; - // Typically for pie series, points need to be processed and generated - // prior to rendering the legend - if (options.legendType === 'point') { // docs: legendType now supported on more series types (at least column and pie) - this.processData(); - this.generatePoints(); - } - if (redraw) { - chart.redraw(animation); - } - }, + addEvent(win, 'resize', reflow); + addEvent(chart, 'destroy', function () { + removeEvent(win, 'resize', reflow); + }); + }, - /** - * 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, - min, - max; + /** + * Resize the chart to a given width and height + * @param {Number} width + * @param {Number} height + * @param {Object|Boolean} animation + */ + setSize: function (width, height, animation) { + var chart = this, + chartWidth, + chartHeight, + renderer = chart.renderer, + globalAnimation; - // 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; - } + // Handle the isResizing counter + chart.isResizing += 1; + + // set the animation for the current process + setAnimation(animation, chart); - if (xAxis) { - xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053) - min = xExtremes.min; - max = xExtremes.max; - } + chart.oldChartHeight = chart.chartHeight; + chart.oldChartWidth = chart.chartWidth; + if (defined(width)) { + chart.chartWidth = chartWidth = mathMax(0, mathRound(width)); + chart.hasUserSize = !!chartWidth; + } + if (defined(height)) { + chart.chartHeight = chartHeight = mathMax(0, mathRound(height)); + } - // 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 = []; + // Resize the container with the global animation applied if enabled (#2503) + globalAnimation = renderer.globalAnimation; + (globalAnimation ? animate : css)(chart.container, { + width: chartWidth + PX, + height: chartHeight + PX + }, globalAnimation); - // 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; - } - } + chart.setChartSize(true); + renderer.setSize(chartWidth, chartHeight, animation); + // handle axes + chart.maxTicks = null; + each(chart.axes, function (axis) { + axis.isDirty = true; + axis.setScale(); + }); - // Find the closest distance between processed points - for (i = processedXData.length - 1; i >= 0; i--) { - distance = processedXData[i] - processedXData[i - 1]; - - if (distance > 0 && (closestPointRange === UNDEFINED || distance < closestPointRange)) { - closestPointRange = distance; + // make sure non-cartesian series are also handled + each(chart.series, function (serie) { + serie.isDirty = true; + }); - // 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); - } - } + chart.isDirtyLegend = true; // force legend redraw + chart.isDirtyBox = true; // force redraw of plot and chart border - // Record the properties - series.cropped = cropped; // undefined or true - series.cropStart = cropStart; - series.processedXData = processedXData; - series.processedYData = processedYData; + chart.layOutTitles(); // #2857 + chart.getMargins(); - if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC - series.pointRange = closestPointRange || 1; - } - series.closestPointRange = closestPointRange; + chart.redraw(animation); - }, - /** - * 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; + chart.oldChartHeight = null; + fireEvent(chart, 'resize'); - // iterate up to find slice start - for (i = 0; i < dataLength; i++) { - if (xData[i] >= min) { - cropStart = mathMax(0, i - cropShoulder); - break; - } - } + // Fire endResize and set isResizing back. If animation is disabled, fire without delay + globalAnimation = renderer.globalAnimation; // Reassign it before using it, it may have changed since the top of this function. + syncTimeout(function () { + if (chart) { + fireEvent(chart, 'endResize', null, function () { + chart.isResizing -= 1; + }); + } + }, globalAnimation === false ? 0 : ((globalAnimation && globalAnimation.duration) || 500)); + }, - // proceed to find slice end - for (; i < dataLength; i++) { - if (xData[i] > max) { - cropEnd = i + cropShoulder; - break; - } - } + /** + * Set the public chart properties. This is done before and after the pre-render + * to determine margin sizes + */ + setChartSize: function (skipAxes) { + var chart = this, + inverted = chart.inverted, + renderer = chart.renderer, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + optionsChart = chart.options.chart, + spacing = chart.spacing, + clipOffset = chart.clipOffset, + clipX, + clipY, + plotLeft, + plotTop, + plotWidth, + plotHeight, + plotBorderWidth; - return { - xData: xData.slice(cropStart, cropEnd), - yData: yData.slice(cropStart, cropEnd), - start: cropStart, - end: cropEnd - }; - }, + chart.plotLeft = plotLeft = mathRound(chart.plotLeft); + chart.plotTop = plotTop = mathRound(chart.plotTop); + chart.plotWidth = plotWidth = mathMax(0, mathRound(chartWidth - plotLeft - chart.marginRight)); + chart.plotHeight = plotHeight = mathMax(0, mathRound(chartHeight - plotTop - chart.marginBottom)); + chart.plotSizeX = inverted ? plotHeight : plotWidth; + chart.plotSizeY = inverted ? plotWidth : plotHeight; - /** - * 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; + chart.plotBorderWidth = optionsChart.plotBorderWidth || 0; - if (!data && !hasGroupedData) { - var arr = []; - arr.length = dataOptions.length; - data = series.data = arr; - } + // Set boxes used for alignment + chart.spacingBox = renderer.spacingBox = { + x: spacing[3], + y: spacing[0], + width: chartWidth - spacing[3] - spacing[1], + height: chartHeight - spacing[0] - spacing[2] + }; + chart.plotBox = renderer.plotBox = { + x: plotLeft, + y: plotTop, + width: plotWidth, + height: plotHeight + }; - 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].index = cursor; // For faster access in Point.update - } + plotBorderWidth = 2 * mathFloor(chart.plotBorderWidth / 2); + clipX = mathCeil(mathMax(plotBorderWidth, clipOffset[3]) / 2); + clipY = mathCeil(mathMax(plotBorderWidth, clipOffset[0]) / 2); + chart.clipBox = { + x: clipX, + y: clipY, + width: mathFloor(chart.plotSizeX - mathMax(plotBorderWidth, clipOffset[1]) / 2 - clipX), + height: mathMax(0, mathFloor(chart.plotSizeY - mathMax(plotBorderWidth, clipOffset[2]) / 2 - clipY)) + }; - // 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 - } - } - } + if (!skipAxes) { + each(chart.axes, function (axis) { + axis.setAxisSize(); + axis.setAxisTranslation(); + }); + } + }, - series.data = data; - series.points = points; - }, + /** + * Initial margins before auto size margins are applied + */ + resetMargins: function () { + var chart = this; - /** - * 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; + each(marginNames, function (m, side) { + chart[m] = pick(chart.margin[side], chart.spacing[side]); + }); + chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left + chart.clipOffset = [0, 0, 0, 0]; + }, - yData = yData || this.stackedYData || this.processedYData; - yDataLength = yData.length; + /** + * Draw the borders and backgrounds for chart and plot area + */ + drawChartBox: function () { + var chart = this, + optionsChart = chart.options.chart, + renderer = chart.renderer, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + chartBackground = chart.chartBackground, + plotBackground = chart.plotBackground, + plotBorder = chart.plotBorder, + plotBGImage = chart.plotBGImage, + chartBorderWidth = optionsChart.borderWidth || 0, + chartBackgroundColor = optionsChart.backgroundColor, + plotBackgroundColor = optionsChart.plotBackgroundColor, + plotBackgroundImage = optionsChart.plotBackgroundImage, + plotBorderWidth = optionsChart.plotBorderWidth || 0, + mgn, + bgAttr, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + plotBox = chart.plotBox, + clipRect = chart.clipRect, + clipBox = chart.clipBox; - for (i = 0; i < yDataLength; i++) { + // Chart area + mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0); - x = xData[i]; - y = yData[i]; + if (chartBorderWidth || chartBackgroundColor) { + if (!chartBackground) { - // For points within the visible range, including the first point outside the - // visible range, consider y extremes - validValue = y !== null && y !== UNDEFINED && (!yAxis.isLog || (y.length || y > 0)); - withinRange = this.getExtremesFromAll || this.options.getExtremesFromAll || this.cropped || - ((xData[i + 1] || x) >= xMin && (xData[i - 1] || x) <= xMax); + bgAttr = { + fill: chartBackgroundColor || NONE + }; + if (chartBorderWidth) { // #980 + bgAttr.stroke = optionsChart.borderColor; + bgAttr['stroke-width'] = chartBorderWidth; + } + chart.chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn, + optionsChart.borderRadius, chartBorderWidth) + .attr(bgAttr) + .addClass(PREFIX + 'background') + .add() + .shadow(optionsChart.shadow); - if (validValue && withinRange) { + } else { // resize + chartBackground.animate( + chartBackground.crisp({ width: chartWidth - mgn, height: chartHeight - mgn }) + ); + } + } - 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; - } - } - } - 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; + // Plot background + if (plotBackgroundColor) { + if (!plotBackground) { + chart.plotBackground = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0) + .attr({ + fill: plotBackgroundColor + }) + .add() + .shadow(optionsChart.plotShadow); + } else { + plotBackground.animate(plotBox); + } + } + if (plotBackgroundImage) { + if (!plotBGImage) { + chart.plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight) + .add(); + } else { + plotBGImage.animate(plotBox); + } + } - // 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; + // Plot clip + if (!clipRect) { + chart.clipRect = renderer.clipRect(clipBox); + } else { + clipRect.animate({ + width: clipBox.width, + height: clipBox.height + }); + } - // Discard disallowed y values for log axes (#3434) - if (yAxis.isLog && yValue !== null && yValue <= 0) { - point.y = yValue = null; - error(10); - } + // Plot area border + if (plotBorderWidth) { + if (!plotBorder) { + chart.plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, -plotBorderWidth) + .attr({ + stroke: optionsChart.plotBorderColor, + 'stroke-width': plotBorderWidth, + fill: NONE, + zIndex: 1 + }) + .add(); + } else { + plotBorder.animate( + plotBorder.crisp({ x: plotLeft, y: plotTop, width: plotWidth, height: plotHeight, strokeWidth: -plotBorderWidth }) //#3282 plotBorder should be negative + ); + } + } - // Get the plotX translation - point.plotX = plotX = mathMin(mathMax(-1e5, xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement, this.type === 'flags')), 1e5); // #3923 + // reset + chart.isDirtyBox = false; + }, + /** + * Detect whether a certain chart property is needed based on inspecting its options + * and series. This mainly applies to the chart.invert property, and in extensions to + * the chart.angular and chart.polar properties. + */ + propFromSeries: function () { + var chart = this, + optionsChart = chart.options.chart, + klass, + seriesOptions = chart.options.series, + i, + value; - // Calculate the bottom y value for stacked series - if (stacking && series.visible && stack && stack[xValue]) { - stackIndicator = series.getStackIndicator(stackIndicator, xValue, series.index); - pointStack = stack[xValue]; - stackValues = pointStack.points[stackIndicator.key]; - yBottom = stackValues[0]; - yValue = stackValues[1]; - if (yBottom === stackThreshold) { - yBottom = pick(threshold, yAxis.min); - } - if (yAxis.isLog && yBottom <= 0) { // #1200, #1232 - yBottom = null; - } + each(['inverted', 'angular', 'polar'], function (key) { - point.total = point.stackTotal = pointStack.total; - point.percentage = pointStack.total && (point.y / pointStack.total * 100); - point.stackY = yValue; + // The default series type's class + klass = seriesTypes[optionsChart.type || optionsChart.defaultSeriesType]; - // Place the stack label - pointStack.setOffset(series.pointXOffset || 0, series.barW || 0); + // Get the value from available chart-wide properties + value = ( + chart[key] || // 1. it is set before + optionsChart[key] || // 2. it is set in the options + (klass && klass.prototype[key]) // 3. it's default series class requires it + ); - } + // 4. Check if any the chart's series require it + i = seriesOptions && seriesOptions.length; + while (!value && i--) { + klass = seriesTypes[seriesOptions[i].type]; + if (klass && klass.prototype[key]) { + value = true; + } + } - // Set translated yBottom or remove it - point.yBottom = defined(yBottom) ? - yAxis.translate(yBottom, 0, 1, 0, 1) : - null; + // Set the chart property + chart[key] = value; + }); - // 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) ? - mathMin(mathMax(-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; + /** + * Link two or more series together. This is done initially from Chart.render, + * and after Chart.addSeries and Series.remove. + */ + linkSeries: function () { + var chart = this, + chartSeries = chart.series; + // Reset links + each(chartSeries, function (series) { + series.linkedSeries.length = 0; + }); - // Set client related positions for mouse tracking - point.clientX = dynamicallyPlaced ? xAxis.translate(xValue, 0, 0, 0, 1) : plotX; // #1514 + // Apply new links + each(chartSeries, function (series) { + var linkedTo = series.options.linkedTo; + if (isString(linkedTo)) { + if (linkedTo === ':previous') { + linkedTo = chart.series[series.index - 1]; + } else { + linkedTo = chart.get(linkedTo); + } + if (linkedTo) { + linkedTo.linkedSeries.push(series); + series.linkedParent = linkedTo; + series.visible = pick(series.options.visible, linkedTo.options.visible, series.visible); // #3879 + } + } + }); + }, - point.negative = point.y < (threshold || 0); + /** + * Render series for the chart + */ + renderSeries: function () { + each(this.series, function (serie) { + serie.translate(); + serie.render(); + }); + }, - // some API data - point.category = categories && categories[point.x] !== UNDEFINED ? - categories[point.x] : point.x; + /** + * Render labels for the chart + */ + renderLabels: function () { + var chart = this, + labels = chart.options.labels; + if (labels.items) { + each(labels.items, function (label) { + var style = extend(labels.style, label.style), + x = pInt(style.left) + chart.plotLeft, + y = pInt(style.top) + chart.plotTop + 12; - // Determine auto enabling of markers (#3635) - if (i) { - closestPointRangePx = mathMin(closestPointRangePx, mathAbs(plotX - lastPlotX)); - } - lastPlotX = plotX; + // delete to prevent rewriting in IE + delete style.left; + delete style.top; - } + chart.renderer.text( + label.html, + x, + y + ) + .attr({ zIndex: 2 }) + .css(style) + .add(); - series.closestPointRangePx = closestPointRangePx; + }); + } + }, - // now that we have the cropped data, build the segments - series.getSegments(); - }, + /** + * Render all graphics for the chart + */ + render: function () { + var chart = this, + axes = chart.axes, + renderer = chart.renderer, + options = chart.options, + tempWidth, + tempHeight, + redoHorizontal, + redoVertical; - /** - * 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']; + // Title + chart.setTitle(); - // 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) { - clipBox.width = 0; + // Legend + chart.legend = new Legend(chart, options.legend); - 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); - - } - if (animation) { - clipRect.count += 1; - } + // Get stacks + if (chart.getStacks) { + chart.getStacks(); + } - if (options.clip !== false) { - this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect); - this.markerGroup.clip(markerClipRect); - this.sharedClipKey = sharedClipKey; - } + // Get chart margins + chart.getMargins(true); + chart.setChartSize(); - // Remove the shared clipping rectangle when all series are shown - if (!animation) { - clipRect.count -= 1; - if (clipRect.count <= 0 && sharedClipKey && chart[sharedClipKey]) { - if (!seriesClipBox) { - chart[sharedClipKey] = chart[sharedClipKey].destroy(); - } - if (chart[sharedClipKey + 'm']) { - chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy(); - } - } - } - }, + // Record preliminary dimensions for later comparison + tempWidth = chart.plotWidth; + tempHeight = chart.plotHeight = chart.plotHeight - 21; // 21 is the most common correction for X axis labels - /** - * Animate in the series - */ - animate: function (init) { - var series = this, - chart = series.chart, - clipRect, - animation = series.options.animation, - sharedClipKey; + // Get margins by pre-rendering axes + each(axes, function (axis) { + axis.setScale(); + }); + chart.getAxisMargins(); - // Animation option is set to true - if (animation && !isObject(animation)) { - animation = defaultPlotOptions[series.type].animation; - } + // If the plot area size has changed significantly, calculate tick positions again + redoHorizontal = tempWidth / chart.plotWidth > 1.1; + redoVertical = tempHeight / chart.plotHeight > 1.05; // Height is more sensitive - // Initialize the animation. Set up the clipping rectangle. - if (init) { + if (redoHorizontal || redoVertical) { - series.setClip(animation); + chart.maxTicks = null; // reset for second pass + each(axes, function (axis) { + if ((axis.horiz && redoHorizontal) || (!axis.horiz && redoVertical)) { + axis.setTickInterval(true); // update to reflect the new margins + } + }); + chart.getMargins(); // second pass to check for new labels + } - // 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); - } + // Draw the borders and backgrounds + chart.drawChartBox(); - // Delete this function to allow it only once - series.animate = null; - - } - }, - /** - * This runs after animation to land on the final plot clipping - */ - afterAnimate: function () { - this.setClip(); - fireEvent(this, 'afterAnimate'); - }, + // Axes + if (chart.hasCartesianSeries) { + each(axes, function (axis) { + if (axis.visible) { + axis.render(); + } + }); + } - /** - * Draw the markers - */ - drawPoints: function () { - var series = this, - pointAttr, - points = series.points, - chart = series.chart, - plotX, - plotY, - i, - point, - radius, - symbol, - isImage, - graphic, - options = series.options, - seriesMarkerOptions = options.marker, - seriesPointAttr = series.pointAttr[''], - pointMarkerOptions, - hasPointMarker, - enabled, - isInside, - markerGroup = series.markerGroup, - xAxis = series.xAxis, - globallyEnabled = pick( - seriesMarkerOptions.enabled, - xAxis.isRadial, - series.closestPointRangePx > 2 * seriesMarkerOptions.radius - ); + // The series + if (!chart.seriesGroup) { + chart.seriesGroup = renderer.g('series-group') + .attr({ zIndex: 3 }) + .add(); + } + chart.renderSeries(); - if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) { + // Labels + chart.renderLabels(); - i = points.length; - while (i--) { - point = points[i]; - plotX = mathFloor(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; + // Credits + chart.showCredits(options.credits); - // only draw the point if y is defined - if (enabled && plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) { + // Set flag + chart.hasRendered = true; - // shortcuts - pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] || seriesPointAttr; - radius = pointAttr.r; - symbol = pick(pointMarkerOptions.symbol, series.symbol); - isImage = symbol.indexOf('url') === 0; + }, - if (graphic) { // update - graphic[isInside ? 'show' : 'hide'](true) // Since the marker group isn't clipped, each individual marker must be toggled - .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(pointAttr) - .add(markerGroup); - } + /** + * Show chart credits based on config options + */ + showCredits: function (credits) { + if (credits.enabled && !this.credits) { + this.credits = this.renderer.text( + credits.text, + 0, + 0 + ) + .on('click', function () { + if (credits.href) { + location.href = credits.href; + } + }) + .attr({ + align: credits.position.align, + zIndex: 8 + }) + .css(credits.style) + .add() + .align(credits.position); + } + }, - } else if (graphic) { - point.graphic = graphic.destroy(); // #1269 - } - } - } + /** + * Clean up memory usage + */ + destroy: function () { + var chart = this, + axes = chart.axes, + series = chart.series, + container = chart.container, + i, + parentNode = container && container.parentNode; - }, + // fire the chart.destoy event + fireEvent(chart, 'destroy'); - /** - * Convert state properties from API naming conventions to SVG attributes - * - * @param {Object} options API options object - * @param {Object} base1 SVG attribute object to inherit from - * @param {Object} base2 Second level SVG attribute object to inherit from - */ - convertAttribs: function (options, base1, base2, base3) { - var conversion = this.pointAttrToOptions, - attr, - option, - obj = {}; + // Delete the chart from charts lookup array + charts[chart.index] = UNDEFINED; + chartCount--; + chart.renderTo.removeAttribute('data-highcharts-chart'); - options = options || {}; - base1 = base1 || {}; - base2 = base2 || {}; - base3 = base3 || {}; + // remove events + removeEvent(chart); - for (attr in conversion) { - option = conversion[attr]; - obj[attr] = pick(options[option], base1[attr], base2[attr], base3[attr]); - } - return obj; - }, + // ==== Destroy collections: + // Destroy axes + i = axes.length; + while (i--) { + axes[i] = axes[i].destroy(); + } - /** - * Get the state attributes. Each series type has its own set of attributes - * that are allowed to change on a point's state change. Series wide attributes are stored for - * all series, and additionally point specific attributes are stored for all - * points with individual marker options. If such options are not defined for the point, - * a reference to the series wide attributes is stored in point.pointAttr. - */ - getAttribs: function () { - var series = this, - seriesOptions = series.options, - normalOptions = defaultPlotOptions[series.type].marker ? seriesOptions.marker : seriesOptions, - stateOptions = normalOptions.states, - stateOptionsHover = stateOptions[HOVER_STATE], - pointStateOptionsHover, - seriesColor = series.color, - seriesNegativeColor = series.options.negativeColor, - normalDefaults = { - stroke: seriesColor, - fill: seriesColor - }, - points = series.points || [], // #927 - i, - j, - threshold, - point, - seriesPointAttr = [], - pointAttr, - pointAttrToOptions = series.pointAttrToOptions, - hasPointSpecificOptions = series.hasPointSpecificOptions, - defaultLineColor = normalOptions.lineColor, - defaultFillColor = normalOptions.fillColor, - turboThreshold = seriesOptions.turboThreshold, - zones = series.zones, - zoneAxis = series.zoneAxis || 'y', - attr, - key; + // Destroy each series + i = series.length; + while (i--) { + series[i] = series[i].destroy(); + } - // series type specific modifications - if (seriesOptions.marker) { // line, spline, area, areaspline, scatter + // ==== Destroy chart properties: + each(['title', 'subtitle', 'chartBackground', 'plotBackground', 'plotBGImage', + 'plotBorder', 'seriesGroup', 'clipRect', 'credits', 'pointer', 'scroller', + 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', 'renderer'], function (name) { + var prop = chart[name]; - // if no hover radius is given, default to normal radius + 2 - stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + stateOptionsHover.radiusPlus; - stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + stateOptionsHover.lineWidthPlus; + if (prop && prop.destroy) { + chart[name] = prop.destroy(); + } + }); - } else { // column, bar, pie + // remove container and all SVG + if (container) { // can break in IE when destroyed before finished loading + container.innerHTML = ''; + removeEvent(container); + if (parentNode) { + discardElement(container); + } - // if no hover color is given, brighten the normal color - stateOptionsHover.color = stateOptionsHover.color || - Color(stateOptionsHover.color || seriesColor) - .brighten(stateOptionsHover.brightness).get(); + } - // if no hover negativeColor is given, brighten the normal negativeColor - stateOptionsHover.negativeColor = stateOptionsHover.negativeColor || - Color(stateOptionsHover.negativeColor || seriesNegativeColor) - .brighten(stateOptionsHover.brightness).get(); - } + // clean it all up + for (i in chart) { + delete chart[i]; + } - // general point attributes for the series normal state - seriesPointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, normalDefaults); + }, - // HOVER_STATE and SELECT_STATE states inherit from normal state except the default radius - each([HOVER_STATE, SELECT_STATE], function (state) { - seriesPointAttr[state] = - series.convertAttribs(stateOptions[state], seriesPointAttr[NORMAL_STATE]); - }); - // set it - series.pointAttr = seriesPointAttr; + /** + * VML namespaces can't be added until after complete. Listening + * for Perini's doScroll hack is not enough. + */ + isReadyToRender: function () { + var chart = this; + // Note: win == win.top is required + if ((!hasSVG && (win == win.top && doc.readyState !== 'complete')) || (useCanVG && !win.canvg)) { // eslint-disable-line eqeqeq + if (useCanVG) { + // Delay rendering until canvg library is downloaded and ready + CanVGController.push(function () { + chart.firstRender(); + }, chart.options.global.canvasToolsURL); + } else { + doc.attachEvent('onreadystatechange', function () { + doc.detachEvent('onreadystatechange', chart.firstRender); + if (doc.readyState === 'complete') { + chart.firstRender(); + } + }); + } + return false; + } + return true; + }, - // Generate the point-specific attribute collections if specific point - // options are given. If not, create a referance to the series wide point - // attributes - i = points.length; - if (!turboThreshold || i < turboThreshold || hasPointSpecificOptions) { - while (i--) { - point = points[i]; - normalOptions = (point.options && point.options.marker) || point.options; - if (normalOptions && normalOptions.enabled === false) { - normalOptions.radius = 0; - } + /** + * Prepare for first rendering after all data are loaded + */ + firstRender: function () { + var chart = this, + options = chart.options, + callback = chart.callback; - if (zones.length) { - j = 0; - threshold = zones[j]; - while (point[zoneAxis] >= threshold.value) { - threshold = zones[++j]; - } - - point.color = point.fillColor = pick(threshold.color, series.color); // #3636, #4267, #4430 - inherit color from series, when color is undefined - - } + // Check whether the chart is ready to render + if (!chart.isReadyToRender()) { + return; + } - hasPointSpecificOptions = seriesOptions.colorByPoint || point.color; // #868 + // Create the container + chart.getContainer(); - // check if the point has specific visual options - if (point.options) { - for (key in pointAttrToOptions) { - if (defined(normalOptions[pointAttrToOptions[key]])) { - hasPointSpecificOptions = true; - } - } - } + // Run an early event after the container and renderer are established + fireEvent(chart, 'init'); - // a specific marker config object is defined for the individual point: - // create it's own attribute collection - if (hasPointSpecificOptions) { - normalOptions = normalOptions || {}; - pointAttr = []; - stateOptions = normalOptions.states || {}; // reassign for individual point - pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {}; - // Handle colors for column and pies - if (!seriesOptions.marker || (point.negative && !pointStateOptionsHover.fillColor && !stateOptionsHover.fillColor)) { // column, bar, point or negative threshold for series with markers (#3636) - // If no hover color is given, brighten the normal color. #1619, #2579 - pointStateOptionsHover[series.pointAttrToOptions.fill] = pointStateOptionsHover.color || (!point.options.color && stateOptionsHover[(point.negative && seriesNegativeColor ? 'negativeColor' : 'color')]) || - Color(point.color) - .brighten(pointStateOptionsHover.brightness || stateOptionsHover.brightness) - .get(); - } + chart.resetMargins(); + chart.setChartSize(); - // normal point state inherits series wide normal state - attr = { color: point.color }; // #868 - if (!defaultFillColor) { // Individual point color or negative color markers (#2219) - attr.fillColor = point.color; - } - if (!defaultLineColor) { - attr.lineColor = point.color; // Bubbles take point color, line markers use white - } - // Color is explicitly set to null or undefined (#1288, #4068) - if (normalOptions.hasOwnProperty('color') && !normalOptions.color) { - delete normalOptions.color; - } - pointAttr[NORMAL_STATE] = series.convertAttribs(extend(attr, normalOptions), seriesPointAttr[NORMAL_STATE]); + // Set the common chart properties (mainly invert) from the given series + chart.propFromSeries(); - // inherit from point normal and series hover - pointAttr[HOVER_STATE] = series.convertAttribs( - stateOptions[HOVER_STATE], - seriesPointAttr[HOVER_STATE], - pointAttr[NORMAL_STATE] - ); + // get axes + chart.getAxes(); - // inherit from point normal and series hover - pointAttr[SELECT_STATE] = series.convertAttribs( - stateOptions[SELECT_STATE], - seriesPointAttr[SELECT_STATE], - pointAttr[NORMAL_STATE] - ); + // Initialize the series + each(options.series || [], function (serieOptions) { + chart.initSeries(serieOptions); + }); + chart.linkSeries(); - // no marker config object is created: copy a reference to the series-wide - // attribute collection - } else { - pointAttr = seriesPointAttr; - } + // Run an event after axes and series are initialized, but before render. At this stage, + // the series data is indexed and cached in the xData and yData arrays, so we can access + // those before rendering. Used in Highstock. + fireEvent(chart, 'beforeRender'); - point.pointAttr = pointAttr; - } - } - }, + // depends on inverted and on margins being set + if (Highcharts.Pointer) { + chart.pointer = new Pointer(chart, options); + } - /** - * Clear DOM objects and free up memory - */ - destroy: function () { - var series = this, - chart = series.chart, - issue134 = /AppleWebKit\/533/.test(userAgent), - destroy, - i, - data = series.data || [], - point, - prop, - axis; + chart.render(); - // add event hook - fireEvent(series, 'destroy'); + // add canvas + chart.renderer.draw(); + // run callbacks + if (callback) { + callback.apply(chart, [chart]); + } + each(chart.callbacks, function (fn) { + if (chart.index !== UNDEFINED) { // Chart destroyed in its own callback (#3600) + fn.apply(chart, [chart]); + } + }); - // remove all events - removeEvent(series); + // Fire the load event + fireEvent(chart, 'load'); - // erase from axes - each(series.axisTypes || [], function (AXIS) { - axis = series[AXIS]; - if (axis) { - erase(axis.series, series); - axis.isDirty = axis.forceRedraw = true; - } - }); + // If the chart was rendered outside the top container, put it back in (#3679) + chart.cloneRenderTo(true); - // 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; + /** + * Creates arrays for spacing and margin from given options. + */ + splashArray: function (target, options) { + var oVar = options[target], + tArray = isObject(oVar) ? oVar : [oVar, oVar, oVar, oVar]; - // Clear the animation timeout if we are destroying the series during initial animation - clearTimeout(series.animationTimeout); + return [pick(options[target + 'Top'], tArray[0]), + pick(options[target + 'Right'], tArray[1]), + pick(options[target + 'Bottom'], tArray[2]), + pick(options[target + 'Left'], tArray[3])]; + } + }; // end Chart - // 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 + var CenteredSeriesMixin = Highcharts.CenteredSeriesMixin = { + /** + * Get the center of the pie based on the size and center options relative to the + * plot area. Borrowed by the polar and gauge series types. + */ + getCenter: function () { - // issue 134 workaround - destroy = issue134 && prop === 'group' ? - 'hide' : - 'destroy'; + var options = this.options, + chart = this.chart, + slicingRoom = 2 * (options.slicedOffset || 0), + handleSlicingRoom, + plotWidth = chart.plotWidth - 2 * slicingRoom, + plotHeight = chart.plotHeight - 2 * slicingRoom, + centerOption = options.center, + positions = [pick(centerOption[0], '50%'), pick(centerOption[1], '50%'), options.size || '100%', options.innerSize || 0], + smallestSize = mathMin(plotWidth, plotHeight), + i, + value; - series[prop][destroy](); - } - } + for (i = 0; i < 4; ++i) { + value = positions[i]; + handleSlicingRoom = i < 2 || (i === 2 && /%$/.test(value)); - // remove from hoverSeries - if (chart.hoverSeries === series) { - chart.hoverSeries = null; - } - erase(chart.series, series); + // i == 0: centerX, relative to width + // i == 1: centerY, relative to height + // i == 2: size, relative to smallestSize + // i == 3: innerSize, relative to size + positions[i] = relativeLength(value, [plotWidth, plotHeight, smallestSize, positions[2]][i]) + + (handleSlicingRoom ? slicingRoom : 0); - // clear all members - for (prop in series) { - delete series[prop]; - } - }, + } + // innerSize cannot be larger than size (#3632) + if (positions[3] > positions[2]) { + positions[3] = positions[2]; + } + return positions; + } + }; - /** - * Return the graph path of a segment - */ - getSegmentPath: function (segment) { - var series = this, - segmentPath = [], - step = series.options.step; + /** + * The Point object and prototype. Inheritable and used as base for PiePoint + */ + var Point = function () {}; + Point.prototype = { - // build the segment line - each(segment, function (point, i) { + /** + * Initialize the point + * @param {Object} series The series object containing this point + * @param {Object} options The data in either number, array or object format + */ + init: function (series, options, x) { - var plotX = point.plotX, - plotY = point.plotY, - lastPoint; + var point = this, + colors; + point.series = series; + point.color = series.color; // #3445 + point.applyOptions(options, x); + point.pointAttr = {}; - if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object - segmentPath.push.apply(segmentPath, series.getPointSpline(segment, point, i)); + if (series.options.colorByPoint) { + colors = series.options.colors || series.chart.options.colors; + point.color = point.color || colors[series.colorCounter++]; + // loop back to zero + if (series.colorCounter === colors.length) { + series.colorCounter = 0; + } + } - } else { + 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. + * + * @param {Object} options + */ + applyOptions: function (options, x) { + var point = this, + series = point.series, + pointValKey = series.options.pointValKey || series.pointValKey; - // moveTo or lineTo - segmentPath.push(i ? L : M); + options = Point.prototype.optionsToObject.call(this, options); - // step line? - if (step && i) { - lastPoint = segment[i - 1]; - if (step === 'right') { - segmentPath.push( - lastPoint.plotX, - plotY, - L - ); + // copy options directly to point + extend(point, options); + point.options = point.options ? extend(point.options, options) : options; - } else if (step === 'center') { - segmentPath.push( - (lastPoint.plotX + plotX) / 2, - lastPoint.plotY, - L, - (lastPoint.plotX + plotX) / 2, - plotY, - L - ); + // For higher dimension series types. For instance, for ranges, point.y is mapped to point.low. + if (pointValKey) { + point.y = point[pointValKey]; + } - } else { - segmentPath.push( - plotX, - lastPoint.plotY, - L - ); - } - } + // 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 (point.x === UNDEFINED && series) { + point.x = x === UNDEFINED ? series.autoIncrement() : x; + } - // normal line to next point - segmentPath.push( - point.plotX, - point.plotY - ); - } - }); + return point; + }, - return segmentPath; - }, + /** + * Transform number or array configs into objects + */ + optionsToObject: function (options) { + var ret = {}, + series = this.series, + keys = series.options.keys, + pointArrayMap = keys || series.pointArrayMap || ['y'], + valueCount = pointArrayMap.length, + firstItemType, + i = 0, + j = 0; - /** - * Get the graph path - */ - getGraphPath: function () { - var series = this, - graphPath = [], - segmentPath, - singlePoints = []; // used in drawTracker + if (typeof options === 'number' || options === null) { + ret[pointArrayMap[0]] = options; - // Divide into segments and build graph and area paths - each(series.segments, function (segment) { + } else if (isArray(options)) { + // with leading x value + if (!keys && options.length > valueCount) { + firstItemType = typeof options[0]; + if (firstItemType === 'string') { + ret.name = options[0]; + } else if (firstItemType === 'number') { + ret.x = options[0]; + } + i++; + } + while (j < valueCount) { + if (!keys || options[i] !== undefined) { // Skip undefined positions for keys + ret[pointArrayMap[j]] = options[i]; + } + i++; + j++; + } + } else if (typeof options === 'object') { + ret = options; - segmentPath = series.getSegmentPath(segment); + // This is the fastest way to detect if there are individual point dataLabels that need + // to be considered in drawDataLabels. These can only occur in object configs. + if (options.dataLabels) { + series._hasPointLabels = true; + } - // add the segment to the graph, or a single point for tracking - if (segment.length > 1) { - graphPath = graphPath.concat(segmentPath); - } else { - singlePoints.push(segment[0]); - } - }); + // Same approach as above for markers + if (options.marker) { + series._hasPointMarkers = true; + } + } + return ret; + }, - // Record it for use in drawGraph and drawTracker, and return graphPath - series.singlePoints = singlePoints; - series.graphPath = graphPath; + /** + * Destroy a point to clear memory. Its reference still stays in series.data. + */ + destroy: function () { + var point = this, + series = point.series, + chart = series.chart, + hoverPoints = chart.hoverPoints, + prop; - return graphPath; + chart.pointCount--; - }, + if (hoverPoints) { + point.setState(); + erase(hoverPoints, point); + if (!hoverPoints.length) { + chart.hoverPoints = null; + } - /** - * Draw the actual graph - */ - drawGraph: function () { - var series = this, - options = this.options, - props = [['graph', options.lineColor || this.color, options.dashStyle]], - lineWidth = options.lineWidth, - roundCap = options.linecap !== 'square', - graphPath = this.getGraphPath(), - fillColor = (this.fillGraph && this.color) || NONE, // polygon series use filled graph - zones = this.zones; + } + if (point === chart.hoverPoint) { + point.onMouseOut(); + } - each(zones, function (threshold, i) { - props.push(['zoneGraph' + i, threshold.color || series.color, threshold.dashStyle || options.dashStyle]); - }); - - // Draw the graph - each(props, function (prop, i) { - var graphKey = prop[0], - graph = series[graphKey], - attribs; + // remove all events + if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive + removeEvent(point); + point.destroyElements(); + } - if (graph) { - graph.animate({ d: graphPath }); + if (point.legendItem) { // pies have legend items + chart.legend.destroyItem(point); + } - } else if ((lineWidth || fillColor) && graphPath.length) { // #1487 - attribs = { - stroke: prop[1], - 'stroke-width': lineWidth, - fill: fillColor, - zIndex: 1 // #1069 - }; - if (prop[2]) { - attribs.dashstyle = prop[2]; - } else if (roundCap) { - attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round'; - } + for (prop in point) { + point[prop] = null; + } - series[graphKey] = series.chart.renderer.path(graphPath) - .attr(attribs) - .add(series.group) - .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 = mathMax(chart.chartWidth, chart.chartHeight), - axis = this[(this.zoneAxis || 'y') + 'Axis'], - extremes, - reversed = axis.reversed, - inverted = chart.inverted, - horiz = axis.horiz, - pxRange, - pxPosMin, - pxPosMax, - ignoreZones = false; + }, - if (zones.length && (graph || area) && axis.min !== UNDEFINED) { - // 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(); - } + /** + * Destroy SVG elements associated with the point + */ + destroyElements: function () { + var point = this, + props = ['graphic', 'dataLabel', 'dataLabelUpper', 'connector', 'shadowGroup'], + prop, + i = 6; + while (i--) { + prop = props[i]; + if (point[prop]) { + point[prop] = point[prop].destroy(); + } + } + }, - // Create the clips - extremes = axis.getExtremes(); - each(zones, function (threshold, i) { + /** + * Return the configuration hash needed for the data label and tooltip formatters + */ + getLabelConfig: function () { + return { + x: this.category, + y: this.y, + color: this.color, + key: this.name || this.category, + series: this.series, + point: this, + percentage: this.percentage, + total: this.total || this.stackTotal + }; + }, - translatedFrom = reversed ? - (horiz ? chart.plotWidth : 0) : - (horiz ? 0 : axis.toPixels(extremes.min)); - translatedFrom = mathMin(mathMax(pick(translatedTo, translatedFrom), 0), chartSizeMax); - translatedTo = mathMin(mathMax(mathRound(axis.toPixels(pick(threshold.value, extremes.max), true)), 0), chartSizeMax); - - if (ignoreZones) { - translatedFrom = translatedTo = axis.toPixels(extremes.max); - } + /** + * Extendable method for formatting each point's tooltip line + * + * @return {String} A string to be concatenated in to the common tooltip text + */ + tooltipFormatter: function (pointFormat) { - pxRange = Math.abs(translatedFrom - translatedTo); - pxPosMin = mathMin(translatedFrom, translatedTo); - pxPosMax = mathMax(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; - } - } + // Insert options for valueDecimals, valuePrefix, and valueSuffix + var series = this.series, + seriesTooltipOptions = series.tooltipOptions, + valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''), + valuePrefix = seriesTooltipOptions.valuePrefix || '', + valueSuffix = seriesTooltipOptions.valueSuffix || ''; - /// VML SUPPPORT - if (chart.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 - }; - } - } - /// END OF VML SUPPORT + // Loop over the point array map and replace unformatted values with sprintf formatting markup + each(series.pointArrayMap || ['y'], function (key) { + key = '{point.' + key; // without the closing bracket + if (valuePrefix || valueSuffix) { + pointFormat = pointFormat.replace(key + '}', valuePrefix + key + '}' + valueSuffix); + } + pointFormat = pointFormat.replace(key + '}', key + ':,.' + valueDecimals + 'f}'); + }); - if (clips[i]) { - clips[i].animate(clipAttr); - } else { - clips[i] = renderer.clipRect(clipAttr); + return format(pointFormat, { + point: this, + series: this.series + }); + }, - if (graph) { - series['zoneGraph' + i].clip(clips[i]); - } + /** + * Fire an event on the Point object. Must not be renamed to fireEvent, as this + * causes a name clash in MooTools + * @param {String} eventType + * @param {Object} eventArgs Additional event arguments + * @param {Function} defaultFunction Default event handler + */ + firePointEvent: function (eventType, eventArgs, defaultFunction) { + var point = this, + series = this.series, + seriesOptions = series.options; - if (area) { - series['zoneArea' + i].clip(clips[i]); - } - } - // if this zone extends out of the axis, ignore the others - ignoreZones = threshold.value > extremes.max; - }); - this.clips = clips; - } - }, + // load event handlers on demand to save time on mouseover/out + if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) { + this.importEvents(); + } - /** - * Initialize and perform group inversion on series.group and series.markerGroup - */ - invertGroups: function () { - var series = this, - chart = series.chart; + // add default handler if in selection mode + if (eventType === 'click' && seriesOptions.allowPointSelect) { + defaultFunction = function (event) { + // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera + if (point.select) { // Could be destroyed by prior event handlers (#2911) + point.select(null, event.ctrlKey || event.metaKey || event.shiftKey); + } + }; + } - // Pie, go away (#1736) - if (!series.xAxis) { - return; - } + fireEvent(this, eventType, eventArgs, defaultFunction); + }, + visible: true + };/** + * @classDescription 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 + * exceeds the cropThreshold, or if the data is grouped, series.data doesn't contain all the points. It + * only contains the points that have been created on demand. + * - 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. + * + * @param {Object} chart + * @param {Object} options + */ + var Series = Highcharts.Series = function () {}; - // A fixed size is needed for inversion to work - function setInvert() { - var size = { - width: series.yAxis.len, - height: series.xAxis.len - }; + Series.prototype = { - each(['group', 'markerGroup'], function (groupName) { - if (series[groupName]) { - series[groupName].attr(size).invert(); - } - }); - } + isCartesian: true, + type: 'line', + pointClass: Point, + sorted: true, // requires the data to be sorted + requireSorting: true, + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'lineColor', + 'stroke-width': 'lineWidth', + fill: 'fillColor', + r: 'radius' + }, + 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 + 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); + }; - addEvent(chart, 'resize', setInvert); // do it on resize - addEvent(series, 'destroy', function () { - removeEvent(chart, 'resize', setInvert); - }); + series.chart = chart; + series.options = options = series.setOptions(options); // merge with plotOptions + series.linkedSeries = []; - // Do it now - setInvert(); // do it now + // bind the axes + series.bindAxes(); - // On subsequent render and redraw, just do setInvert without setting up events again - series.invertGroups = setInvert; - }, + // set some variables + extend(series, { + name: options.name, + state: NORMAL_STATE, + pointAttr: {}, + visible: options.visible !== false, // true by default + selected: options.selected === true // false by default + }); - /** - * 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; + // special + if (useCanVG) { + options.animation = false; + } - // Generate it on first call - if (isNew) { - this[prop] = group = this.chart.renderer.g(name) - .attr({ - visibility: visibility, - zIndex: zIndex || 0.1 // IE8 needs this - }) - .add(parent); + // 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; + } - group.addClass('highcharts-series-' + this.index); - } - - // Place it on first and subsequent (redraw) calls - group[isNew ? 'attr' : 'animate'](this.getPlotBox()); - return group; - }, + series.getColor(); + series.getSymbol(); - /** - * Get the translation and scale for the plot area of this series - */ - getPlotBox: function () { - var chart = this.chart, - xAxis = this.xAxis, - yAxis = this.yAxis; + // Set the data + each(series.parallelArrays, function (key) { + series[key + 'Data'] = []; + }); + series.setData(options.data, false); - // 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 - }; - }, + // Mark cartesian + if (series.isCartesian) { + chart.hasCartesianSeries = true; + } - /** - * Render the graph and markers - */ - render: function () { - var series = this, - chart = series.chart, - group, - options = series.options, - animation = options.animation, - // Animation doesn't work in IE8 quirks when the group div is hidden, - // and looks bad in other oldIE - animDuration = (animation && !!series.animate && chart.renderer.isSVG && pick(animation.duration, 500)) || 0, - visibility = series.visible ? VISIBLE : HIDDEN, - zIndex = options.zIndex, - hasRendered = series.hasRendered, - chartSeriesGroup = chart.seriesGroup; + // Register it in the chart + chartSeries.push(series); + series._i = chartSeries.length - 1; - // the group - group = series.plotGroup( - 'group', - 'series', - visibility, - zIndex, - chartSeriesGroup - ); + // Sort series according to index option (#248, #1123, #2456) + stableSort(chartSeries, sortByIndex); + if (this.yAxis) { + stableSort(this.yAxis.series, sortByIndex); + } - series.markerGroup = series.plotGroup( - 'markerGroup', - 'markers', - visibility, - zIndex, - chartSeriesGroup - ); + each(chartSeries, function (series, i) { + series.index = i; + series.name = series.name || 'Series ' + (i + 1); + }); - // initiate the animation - if (animDuration) { - series.animate(true); - } + }, - // cache attributes for shapes - series.getAttribs(); + /** + * 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; - // SVGRenderer needs to know this before drawing elements (#1089, #1795) - group.inverted = series.isCartesian ? chart.inverted : false; + each(series.axisTypes || [], function (AXIS) { // repeat for xAxis and yAxis - // draw the graph if any - if (series.drawGraph) { - series.drawGraph(); - series.applyZones(); - } + each(chart[AXIS], function (axis) { // loop through the chart's axis objects + axisOptions = axis.options; - each(series.points, function (point) { - if (point.redraw) { - point.redraw(); - } - }); + // 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)) { - // draw the data labels (inn pies they go before the points) - if (series.drawDataLabels) { - series.drawDataLabels(); - } + // register this series in the axis.series lookup + axis.series.push(series); - // draw the points - if (series.visible) { - series.drawPoints(); - } + // set this series.xAxis or series.yAxis reference + series[AXIS] = axis; + // mark dirty for redraw + axis.isDirty = true; + } + }); - // draw the mouse tracking area - if (series.drawTracker && series.options.enableMouseTracking !== false) { - series.drawTracker(); - } + // The series needs an X and an Y axis + if (!series[AXIS] && series.optionalAxis !== AXIS) { + error(18, true); + } - // Handle inverted series and tracker groups - if (chart.inverted) { - series.invertGroups(); - } + }); + }, - // 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); - } + /** + * 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 = typeof i === 'number' ? + // 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)); + }; - // Run the animation - if (animDuration) { - series.animate(); - } + each(series.parallelArrays, fn); + }, - // Call the afterAnimate function on animation complete (but don't overwrite the animation.complete option - // which should be available to the user). - if (!hasRendered) { - if (animDuration) { - series.animationTimeout = setTimeout(function () { - series.afterAnimate(); - }, animDuration); - } else { - series.afterAnimate(); - } - } + /** + * 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 () { - 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; - }, + var options = this.options, + xIncrement = this.xIncrement, + date, + pointInterval, + pointIntervalUnit = options.pointIntervalUnit; - /** - * Redraw the series after an update in the axes. - */ - redraw: function () { - var series = this, - chart = series.chart, - wasDirtyData = series.isDirtyData, // cache it here as it is set to false in render, but used after - wasDirty = series.isDirty, - group = series.group, - xAxis = series.xAxis, - yAxis = series.yAxis; + xIncrement = pick(xIncrement, options.pointStart, 0); - // reposition on resize - if (group) { - if (chart.inverted) { - group.attr({ - width: chart.plotWidth, - height: chart.plotHeight - }); - } + this.pointInterval = pointInterval = pick(this.pointInterval, options.pointInterval, 1); - group.animate({ - translateX: pick(xAxis && xAxis.left, chart.plotLeft), - translateY: pick(yAxis && yAxis.top, chart.plotTop) - }); - } + // Added code for pointInterval strings + if (pointIntervalUnit === 'month' || pointIntervalUnit === 'year') { + date = new Date(xIncrement); + date = (pointIntervalUnit === 'month') ? + +date[setMonth](date[getMonth]() + pointInterval) : + +date[setFullYear](date[getFullYear]() + pointInterval); + pointInterval = date - xIncrement; + } - series.translate(); - series.render(); - if (wasDirtyData) { - fireEvent(series, 'updatedData'); - } - if (wasDirty || wasDirtyData) { // #3945 recalculate the kdtree when dirty - delete this.kdTree; // #3868 recalculate the kdtree with dirty data - } - }, + this.xIncrement = xIncrement + pointInterval; + return xIncrement; + }, - /** - * KD Tree && PointSearching Implementation - */ + /** + * Divide the series data into segments divided by null values. + */ + getSegments: function () { + var series = this, + lastNull = -1, + segments = [], + i, + points = series.points, + pointsLength = points.length; - kdDimensions: 1, - kdAxisArray: ['clientX', 'plotY'], + if (pointsLength) { // no action required for [] - searchPoint: function (e, compareX) { - var series = this, - xAxis = series.xAxis, - yAxis = series.yAxis, - inverted = series.chart.inverted; - - 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); - }, + // if connect nulls, just remove null points + if (series.options.connectNulls) { + i = pointsLength; + while (i--) { + if (points[i].y === null) { + points.splice(i, 1); + } + } + if (points.length) { + segments = [points]; + } - buildKDTree: function () { - var series = this, - dimensions = series.kdDimensions; + // else, split on null points + } else { + each(points, function (point, i) { + if (point.y === null) { + if (i > lastNull + 1) { + segments.push(points.slice(lastNull + 1, i)); + } + lastNull = i; + } else if (i === pointsLength - 1) { // last value + segments.push(points.slice(lastNull + 1, i + 1)); + } + }); + } + } - // Internal function - function _kdtree(points, depth, dimensions) { - var axis, median, length = points && points.length; + // register it + series.segments = segments; + }, - if (length) { + /** + * 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; - // alternate between the axis - axis = series.kdAxisArray[depth % dimensions]; + this.userOptions = itemOptions; - // sort point array - points.sort(function(a, b) { - return a[axis] - b[axis]; - }); - - 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) - }; - - } - } + // 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 + ); - // Start the recursive build process with a clone of the points array and null points filtered out (#3873) - function startRecursive() { - var points = grep(series.points || [], function (point) { // #4390 - return point.y !== null; - }); + // 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 + ); - series.kdTree = _kdtree(points, dimensions, dimensions); - } - delete series.kdTree; - - if (series.options.kdSync) { // For testing tooltips, don't build async - startRecursive(); - } else { - setTimeout(startRecursive); - } - }, + // Delete marker object if not allowed (#1125) + if (typeOptions.marker === null) { + delete options.marker; + } - searchKDTree: function (point, compareX) { - var series = this, - kdX = this.kdAxisArray[0], - kdY = this.kdAxisArray[1], - kdComparer = compareX ? 'distX' : 'dist'; + // 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, + color: options.negativeColor, + fillColor: options.negativeFillColor + }); + } + if (zones.length) { // Push one extra zone for the rest + if (defined(zones[zones.length - 1].value)) { + zones.push({ + color: this.color, + fillColor: this.fillColor + }); + } + } + return options; + }, - // 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); + getCyclic: function (prop, value, defaults) { + var i, + userOptions = this.userOptions, + indexName = '_' + prop + 'Index', + counterName = prop + 'Counter'; - p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE; - p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE; - } - function _search(search, tree, depth, dimensions) { - var point = tree.point, - axis = series.kdAxisArray[depth % dimensions], - tdist, - sideA, - sideB, - ret = point, - nPoint1, - nPoint2; - - setDistance(search, point); + if (!value) { + if (defined(userOptions[indexName])) { // after Series.update() + i = userOptions[indexName]; + } else { + userOptions[indexName] = i = this.chart[counterName] % defaults.length; + this.chart[counterName] += 1; + } + value = defaults[i]; + } + this[prop] = value; + }, - // Pick side based on distance to splitting point - tdist = search[axis] - point[axis]; - sideA = tdist < 0 ? 'left' : 'right'; - sideB = tdist < 0 ? 'right' : 'left'; + /** + * Get the series' color + */ + 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); + } + }, + /** + * Get the series' symbol + */ + getSymbol: function () { + var seriesMarkerOption = this.options.marker; - // End of tree - if (tree[sideA]) { - nPoint1 =_search(search, tree[sideA], depth + 1, dimensions); + this.getCyclic('symbol', seriesMarkerOption.symbol, this.chart.options.symbols); - 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); - } - } - - return ret; - } + // don't substract radius in image symbols (#604) + if (/^url/.test(this.symbol)) { + seriesMarkerOption.radius = 0; + } + }, - if (!this.kdTree) { - this.buildKDTree(); - } + drawLegendSymbol: LegendSymbolMixin.drawLineMarker, - if (this.kdTree) { - return _search(point, - this.kdTree, this.kdDimensions, this.kdDimensions); - } - } + /** + * 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, + hasCategories = xAxis && !!xAxis.categories, + i, + turboThreshold = options.turboThreshold, + pt, + xData = this.xData, + yData = this.yData, + pointArrayMap = series.pointArrayMap, + valueCount = pointArrayMap && pointArrayMap.length; -}; // end Series prototype + data = data || []; + dataLength = data.length; + redraw = pick(redraw, true); -/** - * The class for stack items - */ -function StackItem(axis, options, isNegative, x, stackOption) { - - var inverted = axis.chart.inverted; + // 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); + } + }); - this.axis = axis; + } else { - // Tells if the stack is negative - this.isNegative = isNegative; + // Reset properties + series.xIncrement = null; - // Save the options to be able to style the label - this.options = options; + series.colorCounter = 0; // for series with colorByPoint (#1547) - // Save the x value to be able to position the label later - this.x = x; + // Update parallel arrays + each(this.parallelArrays, function (key) { + series[key + 'Data'].length = 0; + }); - // Initialize total value - this.total = null; + // 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) { - // This will keep each points' extremes stored by series.index and point index - this.points = {}; + // find the first non-null point + i = 0; + while (firstPoint === null && i < dataLength) { + firstPoint = data[i]; + i++; + } - // Save the stack option on the series configuration object, and whether to treat it as percent - this.stack = stackOption; - // 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')), - y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)), - x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0) - }; + if (isNumber(firstPoint)) { // assume all points are numbers + var x = pick(options.pointStart, 0), + pointInterval = pick(options.pointInterval, 1); - this.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center'); -} + for (i = 0; i < dataLength; i++) { + xData[i] = x; + yData[i] = data[i]; + x += pointInterval; + } + series.xIncrement = x; + } 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 { + 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); + if (hasCategories && defined(pt.name)) { // #4401 + xAxis.names[pt.x] = pt.name; // #2046 + } + } + } + } -StackItem.prototype = { - destroy: function () { - destroyObjectProperties(this, this.axis); - }, + // Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON + if (isString(yData[0])) { + error(14, true); + } - /** - * Renders the stack total label and adds it to the stack label group. - */ - render: function (group) { - var options = this.options, - formatOption = options.format, - str = formatOption ? - format(formatOption, this) : - options.formatter.call(this); // format the text in the label + series.data = []; + series.options.data = data; + //series.zData = zData; - // 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 - .attr({ - align: this.textAlign, // fix the text-anchor - rotation: options.rotation, // rotation - visibility: HIDDEN // hidden until setOffset is called - }) - .add(group); // add to the labels-group - } - }, + // destroy old points + i = oldDataLength; + while (i--) { + if (oldData[i] && oldData[i].destroy) { + oldData[i].destroy(); + } + } - /** - * 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 - yZero = axis.translate(0), // stack origin - h = mathAbs(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), - 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 - - // Set visibility (#678) - alignAttr = label.alignAttr; - label[this.options.crop === false || chart.isInsidePlot(alignAttr.x, alignAttr.y) ? 'show' : 'hide'](true); - } - } -}; + // reset minRange (#878) + if (xAxis) { + xAxis.minRange = xAxis.userMinRange; + } -/** - * Generate stacks for each series and calculate stacks total values - */ -Chart.prototype.getStacks = function () { - var chart = this; + // redraw + series.isDirty = series.isDirtyData = chart.isDirtyBox = true; + animation = false; + } - // reset stacks for each yAxis - each(chart.yAxis, function (axis) { - if (axis.stacks && axis.hasVisibleSeries) { - axis.oldStacks = axis.stacks; - } - }); + // Typically for pie series, points need to be processed and generated + // prior to rendering the legend + if (options.legendType === 'point') { // docs: legendType now supported on more series types (at least column and pie) + this.processData(); + this.generatePoints(); + } - each(chart.series, function (series) { - if (series.options.stacking && (series.visible === true || chart.options.chart.ignoreHiddenSeries === false)) { - series.stackKey = series.type + pick(series.options.stack, ''); - } - }); -}; + if (redraw) { + chart.redraw(animation); + } + }, + /** + * 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, + min, + max; -// Stacking methods defined on the Axis prototype + // 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; + } -/** - * Build the stacks from top down - */ -Axis.prototype.buildStacks = function () { - var series = this.series, - reversedStacks = pick(this.options.reversedStacks, true), - i = series.length; - if (!this.isXAxis) { - this.usePercentage = false; - while (i--) { - series[reversedStacks ? i : series.length - i - 1].setStackedPoints(); - } - // Loop up again to compute percent stack - if (this.usePercentage) { - for (i = 0; i < series.length; i++) { - series[i].setPercentStacks(); - } - } - } -}; + if (xAxis) { + xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053) + min = xExtremes.min; + max = xExtremes.max; + } -Axis.prototype.renderStackTotals = function () { - var axis = this, - chart = axis.chart, - renderer = chart.renderer, - stacks = axis.stacks, - stackKey, - oneStack, - stackCategory, - stackTotalGroup = axis.stackTotalGroup; + // optionally filter out points outside the plot area + if (isCartesian && series.sorted && !getExtremesFromAll && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) { - // Create a separate group for the stack total labels - if (!stackTotalGroup) { - axis.stackTotalGroup = stackTotalGroup = - renderer.g('stack-labels') - .attr({ - visibility: VISIBLE, - zIndex: 6 - }) - .add(); - } + // it's outside current extremes + if (processedXData[dataLength - 1] < min || processedXData[0] > max) { + processedXData = []; + processedYData = []; - // 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); + // 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; + } + } - // Render each stack total - for (stackKey in stacks) { - oneStack = stacks[stackKey]; - for (stackCategory in oneStack) { - oneStack[stackCategory].render(stackTotalGroup); - } - } -}; -/** - * Set all the stacks to initial states and destroy unused ones. - */ -Axis.prototype.resetStacks = function () { - var stacks = this.stacks, - type, - i; - if (!this.isXAxis) { - for (type in stacks) { - for (i in stacks[type]) { + // Find the closest distance between processed points + for (i = processedXData.length - 1; i >= 0; i--) { + distance = processedXData[i] - processedXData[i - 1]; - // Clean up memory after point deletion (#1044, #4320) - if (stacks[type][i].touched < this.stacksTouched) { - stacks[type][i].destroy(); - delete stacks[type][i]; + if (distance > 0 && (closestPointRange === UNDEFINED || distance < closestPointRange)) { + closestPointRange = distance; - // Reset stacks - } else { - stacks[type][i].total = null; - stacks[type][i].cum = 0; - } - } - } - } -}; + // 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); + } + } -Axis.prototype.cleanStacks = function () { - var stacks, type, i; + // Record the properties + series.cropped = cropped; // undefined or true + series.cropStart = cropStart; + series.processedXData = processedXData; + series.processedYData = processedYData; - if (!this.isXAxis) { - if (this.oldStacks) { - stacks = this.stacks = this.oldStacks; - } + series.closestPointRange = closestPointRange; - // reset stacks - for (type in stacks) { - for (i in stacks[type]) { - stacks[type][i].cum = stacks[type][i].total; - } - } - } -}; + }, + /** + * 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; -// Stacking methods defnied for Series prototype + // iterate up to find slice start + for (i = 0; i < dataLength; i++) { + if (xData[i] >= min) { + cropStart = mathMax(0, i - cropShoulder); + break; + } + } -/** - * Adds series' points value to corresponding stack - */ -Series.prototype.setStackedPoints = function () { - if (!this.options.stacking || (this.visible !== true && this.chart.options.chart.ignoreHiddenSeries !== false)) { - return; - } + // proceed to find slice end + for (j = i; j < dataLength; j++) { + if (xData[j] > max) { + cropEnd = j + cropShoulder; + break; + } + } - var series = this, - xData = series.processedXData, - yData = series.processedYData, - stackedYData = [], - yDataLength = yData.length, - seriesOptions = series.options, - threshold = seriesOptions.threshold, - stackThreshold = seriesOptions.startFromThreshold ? threshold : 0, - stackOption = seriesOptions.stack, - stacking = seriesOptions.stacking, - stackKey = series.stackKey, - negKey = '-' + stackKey, - negStacks = series.negStacks, - yAxis = series.yAxis, - stacks = yAxis.stacks, - oldStacks = yAxis.oldStacks, - stackIndicator, - isNegative, - stack, - other, - key, - pointKey, - i, - x, - y; + return { + xData: xData.slice(cropStart, cropEnd), + yData: yData.slice(cropStart, cropEnd), + start: cropStart, + end: cropEnd + }; + }, - yAxis.stacksTouched += 1; + /** + * 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; - // 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); - 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) - isNegative = negStacks && y < (stackThreshold ? 0 : threshold); - key = isNegative ? negKey : stackKey; + if (!data && !hasGroupedData) { + var arr = []; + arr.length = dataOptions.length; + data = series.data = arr; + } - // Create empty object for this stack if it doesn't exist yet - if (!stacks[key]) { - stacks[key] = {}; - } + 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].index = cursor; // For faster access in Point.update + } - // Initialize StackItem for this x - 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); - } - } + // 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 + } + } + } - // If the StackItem doesn't exist, create it first - stack = stacks[key][x]; - //stack.points[pointKey] = [stack.cum || stackThreshold]; - stack.points[pointKey] = [pick(stack.cum, stackThreshold)]; - stack.touched = yAxis.stacksTouched; - + series.data = data; + series.points = points; + }, - // Add value to the stack total - if (stacking === 'percent') { + /** + * 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; - // 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 = mathMax(other.total, stack.total) + mathAbs(y) || 0; + yData = yData || this.stackedYData || this.processedYData; + yDataLength = yData.length; - // Percent stacked areas - } else { - stack.total = correctFloat(stack.total + (mathAbs(y) || 0)); - } - } else { - stack.total = correctFloat(stack.total + (y || 0)); - } + for (i = 0; i < yDataLength; i++) { - stack.cum = pick(stack.cum, stackThreshold) + (y || 0); + x = xData[i]; + y = yData[i]; - stack.points[pointKey].push(stack.cum); - stackedYData[i] = stack.cum; + // For points within the visible range, including the first point outside the + // visible range, consider y extremes + validValue = y !== null && y !== UNDEFINED && (!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 (stacking === 'percent') { - yAxis.usePercentage = true; - } + 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; + } + } + } + this.dataMin = arrayMin(activeYData); + this.dataMax = arrayMax(activeYData); + }, - this.stackedYData = stackedYData; // To be used in getExtremes + /** + * 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; - // Reset old stacks - yAxis.oldStacks = {}; -}; + // 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; -/** - * Iterate over all stacks and compute the absolute values to percent - */ -Series.prototype.setPercentStacks = function () { - var series = this, - stackKey = series.stackKey, - stacks = series.yAxis.stacks, - processedXData = series.processedXData, - stackIndicator; + // Discard disallowed y values for log axes (#3434) + if (yAxis.isLog && yValue !== null && yValue <= 0) { + point.y = yValue = null; + error(10); + } - each([stackKey, '-' + stackKey], function (key) { - var i = processedXData.length, - x, - stack, - pointExtremes, - totalFactor; + // Get the plotX translation + point.plotX = plotX = mathMin(mathMax(-1e5, xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement, this.type === 'flags')), 1e5); // #3923 - while (i--) { - x = processedXData[i]; - stackIndicator = series.getStackIndicator(stackIndicator, x, series.index); - 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 - series.stackedYData[i] = pointExtremes[1]; - } - } - }); -}; -/** -* 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) { - stackIndicator = { - x: x, - index: 0 - }; - } else { - stackIndicator.index++; - } - - stackIndicator.key = [index, x, stackIndicator.index].join(','); + // Calculate the bottom y value for stacked series + if (stacking && series.visible && stack && stack[xValue]) { + stackIndicator = series.getStackIndicator(stackIndicator, xValue, series.index); + pointStack = stack[xValue]; + stackValues = pointStack.points[stackIndicator.key]; + yBottom = stackValues[0]; + yValue = stackValues[1]; - return stackIndicator; -}; + if (yBottom === stackThreshold) { + yBottom = pick(threshold, yAxis.min); + } + if (yAxis.isLog && yBottom <= 0) { // #1200, #1232 + yBottom = null; + } -// Extend the Chart prototype for dynamic methods -extend(Chart.prototype, { + point.total = point.stackTotal = pointStack.total; + point.percentage = pointStack.total && (point.y / pointStack.total * 100); + point.stackY = yValue; - /** - * Add a series dynamically after time - * - * @param {Object} options The config options - * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true. - * @param {Boolean|Object} animation Whether to apply animation, and optionally animation - * configuration - * - * @return {Object} series The newly created series object - */ - addSeries: function (options, redraw, animation) { - var series, - chart = this; + // Place the stack label + pointStack.setOffset(series.pointXOffset || 0, series.barW || 0); - if (options) { - redraw = pick(redraw, true); // defaults to true + } - fireEvent(chart, 'addSeries', { options: options }, function () { - series = chart.initSeries(options); + // Set translated yBottom or remove it + point.yBottom = defined(yBottom) ? + yAxis.translate(yBottom, 0, 1, 0, 1) : + null; - chart.isDirtyLegend = true; // the series array is out of sync with the display - chart.linkSeries(); - if (redraw) { - chart.redraw(animation); - } - }); - } + // general hook, used for Highstock compare mode + if (hasModifyValue) { + yValue = series.modifyValue(yValue, point); + } - return series; - }, + // Set the the plotY value, reset it for redraws + point.plotY = plotY = (typeof yValue === 'number' && yValue !== Infinity) ? + mathMin(mathMax(-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; - /** - * Add an axis to the chart - * @param {Object} options The axis option - * @param {Boolean} isX Whether it is an X axis or a value axis + + // Set client related positions for mouse tracking + point.clientX = dynamicallyPlaced ? xAxis.translate(xValue, 0, 0, 0, 1) : plotX; // #1514 + + point.negative = point.y < (threshold || 0); + + // some API data + point.category = categories && categories[point.x] !== UNDEFINED ? + categories[point.x] : point.x; + + // Determine auto enabling of markers (#3635) + if (i) { + closestPointRangePx = mathMin(closestPointRangePx, mathAbs(plotX - lastPlotX)); + } + lastPlotX = plotX; + + } + + series.closestPointRangePx = closestPointRangePx; + + // now that we have the cropped data, build the segments + series.getSegments(); + }, + + /** + * 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']; + + // 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) { + 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); + + } + if (animation) { + clipRect.count += 1; + } + + if (options.clip !== false) { + this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect); + this.markerGroup.clip(markerClipRect); + this.sharedClipKey = sharedClipKey; + } + + // Remove the shared clipping rectangle when all series are shown + if (!animation) { + clipRect.count -= 1; + if (clipRect.count <= 0 && sharedClipKey && chart[sharedClipKey]) { + if (!seriesClipBox) { + chart[sharedClipKey] = chart[sharedClipKey].destroy(); + } + if (chart[sharedClipKey + 'm']) { + chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy(); + } + } + } + }, + + /** + * Animate in the series + */ + animate: function (init) { + var series = this, + chart = series.chart, + clipRect, + animation = series.options.animation, + sharedClipKey; + + // Animation option is set to true + if (animation && !isObject(animation)) { + animation = defaultPlotOptions[series.type].animation; + } + + // 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); + } + + // Delete this function to allow it only once + series.animate = null; + + } + }, + + /** + * This runs after animation to land on the final plot clipping + */ + afterAnimate: function () { + this.setClip(); + fireEvent(this, 'afterAnimate'); + }, + + /** + * Draw the markers + */ + drawPoints: function () { + var series = this, + pointAttr, + points = series.points, + chart = series.chart, + plotX, + plotY, + i, + point, + radius, + symbol, + isImage, + graphic, + options = series.options, + seriesMarkerOptions = options.marker, + seriesPointAttr = series.pointAttr[''], + pointMarkerOptions, + hasPointMarker, + enabled, + isInside, + markerGroup = series.markerGroup, + xAxis = series.xAxis, + globallyEnabled = pick( + seriesMarkerOptions.enabled, + xAxis.isRadial, + series.closestPointRangePx > 2 * seriesMarkerOptions.radius + ); + + if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) { + + i = points.length; + while (i--) { + point = points[i]; + plotX = mathFloor(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; + + // only draw the point if y is defined + if (enabled && plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) { + + // shortcuts + pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] || seriesPointAttr; + radius = pointAttr.r; + symbol = pick(pointMarkerOptions.symbol, series.symbol); + isImage = symbol.indexOf('url') === 0; + + if (graphic) { // update + graphic[isInside ? 'show' : 'hide'](true) // Since the marker group isn't clipped, each individual marker must be toggled + .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(pointAttr) + .add(markerGroup); + } + + } else if (graphic) { + point.graphic = graphic.destroy(); // #1269 + } + } + } + + }, + + /** + * Convert state properties from API naming conventions to SVG attributes + * + * @param {Object} options API options object + * @param {Object} base1 SVG attribute object to inherit from + * @param {Object} base2 Second level SVG attribute object to inherit from + */ + convertAttribs: function (options, base1, base2, base3) { + var conversion = this.pointAttrToOptions, + attr, + option, + obj = {}; + + options = options || {}; + base1 = base1 || {}; + base2 = base2 || {}; + base3 = base3 || {}; + + for (attr in conversion) { + option = conversion[attr]; + obj[attr] = pick(options[option], base1[attr], base2[attr], base3[attr]); + } + return obj; + }, + + /** + * Get the state attributes. Each series type has its own set of attributes + * that are allowed to change on a point's state change. Series wide attributes are stored for + * all series, and additionally point specific attributes are stored for all + * points with individual marker options. If such options are not defined for the point, + * a reference to the series wide attributes is stored in point.pointAttr. + */ + getAttribs: function () { + var series = this, + seriesOptions = series.options, + normalOptions = defaultPlotOptions[series.type].marker ? seriesOptions.marker : seriesOptions, + stateOptions = normalOptions.states, + stateOptionsHover = stateOptions[HOVER_STATE], + pointStateOptionsHover, + seriesColor = series.color, + seriesNegativeColor = series.options.negativeColor, + normalDefaults = { + stroke: seriesColor, + fill: seriesColor + }, + points = series.points || [], // #927 + i, + j, + threshold, + point, + seriesPointAttr = [], + pointAttr, + pointAttrToOptions = series.pointAttrToOptions, + hasPointSpecificOptions = series.hasPointSpecificOptions, + defaultLineColor = normalOptions.lineColor, + defaultFillColor = normalOptions.fillColor, + turboThreshold = seriesOptions.turboThreshold, + zones = series.zones, + zoneAxis = series.zoneAxis || 'y', + attr, + key; + + // series type specific modifications + if (seriesOptions.marker) { // line, spline, area, areaspline, scatter + + // if no hover radius is given, default to normal radius + 2 + stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + stateOptionsHover.radiusPlus; + stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + stateOptionsHover.lineWidthPlus; + + } else { // column, bar, pie + + // if no hover color is given, brighten the normal color + stateOptionsHover.color = stateOptionsHover.color || + Color(stateOptionsHover.color || seriesColor) + .brighten(stateOptionsHover.brightness).get(); + + // if no hover negativeColor is given, brighten the normal negativeColor + stateOptionsHover.negativeColor = stateOptionsHover.negativeColor || + Color(stateOptionsHover.negativeColor || seriesNegativeColor) + .brighten(stateOptionsHover.brightness).get(); + } + + // general point attributes for the series normal state + seriesPointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, normalDefaults); + + // HOVER_STATE and SELECT_STATE states inherit from normal state except the default radius + each([HOVER_STATE, SELECT_STATE], function (state) { + seriesPointAttr[state] = + series.convertAttribs(stateOptions[state], seriesPointAttr[NORMAL_STATE]); + }); + + // set it + series.pointAttr = seriesPointAttr; + + + // Generate the point-specific attribute collections if specific point + // options are given. If not, create a referance to the series wide point + // attributes + i = points.length; + if (!turboThreshold || i < turboThreshold || hasPointSpecificOptions) { + while (i--) { + point = points[i]; + normalOptions = (point.options && point.options.marker) || point.options; + if (normalOptions && normalOptions.enabled === false) { + normalOptions.radius = 0; + } + + if (zones.length) { + j = 0; + threshold = zones[j]; + while (point[zoneAxis] >= threshold.value) { + threshold = zones[++j]; + } + + point.color = point.fillColor = pick(threshold.color, series.color); // #3636, #4267, #4430 - inherit color from series, when color is undefined + + } + + hasPointSpecificOptions = seriesOptions.colorByPoint || point.color; // #868 + + // check if the point has specific visual options + if (point.options) { + for (key in pointAttrToOptions) { + if (defined(normalOptions[pointAttrToOptions[key]])) { + hasPointSpecificOptions = true; + } + } + } + + // a specific marker config object is defined for the individual point: + // create it's own attribute collection + if (hasPointSpecificOptions) { + normalOptions = normalOptions || {}; + pointAttr = []; + stateOptions = normalOptions.states || {}; // reassign for individual point + pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {}; + + // Handle colors for column and pies + if (!seriesOptions.marker || (point.negative && !pointStateOptionsHover.fillColor && !stateOptionsHover.fillColor)) { // column, bar, point or negative threshold for series with markers (#3636) + // If no hover color is given, brighten the normal color. #1619, #2579 + pointStateOptionsHover[series.pointAttrToOptions.fill] = pointStateOptionsHover.color || (!point.options.color && stateOptionsHover[(point.negative && seriesNegativeColor ? 'negativeColor' : 'color')]) || + Color(point.color) + .brighten(pointStateOptionsHover.brightness || stateOptionsHover.brightness) + .get(); + } + + // normal point state inherits series wide normal state + attr = { color: point.color }; // #868 + if (!defaultFillColor) { // Individual point color or negative color markers (#2219) + attr.fillColor = point.color; + } + if (!defaultLineColor) { + attr.lineColor = point.color; // Bubbles take point color, line markers use white + } + // Color is explicitly set to null or undefined (#1288, #4068) + if (normalOptions.hasOwnProperty('color') && !normalOptions.color) { + delete normalOptions.color; + } + pointAttr[NORMAL_STATE] = series.convertAttribs(extend(attr, normalOptions), seriesPointAttr[NORMAL_STATE]); + + // inherit from point normal and series hover + pointAttr[HOVER_STATE] = series.convertAttribs( + stateOptions[HOVER_STATE], + seriesPointAttr[HOVER_STATE], + pointAttr[NORMAL_STATE] + ); + + // inherit from point normal and series hover + pointAttr[SELECT_STATE] = series.convertAttribs( + stateOptions[SELECT_STATE], + seriesPointAttr[SELECT_STATE], + pointAttr[NORMAL_STATE] + ); + + + // no marker config object is created: copy a reference to the series-wide + // attribute collection + } else { + pointAttr = seriesPointAttr; + } + + point.pointAttr = pointAttr; + } + } + }, + + /** + * Clear DOM objects and free up memory + */ + destroy: function () { + var series = this, + chart = series.chart, + issue134 = /AppleWebKit\/533/.test(userAgent), + destroy, + i, + data = series.data || [], + point, + prop, + axis; + + // add event hook + fireEvent(series, 'destroy'); + + // remove all events + removeEvent(series); + + // erase from axes + each(series.axisTypes || [], function (AXIS) { + axis = series[AXIS]; + if (axis) { + erase(axis.series, series); + axis.isDirty = axis.forceRedraw = true; + } + }); + + // 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; + + // Clear the animation timeout if we are destroying the series during initial animation + clearTimeout(series.animationTimeout); + + // 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 + + // issue 134 workaround + destroy = issue134 && prop === 'group' ? + 'hide' : + 'destroy'; + + series[prop][destroy](); + } + } + + // remove from hoverSeries + if (chart.hoverSeries === series) { + chart.hoverSeries = null; + } + erase(chart.series, series); + + // clear all members + for (prop in series) { + delete series[prop]; + } + }, + + /** + * Return the graph path of a segment + */ + getSegmentPath: function (segment) { + var series = this, + segmentPath = [], + step = series.options.step; + + // build the segment line + each(segment, function (point, i) { + + var plotX = point.plotX, + plotY = point.plotY, + lastPoint; + + if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object + segmentPath.push.apply(segmentPath, series.getPointSpline(segment, point, i)); + + } else { + + // moveTo or lineTo + segmentPath.push(i ? L : M); + + // step line? + if (step && i) { + lastPoint = segment[i - 1]; + if (step === 'right') { + segmentPath.push( + lastPoint.plotX, + plotY, + L + ); + + } else if (step === 'center') { + segmentPath.push( + (lastPoint.plotX + plotX) / 2, + lastPoint.plotY, + L, + (lastPoint.plotX + plotX) / 2, + plotY, + L + ); + + } else { + segmentPath.push( + plotX, + lastPoint.plotY, + L + ); + } + } + + // normal line to next point + segmentPath.push( + point.plotX, + point.plotY + ); + } + }); + + return segmentPath; + }, + + /** + * Get the graph path + */ + getGraphPath: function () { + var series = this, + graphPath = [], + segmentPath, + singlePoints = []; // used in drawTracker + + // Divide into segments and build graph and area paths + each(series.segments, function (segment) { + + segmentPath = series.getSegmentPath(segment); + + // add the segment to the graph, or a single point for tracking + if (segment.length > 1) { + graphPath = graphPath.concat(segmentPath); + } else { + singlePoints.push(segment[0]); + } + }); + + // Record it for use in drawGraph and drawTracker, and return graphPath + series.singlePoints = singlePoints; + series.graphPath = graphPath; + + return graphPath; + + }, + + /** + * Draw the actual graph + */ + drawGraph: function () { + var series = this, + options = this.options, + props = [['graph', options.lineColor || this.color, options.dashStyle]], + lineWidth = options.lineWidth, + roundCap = options.linecap !== 'square', + graphPath = this.getGraphPath(), + fillColor = (this.fillGraph && this.color) || NONE, // polygon series use filled graph + zones = this.zones; + + each(zones, function (threshold, i) { + props.push(['zoneGraph' + i, threshold.color || series.color, threshold.dashStyle || options.dashStyle]); + }); + + // Draw the graph + each(props, function (prop, i) { + var graphKey = prop[0], + graph = series[graphKey], + attribs; + + if (graph) { + graph.animate({ d: graphPath }); + + } else if ((lineWidth || fillColor) && graphPath.length) { // #1487 + attribs = { + stroke: prop[1], + 'stroke-width': lineWidth, + fill: fillColor, + zIndex: 1 // #1069 + }; + if (prop[2]) { + attribs.dashstyle = prop[2]; + } else if (roundCap) { + attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round'; + } + + series[graphKey] = series.chart.renderer.path(graphPath) + .attr(attribs) + .add(series.group) + .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 = mathMax(chart.chartWidth, chart.chartHeight), + axis = this[(this.zoneAxis || 'y') + 'Axis'], + extremes, + reversed = axis.reversed, + inverted = chart.inverted, + horiz = axis.horiz, + pxRange, + pxPosMin, + pxPosMax, + ignoreZones = false; + + if (zones.length && (graph || area) && axis.min !== UNDEFINED) { + // 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(); + } + + // Create the clips + extremes = axis.getExtremes(); + each(zones, function (threshold, i) { + + translatedFrom = reversed ? + (horiz ? chart.plotWidth : 0) : + (horiz ? 0 : axis.toPixels(extremes.min)); + translatedFrom = mathMin(mathMax(pick(translatedTo, translatedFrom), 0), chartSizeMax); + translatedTo = mathMin(mathMax(mathRound(axis.toPixels(pick(threshold.value, extremes.max), true)), 0), chartSizeMax); + + if (ignoreZones) { + translatedFrom = translatedTo = axis.toPixels(extremes.max); + } + + pxRange = Math.abs(translatedFrom - translatedTo); + pxPosMin = mathMin(translatedFrom, translatedTo); + pxPosMax = mathMax(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; + } + } + + /// VML SUPPPORT + if (chart.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 + }; + } + } + /// END OF VML SUPPORT + + if (clips[i]) { + clips[i].animate(clipAttr); + } else { + clips[i] = renderer.clipRect(clipAttr); + + if (graph) { + series['zoneGraph' + i].clip(clips[i]); + } + + if (area) { + series['zoneArea' + i].clip(clips[i]); + } + } + // 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 () { + var series = this, + chart = series.chart; + + // Pie, go away (#1736) + if (!series.xAxis) { + return; + } + + // 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(); + } + }); + } + + addEvent(chart, 'resize', setInvert); // do it on resize + addEvent(series, 'destroy', function () { + removeEvent(chart, 'resize', setInvert); + }); + + // Do it now + setInvert(); // do it now + + // On subsequent render and redraw, just do setInvert without setting up events again + series.invertGroups = setInvert; + }, + + /** + * 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; + + // Generate it on first call + if (isNew) { + this[prop] = group = this.chart.renderer.g(name) + .attr({ + visibility: visibility, + zIndex: zIndex || 0.1 // IE8 needs this + }) + .add(parent); + + group.addClass('highcharts-series-' + this.index); + } + + // Place it on first and subsequent (redraw) calls + group[isNew ? 'attr' : 'animate'](this.getPlotBox()); + return group; + }, + + /** + * Get the translation and scale for the plot area of this series + */ + getPlotBox: function () { + var chart = this.chart, + xAxis = this.xAxis, + yAxis = this.yAxis; + + // 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 + }; + }, + + /** + * Render the graph and markers + */ + render: function () { + var series = this, + chart = series.chart, + group, + options = series.options, + animation = options.animation, + // Animation doesn't work in IE8 quirks when the group div is hidden, + // and looks bad in other oldIE + animDuration = (animation && !!series.animate && chart.renderer.isSVG && pick(animation.duration, 500)) || 0, + visibility = series.visible ? 'inherit' : 'hidden', // #2597 + zIndex = options.zIndex, + hasRendered = series.hasRendered, + chartSeriesGroup = chart.seriesGroup; + + // the group + group = series.plotGroup( + 'group', + 'series', + visibility, + zIndex, + chartSeriesGroup + ); + + series.markerGroup = series.plotGroup( + 'markerGroup', + 'markers', + visibility, + zIndex, + chartSeriesGroup + ); + + // initiate the animation + if (animDuration) { + series.animate(true); + } + + // cache attributes for shapes + series.getAttribs(); + + // SVGRenderer needs to know this before drawing elements (#1089, #1795) + group.inverted = series.isCartesian ? chart.inverted : false; + + // draw the graph if any + if (series.drawGraph) { + series.drawGraph(); + series.applyZones(); + } + + 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 points + if (series.visible) { + series.drawPoints(); + } + + + // draw the mouse tracking area + if (series.drawTracker && series.options.enableMouseTracking !== false) { + series.drawTracker(); + } + + // Handle inverted series and tracker groups + if (chart.inverted) { + series.invertGroups(); + } + + // 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); + } + + // Run the animation + if (animDuration) { + series.animate(); + } + + // 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); + } + + 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; + }, + + /** + * Redraw the series after an update in the axes. + */ + redraw: function () { + var series = this, + chart = series.chart, + wasDirtyData = series.isDirtyData, // cache it here as it is set to false in render, but used after + wasDirty = series.isDirty, + 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 + }); + } + + group.animate({ + translateX: pick(xAxis && xAxis.left, chart.plotLeft), + translateY: pick(yAxis && yAxis.top, chart.plotTop) + }); + } + + series.translate(); + series.render(); + if (wasDirtyData) { + fireEvent(series, 'updatedData'); + } + if (wasDirty || wasDirtyData) { // #3945 recalculate the kdtree when dirty + delete this.kdTree; // #3868 recalculate the kdtree with dirty data + } + }, + + /** + * KD Tree && PointSearching Implementation + */ + + kdDimensions: 1, + kdAxisArray: ['clientX', 'plotY'], + + searchPoint: function (e, compareX) { + var series = this, + xAxis = series.xAxis, + yAxis = series.yAxis, + inverted = series.chart.inverted; + + 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); + }, + + buildKDTree: function () { + var series = this, + dimensions = series.kdDimensions; + + // Internal function + function _kdtree(points, depth, dimensions) { + var axis, median, length = points && points.length; + + if (length) { + + // alternate between the axis + axis = series.kdAxisArray[depth % dimensions]; + + // sort point array + points.sort(function (a, b) { + return a[axis] - b[axis]; + }); + + 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() { + var points = grep(series.points || [], function (point) { // #4390 + return point.y !== null; + }); + + series.kdTree = _kdtree(points, dimensions, dimensions); + } + delete series.kdTree; + + // For testing tooltips, don't build async + syncTimeout(startRecursive, series.options.kdNow ? 0 : 1); + }, + + searchKDTree: function (point, compareX) { + var series = this, + kdX = this.kdAxisArray[0], + kdY = this.kdAxisArray[1], + kdComparer = compareX ? 'distX' : 'dist'; + + // 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); + + p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE; + p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE; + } + function _search(search, tree, depth, dimensions) { + var point = tree.point, + axis = series.kdAxisArray[depth % dimensions], + tdist, + sideA, + sideB, + ret = point, + nPoint1, + nPoint2; + + setDistance(search, point); + + // Pick side based on distance to splitting point + tdist = search[axis] - point[axis]; + sideA = tdist < 0 ? 'left' : 'right'; + sideB = tdist < 0 ? 'right' : 'left'; + + // End of tree + if (tree[sideA]) { + nPoint1 = _search(search, tree[sideA], depth + 1, dimensions); + + 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); + } + } + + return ret; + } + + if (!this.kdTree) { + this.buildKDTree(); + } + + if (this.kdTree) { + return _search(point, + this.kdTree, this.kdDimensions, this.kdDimensions); + } + } + + }; // end Series prototype + + /** + * The class for stack items */ - addAxis: function (options, isX, redraw, animation) { - var key = isX ? 'xAxis' : 'yAxis', - chartOptions = this.options, - axis; + function StackItem(axis, options, isNegative, x, stackOption) { - /*jslint unused: false*/ - axis = new Axis(this, merge(options, { - index: this[key].length, - isX: isX - })); - /*jslint unused: true*/ + var inverted = axis.chart.inverted; - // Push the new axis options to the chart options - chartOptions[key] = splat(chartOptions[key] || {}); - chartOptions[key].push(options); + this.axis = axis; - if (pick(redraw, true)) { - this.redraw(animation); - } - }, + // Tells if the stack is negative + this.isNegative = isNegative; - /** - * Dim the chart and show a loading text or symbol - * @param {String} str An optional text to show in the loading label instead of the default one - */ - showLoading: function (str) { - var chart = this, - options = chart.options, - loadingDiv = chart.loadingDiv, - loadingOptions = options.loading, - setLoadingSize = function () { - if (loadingDiv) { - css(loadingDiv, { - left: chart.plotLeft + PX, - top: chart.plotTop + PX, - width: chart.plotWidth + PX, - height: chart.plotHeight + PX - }); - } - }; + // Save the options to be able to style the label + this.options = options; - // create the layer at the first call - if (!loadingDiv) { - chart.loadingDiv = loadingDiv = createElement(DIV, { - className: PREFIX + 'loading' - }, extend(loadingOptions.style, { - zIndex: 10, - display: NONE - }), chart.container); + // Save the x value to be able to position the label later + this.x = x; - chart.loadingSpan = createElement( - 'span', - null, - loadingOptions.labelStyle, - loadingDiv - ); - addEvent(chart, 'redraw', setLoadingSize); // #1080 - } + // Initialize total value + this.total = null; - // update text - chart.loadingSpan.innerHTML = str || options.lang.loading; + // This will keep each points' extremes stored by series.index and point index + this.points = {}; - // show it - if (!chart.loadingShown) { - css(loadingDiv, { - opacity: 0, - display: '' - }); - animate(loadingDiv, { - opacity: loadingOptions.style.opacity - }, { - duration: loadingOptions.showDuration || 0 - }); - chart.loadingShown = true; - } - setLoadingSize(); - }, + // Save the stack option on the series configuration object, and whether to treat it as percent + this.stack = stackOption; - /** - * Hide the loading layer - */ - hideLoading: function () { - var options = this.options, - loadingDiv = this.loadingDiv; + // 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')), + y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)), + x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0) + }; - if (loadingDiv) { - animate(loadingDiv, { - opacity: 0 - }, { - duration: options.loading.hideDuration || 100, - complete: function () { - css(loadingDiv, { display: NONE }); - } - }); - } - this.loadingShown = false; - } -}); + this.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center'); + } -// extend the Point prototype for dynamic methods -extend(Point.prototype, { - /** - * Update the point 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 - * @param {Boolean|Object} animation Whether to apply animation, and optionally animation - * configuration - * - */ - update: function (options, redraw, animation, runEvent) { - var point = this, - series = point.series, - graphic = point.graphic, - i, - chart = series.chart, - seriesOptions = series.options, - names = series.xAxis && series.xAxis.names; + StackItem.prototype = { + destroy: function () { + destroyObjectProperties(this, this.axis); + }, - redraw = pick(redraw, true); + /** + * Renders the stack total label and adds it to the stack label group. + */ + render: function (group) { + var options = this.options, + formatOption = options.format, + str = formatOption ? + format(formatOption, this) : + options.formatter.call(this); // format the text in the label - function update() { + // 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 + .attr({ + align: this.textAlign, // fix the text-anchor + rotation: options.rotation, // rotation + visibility: HIDDEN // hidden until setOffset is called + }) + .add(group); // add to the labels-group + } + }, - point.applyOptions(options); + /** + * 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 + yZero = axis.translate(0), // stack origin + h = mathAbs(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), + width: inverted ? h : xWidth, + height: inverted ? xWidth : h + }, + label = this.label, + alignAttr; - // Update visuals - if (point.y === null && graphic) { // #4146 - point.graphic = graphic.destroy(); - } - if (isObject(options) && !isArray(options)) { - // Defer the actual redraw until getAttribs has been called (#3260) - point.redraw = function () { - if (graphic && graphic.element) { - if (options && options.marker && options.marker.symbol) { - point.graphic = graphic.destroy(); - } - } - if (options && options.dataLabels && point.dataLabel) { // #2468 - point.dataLabel = point.dataLabel.destroy(); - } - point.redraw = null; - }; - } + if (label) { + label.align(this.alignOptions, null, stackBox); // align the label to the box - // record changes in the parallel arrays - i = point.index; - series.updateParallelArrays(point, i); - if (names && point.name) { - names[point.x] = point.name; - } + // Set visibility (#678) + alignAttr = label.alignAttr; + label[this.options.crop === false || chart.isInsidePlot(alignAttr.x, alignAttr.y) ? 'show' : 'hide'](true); + } + } + }; - seriesOptions.data[i] = point.options; + /** + * Generate stacks for each series and calculate stacks total values + */ + Chart.prototype.getStacks = function () { + var chart = this; - // redraw - series.isDirty = series.isDirtyData = true; - if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320 - chart.isDirtyBox = true; - } + // reset stacks for each yAxis + each(chart.yAxis, function (axis) { + if (axis.stacks && axis.hasVisibleSeries) { + axis.oldStacks = axis.stacks; + } + }); - if (seriesOptions.legendType === 'point') { // #1831, #1885 - chart.isDirtyLegend = true; - } - if (redraw) { - chart.redraw(animation); - } - } + each(chart.series, function (series) { + if (series.options.stacking && (series.visible === true || chart.options.chart.ignoreHiddenSeries === false)) { + series.stackKey = series.type + pick(series.options.stack, ''); + } + }); + }; - // Fire the event with a default handler of doing the update - if (runEvent === false) { // When called from setData - update(); - } else { - point.firePointEvent('update', { options: options }, update); - } - }, - /** - * Remove a point and optionally redraw the series and if necessary the axes - * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call - * @param {Boolean|Object} animation Whether to apply animation, and optionally animation - * configuration - */ - remove: function (redraw, animation) { - this.series.removePoint(inArray(this, this.series.data), redraw, animation); - } -}); + // Stacking methods defined on the Axis prototype -// Extend the series prototype for dynamic methods -extend(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 - * configuration - */ - addPoint: function (options, redraw, shift, animation) { - var series = this, - seriesOptions = series.options, - data = series.data, - graph = series.graph, - area = series.area, - chart = series.chart, - names = series.xAxis && series.xAxis.names, - currentShift = (graph && graph.shift) || 0, - shiftShapes = ['graph', 'area'], - dataOptions = seriesOptions.data, - point, - isInTheMiddle, - xData = series.xData, - i, - x; + /** + * Build the stacks from top down + */ + Axis.prototype.buildStacks = function () { + var series = this.series, + reversedStacks = pick(this.options.reversedStacks, true), + i = series.length; + if (!this.isXAxis) { + this.usePercentage = false; + while (i--) { + series[reversedStacks ? i : series.length - i - 1].setStackedPoints(); + } + // Loop up again to compute percent stack + if (this.usePercentage) { + for (i = 0; i < series.length; i++) { + series[i].setPercentStacks(); + } + } + } + }; - setAnimation(animation, chart); + Axis.prototype.renderStackTotals = function () { + var axis = this, + chart = axis.chart, + renderer = chart.renderer, + stacks = axis.stacks, + stackKey, + oneStack, + stackCategory, + stackTotalGroup = axis.stackTotalGroup; - // Make graph animate sideways - if (shift) { - i = series.zones.length; - while (i--) { - shiftShapes.push('zoneGraph' + i, 'zoneArea' + i); - } - each(shiftShapes, function (shape) { - if (series[shape]) { - series[shape].shift = currentShift + (seriesOptions.step ? 2 : 1); - } - }); - } - if (area) { - area.isArea = true; // needed in animation, both with and without shift - } + // Create a separate group for the stack total labels + if (!stackTotalGroup) { + axis.stackTotalGroup = stackTotalGroup = + renderer.g('stack-labels') + .attr({ + visibility: VISIBLE, + zIndex: 6 + }) + .add(); + } - // Optional redraw, defaults to true - redraw = pick(redraw, true); + // 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); - // Get options and push the point to xData, yData and series.options. In series.generatePoints - // the Point instance will be created on demand and pushed to the series.data array. - point = { series: series }; - series.pointClass.prototype.applyOptions.apply(point, [options]); - x = point.x; + // Render each stack total + for (stackKey in stacks) { + oneStack = stacks[stackKey]; + for (stackCategory in oneStack) { + oneStack[stackCategory].render(stackTotalGroup); + } + } + }; - // Get the insertion point - i = xData.length; - if (series.requireSorting && x < xData[i - 1]) { - isInTheMiddle = true; - while (i && xData[i - 1] > x) { - i--; - } - } + /** + * Set all the stacks to initial states and destroy unused ones. + */ + Axis.prototype.resetStacks = function () { + var stacks = this.stacks, + type, + i; + if (!this.isXAxis) { + for (type in stacks) { + for (i in stacks[type]) { - series.updateParallelArrays(point, 'splice', i, 0, 0); // insert undefined item - series.updateParallelArrays(point, i); // update it + // Clean up memory after point deletion (#1044, #4320) + if (stacks[type][i].touched < this.stacksTouched) { + stacks[type][i].destroy(); + delete stacks[type][i]; - if (names && point.name) { - names[x] = point.name; - } - dataOptions.splice(i, 0, options); + // Reset stacks + } else { + stacks[type][i].total = null; + stacks[type][i].cum = 0; + } + } + } + } + }; - if (isInTheMiddle) { - series.data.splice(i, 0, null); - series.processData(); - } + Axis.prototype.cleanStacks = function () { + var stacks, type, i; - // Generate points to be added to the legend (#1329) - if (seriesOptions.legendType === 'point') { - series.generatePoints(); - } + if (!this.isXAxis) { + if (this.oldStacks) { + stacks = this.stacks = this.oldStacks; + } - // Shift the first point off the parallel arrays - // todo: consider series.removePoint(i) method - if (shift) { - if (data[0] && data[0].remove) { - data[0].remove(false); - } else { - data.shift(); - series.updateParallelArrays(point, 'shift'); + // reset stacks + for (type in stacks) { + for (i in stacks[type]) { + stacks[type][i].cum = stacks[type][i].total; + } + } + } + }; - dataOptions.shift(); - } - } - // redraw - series.isDirty = true; - series.isDirtyData = true; - if (redraw) { - series.getAttribs(); // #1937 - chart.redraw(); - } - }, + // Stacking methods defnied for Series prototype - /** - * Remove a point (rendered or not), by index - */ - removePoint: function (i, redraw, animation) { + /** + * Adds series' points value to corresponding stack + */ + Series.prototype.setStackedPoints = function () { + if (!this.options.stacking || (this.visible !== true && this.chart.options.chart.ignoreHiddenSeries !== false)) { + return; + } - var series = this, - data = series.data, - point = data[i], - points = series.points, - chart = series.chart, - remove = function () { + var series = this, + xData = series.processedXData, + yData = series.processedYData, + stackedYData = [], + yDataLength = yData.length, + seriesOptions = series.options, + threshold = seriesOptions.threshold, + stackThreshold = seriesOptions.startFromThreshold ? threshold : 0, + stackOption = seriesOptions.stack, + stacking = seriesOptions.stacking, + stackKey = series.stackKey, + negKey = '-' + stackKey, + negStacks = series.negStacks, + yAxis = series.yAxis, + stacks = yAxis.stacks, + oldStacks = yAxis.oldStacks, + stackIndicator, + isNegative, + stack, + other, + key, + pointKey, + i, + x, + y; - if (data.length === points.length) { - points.splice(i, 1); - } - data.splice(i, 1); - series.options.data.splice(i, 1); - series.updateParallelArrays(point || { series: series }, 'splice', i, 1); - if (point) { - point.destroy(); - } + yAxis.stacksTouched += 1; - // redraw - series.isDirty = true; - series.isDirtyData = true; - if (redraw) { - chart.redraw(); - } - }; + // 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); + 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) + isNegative = negStacks && y < (stackThreshold ? 0 : threshold); + key = isNegative ? negKey : stackKey; - setAnimation(animation, chart); - redraw = pick(redraw, true); + // Create empty object for this stack if it doesn't exist yet + if (!stacks[key]) { + stacks[key] = {}; + } - // Fire the event with a default handler of removing the point - if (point) { - point.firePointEvent('remove', null, remove); - } else { - remove(); - } - }, + // Initialize StackItem for this x + 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); + } + } - /** - * Remove a series and optionally redraw the chart - * - * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call - * @param {Boolean|Object} animation Whether to apply animation, and optionally animation - * configuration - */ + // If the StackItem doesn't exist, create it first + stack = stacks[key][x]; + stack.points[pointKey] = [pick(stack.cum, stackThreshold)]; + stack.touched = yAxis.stacksTouched; - remove: function (redraw, animation) { - var series = this, - chart = series.chart; - redraw = pick(redraw, true); + // 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]; + } - if (!series.isRemoving) { /* prevent triggering native event in jQuery - (calling the remove function from the remove event) */ - series.isRemoving = true; + // Add value to the stack total + if (stacking === 'percent') { - // fire the event with a default handler of removing the point - fireEvent(series, 'remove', null, function () { + // 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 = mathMax(other.total, stack.total) + mathAbs(y) || 0; + // Percent stacked areas + } else { + stack.total = correctFloat(stack.total + (mathAbs(y) || 0)); + } + } else { + stack.total = correctFloat(stack.total + (y || 0)); + } - // destroy elements - series.destroy(); + stack.cum = pick(stack.cum, stackThreshold) + (y || 0); + stack.points[pointKey].push(stack.cum); + stackedYData[i] = stack.cum; - // redraw - chart.isDirtyLegend = chart.isDirtyBox = true; - chart.linkSeries(); + } - if (redraw) { - chart.redraw(animation); - } - }); + if (stacking === 'percent') { + yAxis.usePercentage = true; + } - } - series.isRemoving = false; - }, + this.stackedYData = stackedYData; // To be used in getExtremes - /** - * Update the series with a new set of options - */ - update: function (newOptions, redraw) { - var series = this, - chart = this.chart, - // must use user options when changing type because this.options is merged - // in with type specific plotOptions - oldOptions = this.userOptions, - oldType = this.type, - proto = seriesTypes[oldType].prototype, - preserve = ['group', 'markerGroup', 'dataLabelsGroup'], - n; + // Reset old stacks + yAxis.oldStacks = {}; + }; - // If we're changing type or zIndex, create new groups (#3380, #3404) - if ((newOptions.type && newOptions.type !== oldType) || newOptions.zIndex !== undefined) { - preserve.length = 0; - } + /** + * Iterate over all stacks and compute the absolute values to percent + */ + Series.prototype.setPercentStacks = function () { + var series = this, + stackKey = series.stackKey, + stacks = series.yAxis.stacks, + processedXData = series.processedXData, + stackIndicator; - // Make sure groups are not destroyed (#3094) - each(preserve, function (prop) { - preserve[prop] = series[prop]; - delete series[prop]; - }); + each([stackKey, '-' + stackKey], function (key) { + var i = processedXData.length, + x, + stack, + pointExtremes, + totalFactor; - // Do the merge, with some forced options - newOptions = merge(oldOptions, { - animation: false, - index: this.index, - pointStart: this.xData[0] // when updating after addPoint - }, { data: this.options.data }, newOptions); + while (i--) { + x = processedXData[i]; + stackIndicator = series.getStackIndicator(stackIndicator, x, series.index); + 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 + series.stackedYData[i] = pointExtremes[1]; + } + } + }); + }; - // Destroy the series and delete all properties. Reinsert all methods - // and properties from the new type prototype (#2270, #3719) - this.remove(false); - for (n in proto) { - this[n] = UNDEFINED; - } - extend(this, seriesTypes[newOptions.type || oldType].prototype); + /** + * 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) { + stackIndicator = { + x: x, + index: 0 + }; + } else { + stackIndicator.index++; + } - // Re-register groups (#3094) - each(preserve, function (prop) { - series[prop] = preserve[prop]; - }); + stackIndicator.key = [index, x, stackIndicator.index].join(','); - this.init(chart, newOptions); - chart.linkSeries(); // Links are lost in this.remove (#3028) - if (pick(redraw, true)) { - chart.redraw(false); - } - } -}); + return stackIndicator; + }; -// Extend the Axis.prototype for dynamic methods -extend(Axis.prototype, { + // Extend the Chart prototype for dynamic methods + extend(Chart.prototype, { - /** - * Update the axis with a new options structure - */ - update: function (newOptions, redraw) { - var chart = this.chart; + /** + * Add a series dynamically after time + * + * @param {Object} options The config options + * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true. + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + * + * @return {Object} series The newly created series object + */ + addSeries: function (options, redraw, animation) { + var series, + chart = this; - newOptions = chart.options[this.coll][this.options.index] = merge(this.userOptions, newOptions); + if (options) { + redraw = pick(redraw, true); // defaults to true - this.destroy(true); - this._addedPlotLB = this.chart._labelPanes = UNDEFINED; // #1611, #2887, #4314 + fireEvent(chart, 'addSeries', { options: options }, function () { + series = chart.initSeries(options); - this.init(chart, extend(newOptions, { events: UNDEFINED })); + chart.isDirtyLegend = true; // the series array is out of sync with the display + chart.linkSeries(); + if (redraw) { + chart.redraw(animation); + } + }); + } - chart.isDirtyBox = true; - if (pick(redraw, true)) { - chart.redraw(); - } - }, + return series; + }, - /** - * Remove the axis from the chart + /** + * Add an axis to the chart + * @param {Object} options The axis option + * @param {Boolean} isX Whether it is an X axis or a value axis + */ + addAxis: function (options, isX, redraw, animation) { + var key = isX ? 'xAxis' : 'yAxis', + chartOptions = this.options; + + new Axis(this, merge(options, { // eslint-disable-line no-new + index: this[key].length, + isX: isX + })); + + // Push the new axis options to the chart options + chartOptions[key] = splat(chartOptions[key] || {}); + chartOptions[key].push(options); + + if (pick(redraw, true)) { + this.redraw(animation); + } + }, + + /** + * Dim the chart and show a loading text or symbol + * @param {String} str An optional text to show in the loading label instead of the default one + */ + showLoading: function (str) { + var chart = this, + options = chart.options, + loadingDiv = chart.loadingDiv, + loadingOptions = options.loading, + setLoadingSize = function () { + if (loadingDiv) { + css(loadingDiv, { + left: chart.plotLeft + PX, + top: chart.plotTop + PX, + width: chart.plotWidth + PX, + height: chart.plotHeight + PX + }); + } + }; + + // create the layer at the first call + if (!loadingDiv) { + chart.loadingDiv = loadingDiv = createElement(DIV, { + className: PREFIX + 'loading' + }, extend(loadingOptions.style, { + zIndex: 10, + display: NONE + }), chart.container); + + chart.loadingSpan = createElement( + 'span', + null, + loadingOptions.labelStyle, + loadingDiv + ); + addEvent(chart, 'redraw', setLoadingSize); // #1080 + } + + // update text + chart.loadingSpan.innerHTML = str || options.lang.loading; + + // show it + if (!chart.loadingShown) { + css(loadingDiv, { + opacity: 0, + display: '' + }); + animate(loadingDiv, { + opacity: loadingOptions.style.opacity + }, { + duration: loadingOptions.showDuration || 0 + }); + chart.loadingShown = true; + } + setLoadingSize(); + }, + + /** + * Hide the loading layer + */ + hideLoading: function () { + var options = this.options, + loadingDiv = this.loadingDiv; + + if (loadingDiv) { + animate(loadingDiv, { + opacity: 0 + }, { + duration: options.loading.hideDuration || 100, + complete: function () { + css(loadingDiv, { display: NONE }); + } + }); + } + this.loadingShown = false; + } + }); + + // extend the Point prototype for dynamic methods + extend(Point.prototype, { + /** + * Update the point 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 + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + * + */ + update: function (options, redraw, animation, runEvent) { + var point = this, + series = point.series, + graphic = point.graphic, + i, + chart = series.chart, + seriesOptions = series.options, + names = series.xAxis && series.xAxis.names; + + redraw = pick(redraw, true); + + function update() { + + point.applyOptions(options); + + // Update visuals + if (point.y === null && graphic) { // #4146 + point.graphic = graphic.destroy(); + } + if (isObject(options) && !isArray(options)) { + // Defer the actual redraw until getAttribs has been called (#3260) + point.redraw = function () { + if (graphic && graphic.element) { + if (options && options.marker && options.marker.symbol) { + point.graphic = graphic.destroy(); + } + } + if (options && options.dataLabels && point.dataLabel) { // #2468 + point.dataLabel = point.dataLabel.destroy(); + } + point.redraw = null; + }; + } + + // record changes in the parallel arrays + i = point.index; + series.updateParallelArrays(point, i); + if (names && point.name) { + names[point.x] = point.name; + } + + // Record the options to options.data. If there is an object from before, + // use point options, otherwise use raw options. (#4701) + seriesOptions.data[i] = isObject(seriesOptions.data[i]) ? point.options : options; + + // redraw + series.isDirty = series.isDirtyData = true; + if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320 + chart.isDirtyBox = true; + } + + if (seriesOptions.legendType === 'point') { // #1831, #1885 + chart.isDirtyLegend = true; + } + if (redraw) { + chart.redraw(animation); + } + } + + // Fire the event with a default handler of doing the update + if (runEvent === false) { // When called from setData + update(); + } else { + point.firePointEvent('update', { options: options }, update); + } + }, + + /** + * Remove a point and optionally redraw the series and if necessary the axes + * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + remove: function (redraw, animation) { + this.series.removePoint(inArray(this, this.series.data), redraw, animation); + } + }); + + // Extend the series prototype for dynamic methods + extend(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 + * configuration + */ + addPoint: function (options, redraw, shift, animation) { + var series = this, + seriesOptions = series.options, + data = series.data, + graph = series.graph, + area = series.area, + chart = series.chart, + names = series.xAxis && series.xAxis.names, + currentShift = (graph && graph.shift) || 0, + shiftShapes = ['graph', 'area'], + dataOptions = seriesOptions.data, + point, + isInTheMiddle, + xData = series.xData, + i, + x; + + setAnimation(animation, chart); + + // Make graph animate sideways + if (shift) { + i = series.zones.length; + while (i--) { + shiftShapes.push('zoneGraph' + i, 'zoneArea' + i); + } + each(shiftShapes, function (shape) { + if (series[shape]) { + series[shape].shift = currentShift + (seriesOptions.step ? 2 : 1); + } + }); + } + if (area) { + area.isArea = true; // needed in animation, both with and without shift + } + + // Optional redraw, defaults to true + redraw = pick(redraw, true); + + // Get options and push the point to xData, yData and series.options. In series.generatePoints + // the Point instance will be created on demand and pushed to the series.data array. + point = { series: series }; + series.pointClass.prototype.applyOptions.apply(point, [options]); + x = point.x; + + // Get the insertion point + i = xData.length; + if (series.requireSorting && x < xData[i - 1]) { + isInTheMiddle = true; + while (i && xData[i - 1] > x) { + i--; + } + } + + series.updateParallelArrays(point, 'splice', i, 0, 0); // insert undefined item + series.updateParallelArrays(point, i); // update it + + if (names && point.name) { + names[x] = point.name; + } + dataOptions.splice(i, 0, options); + + if (isInTheMiddle) { + series.data.splice(i, 0, null); + series.processData(); + } + + // Generate points to be added to the legend (#1329) + if (seriesOptions.legendType === 'point') { + series.generatePoints(); + } + + // Shift the first point off the parallel arrays + if (shift) { + if (data[0] && data[0].remove) { + data[0].remove(false); + } else { + data.shift(); + series.updateParallelArrays(point, 'shift'); + + dataOptions.shift(); + } + } + + // redraw + series.isDirty = true; + series.isDirtyData = true; + if (redraw) { + series.getAttribs(); // #1937 + chart.redraw(); + } + }, + + /** + * Remove a point (rendered or not), by index + */ + removePoint: function (i, redraw, animation) { + + var series = this, + data = series.data, + point = data[i], + points = series.points, + chart = series.chart, + remove = function () { + + if (data.length === points.length) { + points.splice(i, 1); + } + data.splice(i, 1); + series.options.data.splice(i, 1); + series.updateParallelArrays(point || { series: series }, 'splice', i, 1); + + if (point) { + point.destroy(); + } + + // redraw + series.isDirty = true; + series.isDirtyData = true; + if (redraw) { + chart.redraw(); + } + }; + + setAnimation(animation, chart); + redraw = pick(redraw, true); + + // Fire the event with a default handler of removing the point + if (point) { + point.firePointEvent('remove', null, remove); + } else { + remove(); + } + }, + + /** + * Remove a series and optionally redraw the chart + * + * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + + remove: function (redraw, animation) { + var series = this, + chart = series.chart; + redraw = pick(redraw, true); + + if (!series.isRemoving) { /* prevent triggering native event in jQuery + (calling the remove function from the remove event) */ + series.isRemoving = true; + + // fire the event with a default handler of removing the point + fireEvent(series, 'remove', null, function () { + + + // destroy elements + series.destroy(); + + + // redraw + chart.isDirtyLegend = chart.isDirtyBox = true; + chart.linkSeries(); + + if (redraw) { + chart.redraw(animation); + } + }); + + } + series.isRemoving = false; + }, + + /** + * Update the series with a new set of options + */ + update: function (newOptions, redraw) { + var series = this, + chart = this.chart, + // must use user options when changing type because this.options is merged + // in with type specific plotOptions + oldOptions = this.userOptions, + oldType = this.type, + proto = seriesTypes[oldType].prototype, + preserve = ['group', 'markerGroup', 'dataLabelsGroup'], + n; + + // If we're changing type or zIndex, create new groups (#3380, #3404) + if ((newOptions.type && newOptions.type !== oldType) || newOptions.zIndex !== undefined) { + preserve.length = 0; + } + + // Make sure groups are not destroyed (#3094) + each(preserve, function (prop) { + preserve[prop] = series[prop]; + delete series[prop]; + }); + + // Do the merge, with some forced options + newOptions = merge(oldOptions, { + animation: false, + index: this.index, + pointStart: this.xData[0] // when updating after addPoint + }, { data: this.options.data }, newOptions); + + // Destroy the series and delete all properties. Reinsert all methods + // and properties from the new type prototype (#2270, #3719) + this.remove(false); + for (n in proto) { + this[n] = UNDEFINED; + } + extend(this, seriesTypes[newOptions.type || oldType].prototype); + + // Re-register groups (#3094) + each(preserve, function (prop) { + series[prop] = preserve[prop]; + }); + + this.init(chart, newOptions); + chart.linkSeries(); // Links are lost in this.remove (#3028) + if (pick(redraw, true)) { + chart.redraw(false); + } + } + }); + + // Extend the Axis.prototype for dynamic methods + extend(Axis.prototype, { + + /** + * Update the axis with a new options structure + */ + update: function (newOptions, redraw) { + var chart = this.chart; + + newOptions = chart.options[this.coll][this.options.index] = merge(this.userOptions, newOptions); + + this.destroy(true); + this._addedPlotLB = this.chart._labelPanes = UNDEFINED; // #1611, #2887, #4314 + + this.init(chart, extend(newOptions, { events: UNDEFINED })); + + chart.isDirtyBox = true; + if (pick(redraw, true)) { + chart.redraw(); + } + }, + + /** + * Remove the axis from the chart + */ + remove: function (redraw) { + var chart = this.chart, + key = this.coll, // xAxis or yAxis + axisSeries = this.series, + i = axisSeries.length; + + // Remove associated series (#2687) + while (i--) { + if (axisSeries[i]) { + axisSeries[i].remove(false); + } + } + + // Remove the axis + erase(chart.axes, this); + erase(chart[key], this); + chart.options[key].splice(this.options.index, 1); + each(chart[key], function (axis, i) { // Re-index, #1706 + axis.options.index = i; + }); + this.destroy(); + chart.isDirtyBox = true; + + if (pick(redraw, true)) { + chart.redraw(); + } + }, + + /** + * Update the axis title by options + */ + setTitle: function (newTitleOptions, redraw) { + this.update({ title: newTitleOptions }, redraw); + }, + + /** + * Set new axis categories and optionally redraw + * @param {Array} categories + * @param {Boolean} redraw + */ + setCategories: function (categories, redraw) { + this.update({ categories: categories }, redraw); + } + + }); + + + /** + * LineSeries object */ - remove: function (redraw) { - var chart = this.chart, - key = this.coll, // xAxis or yAxis - axisSeries = this.series, - i = axisSeries.length; + var LineSeries = extendClass(Series); + seriesTypes.line = LineSeries; - // Remove associated series (#2687) - while (i--) { - if (axisSeries[i]) { - axisSeries[i].remove(false); - } - } + /** + * Set the default options for area + */ + defaultPlotOptions.area = merge(defaultSeriesOptions, { + softThreshold: false, + threshold: 0 + // trackByArea: false, + // lineColor: null, // overrides color, but lets fillColor be unaltered + // fillOpacity: 0.75, + // fillColor: null + }); - // Remove the axis - erase(chart.axes, this); - erase(chart[key], this); - chart.options[key].splice(this.options.index, 1); - each(chart[key], function (axis, i) { // Re-index, #1706 - axis.options.index = i; - }); - this.destroy(); - chart.isDirtyBox = true; + /** + * AreaSeries object + */ + var AreaSeries = extendClass(Series, { + type: 'area', + singleStacks: false, + /** + * For stacks, don't split segments on null values. Instead, draw null values with + * no marker. Also insert dummy points for any X position that exists in other series + * in the stack. + */ + getSegments: function () { + var series = this, + segments = [], + segment = [], + keys = [], + xAxis = this.xAxis, + yAxis = this.yAxis, + stack = yAxis.stacks[this.stackKey], + pointMap = {}, + plotX, + plotY, + points = this.points, + connectNulls = this.options.connectNulls, + stackIndicator, + i, + x; - if (pick(redraw, true)) { - chart.redraw(); - } - }, + if (this.options.stacking && !this.cropped) { // cropped causes artefacts in Stock, and perf issue + // Create a map where we can quickly look up the points by their X value. + for (i = 0; i < points.length; i++) { + pointMap[points[i].x] = points[i]; + } - /** - * Update the axis title by options - */ - setTitle: function (newTitleOptions, redraw) { - this.update({ title: newTitleOptions }, redraw); - }, + // Sort the keys (#1651) + for (x in stack) { + if (stack[x].total !== null) { // nulled after switching between grouping and not (#1651, #2336) + keys.push(+x); + } + } + keys.sort(function (a, b) { + return a - b; + }); - /** - * Set new axis categories and optionally redraw - * @param {Array} categories - * @param {Boolean} redraw - */ - setCategories: function (categories, redraw) { - this.update({ categories: categories }, redraw); - } + each(keys, function (x) { + var threshold = null, + stackPoint, + skip = connectNulls && (!pointMap[x] || pointMap[x].y === null); // #1836 -}); + if (!skip) { + // The point exists, push it to the segment + if (pointMap[x]) { + segment.push(pointMap[x]); -/** - * LineSeries object - */ -var LineSeries = extendClass(Series); -seriesTypes.line = LineSeries; + // There is no point for this X value in this series, so we + // insert a dummy point in order for the areas to be drawn + // correctly. + } else { -/** - * Set the default options for area - */ -defaultPlotOptions.area = merge(defaultSeriesOptions, { - softThreshold: false, - threshold: 0 - // trackByArea: false, - // lineColor: null, // overrides color, but lets fillColor be unaltered - // fillOpacity: 0.75, - // fillColor: null -}); + // Loop down the stack to find the series below this one that has + // a value (#1991) + for (i = series.index; i <= yAxis.series.length; i++) { + stackIndicator = series.getStackIndicator(null, x, i); + stackPoint = stack[x].points[stackIndicator.key]; + if (stackPoint) { + threshold = stackPoint[1]; + break; + } + } -/** - * AreaSeries object - */ -var AreaSeries = extendClass(Series, { - type: 'area', - /** - * For stacks, don't split segments on null values. Instead, draw null values with - * no marker. Also insert dummy points for any X position that exists in other series - * in the stack. - */ - getSegments: function () { - var series = this, - segments = [], - segment = [], - keys = [], - xAxis = this.xAxis, - yAxis = this.yAxis, - stack = yAxis.stacks[this.stackKey], - pointMap = {}, - plotX, - plotY, - points = this.points, - connectNulls = this.options.connectNulls, - stackIndicator, - i, - x; + plotX = xAxis.translate(x); + plotY = yAxis.getThreshold(threshold); + segment.push({ + y: null, + plotX: plotX, + clientX: plotX, + plotY: plotY, + yBottom: plotY, + onMouseOver: noop + }); + } + } + }); - if (this.options.stacking && !this.cropped) { // cropped causes artefacts in Stock, and perf issue - // Create a map where we can quickly look up the points by their X value. - for (i = 0; i < points.length; i++) { - pointMap[points[i].x] = points[i]; - } + if (segment.length) { + segments.push(segment); + } - // Sort the keys (#1651) - for (x in stack) { - if (stack[x].total !== null) { // nulled after switching between grouping and not (#1651, #2336) - keys.push(+x); - } - } - keys.sort(function (a, b) { - return a - b; - }); + } else { + Series.prototype.getSegments.call(this); + segments = this.segments; + } - each(keys, function (x) { - var threshold = null, - stackPoint; + this.segments = segments; + }, - if (connectNulls && (!pointMap[x] || pointMap[x].y === null)) { // #1836 - return; + /** + * Extend the base Series getSegmentPath method by adding the path for the area. + * This path is pushed to the series.areaPath property. + */ + getSegmentPath: function (segment) { - // The point exists, push it to the segment - } else if (pointMap[x]) { - segment.push(pointMap[x]); + var segmentPath = Series.prototype.getSegmentPath.call(this, segment), // call base method + areaSegmentPath = [].concat(segmentPath), // work on a copy for the area path + i, + options = this.options, + segLength = segmentPath.length, + translatedThreshold = this.yAxis.getThreshold(options.threshold), // #2181 + yBottom; - // There is no point for this X value in this series, so we - // insert a dummy point in order for the areas to be drawn - // correctly. - } else { + if (segLength === 3) { // for animation from 1 to two points + areaSegmentPath.push(L, segmentPath[1], segmentPath[2]); + } + if (options.stacking && !this.closedStacks) { - // Loop down the stack to find the series below this one that has - // a value (#1991) - for (i = series.index; i <= yAxis.series.length; i++) { - stackIndicator = series.getStackIndicator(null, x, i); - stackPoint = stack[x].points[stackIndicator.key]; - if (stackPoint) { - threshold = stackPoint[1]; - break; - } - } + // Follow stack back. Later, implement areaspline. A general solution could be to + // reverse the entire graphPath of the previous series, though may be hard with + // splines and with series with different extremes + for (i = segment.length - 1; i >= 0; i--) { - plotX = xAxis.translate(x); - plotY = yAxis.getThreshold(threshold); - segment.push({ - y: null, - plotX: plotX, - clientX: plotX, - plotY: plotY, - yBottom: plotY, - onMouseOver: noop - }); - } - }); + yBottom = pick(segment[i].yBottom, translatedThreshold); - if (segment.length) { - segments.push(segment); - } + // step line? + if (i < segment.length - 1 && options.step) { + areaSegmentPath.push(segment[i + 1].plotX, yBottom); + } - } else { - Series.prototype.getSegments.call(this); - segments = this.segments; - } + areaSegmentPath.push(segment[i].plotX, yBottom); + } - this.segments = segments; - }, - - /** - * Extend the base Series getSegmentPath method by adding the path for the area. - * This path is pushed to the series.areaPath property. - */ - getSegmentPath: function (segment) { - - var segmentPath = Series.prototype.getSegmentPath.call(this, segment), // call base method - areaSegmentPath = [].concat(segmentPath), // work on a copy for the area path - i, - options = this.options, - segLength = segmentPath.length, - translatedThreshold = this.yAxis.getThreshold(options.threshold), // #2181 - yBottom; - - if (segLength === 3) { // for animation from 1 to two points - areaSegmentPath.push(L, segmentPath[1], segmentPath[2]); - } - if (options.stacking && !this.closedStacks) { - - // Follow stack back. Todo: implement areaspline. A general solution could be to - // reverse the entire graphPath of the previous series, though may be hard with - // splines and with series with different extremes - for (i = segment.length - 1; i >= 0; i--) { + } else { // follow zero line back + this.closeSegment(areaSegmentPath, segment, translatedThreshold); + } + this.areaPath = this.areaPath.concat(areaSegmentPath); + return segmentPath; + }, - yBottom = pick(segment[i].yBottom, translatedThreshold); - - // step line? - if (i < segment.length - 1 && options.step) { - areaSegmentPath.push(segment[i + 1].plotX, yBottom); - } - - areaSegmentPath.push(segment[i].plotX, yBottom); - } + /** + * Extendable method to close the segment path of an area. This is overridden in polar + * charts. + */ + closeSegment: function (path, segment, translatedThreshold) { + path.push( + L, + segment[segment.length - 1].plotX, + translatedThreshold, + L, + segment[0].plotX, + translatedThreshold + ); + }, - } else { // follow zero line back - this.closeSegment(areaSegmentPath, segment, translatedThreshold); - } - this.areaPath = this.areaPath.concat(areaSegmentPath); - return segmentPath; - }, - - /** - * Extendable method to close the segment path of an area. This is overridden in polar - * charts. - */ - closeSegment: function (path, segment, translatedThreshold) { - path.push( - L, - segment[segment.length - 1].plotX, - translatedThreshold, - L, - segment[0].plotX, - translatedThreshold - ); - }, - - /** - * Draw the graph and the underlying area. This method calls the Series base - * function and adds the area. The areaPath is calculated in the getSegmentPath - * method called from Series.prototype.drawGraph. - */ - drawGraph: function () { - - // Define or reset areaPath - this.areaPath = []; - - // Call the base method - Series.prototype.drawGraph.apply(this); - - // Define local variables - var series = this, - areaPath = this.areaPath, - options = this.options, - zones = this.zones, - props = [['area', this.color, options.fillColor]]; // area name, main color, fill color - - each(zones, function (threshold, i) { - props.push(['zoneArea' + i, threshold.color || series.color, threshold.fillColor || options.fillColor]); - }); - each(props, function (prop) { - var areaKey = prop[0], - area = series[areaKey]; - - // Create or update the area - if (area) { // update - area.animate({ d: areaPath }); - - } else { // create - series[areaKey] = series.chart.renderer.path(areaPath) - .attr({ - fill: pick( - prop[2], - Color(prop[1]).setOpacity(pick(options.fillOpacity, 0.75)).get() - ), - zIndex: 0 // #1069 - }).add(series.group); - } - }); - }, + /** + * Draw the graph and the underlying area. This method calls the Series base + * function and adds the area. The areaPath is calculated in the getSegmentPath + * method called from Series.prototype.drawGraph. + */ + drawGraph: function () { - drawLegendSymbol: LegendSymbolMixin.drawRectangle -}); + // Define or reset areaPath + this.areaPath = []; -seriesTypes.area = AreaSeries; -/** - * Set the default options for spline - */ -defaultPlotOptions.spline = merge(defaultSeriesOptions); + // Call the base method + Series.prototype.drawGraph.apply(this); -/** - * SplineSeries object - */ -var SplineSeries = extendClass(Series, { - type: 'spline', + // Define local variables + var series = this, + areaPath = this.areaPath, + options = this.options, + zones = this.zones, + props = [['area', this.color, options.fillColor]]; // area name, main color, fill color - /** - * Get the spline segment from a given point's previous neighbour to the given point - */ - getPointSpline: function (segment, 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 - denom = smoothing + 1, - plotX = point.plotX, - plotY = point.plotY, - lastPoint = segment[i - 1], - nextPoint = segment[i + 1], - leftContX, - leftContY, - rightContX, - rightContY, - ret; + each(zones, function (threshold, i) { + props.push(['zoneArea' + i, threshold.color || series.color, threshold.fillColor || options.fillColor]); + }); + each(props, function (prop) { + var areaKey = prop[0], + area = series[areaKey]; - // find control points - if (lastPoint && nextPoint) { - - var lastX = lastPoint.plotX, - lastY = lastPoint.plotY, - nextX = nextPoint.plotX, - nextY = nextPoint.plotY, - correction; + // Create or update the area + if (area) { // update + area.animate({ d: areaPath }); - leftContX = (smoothing * plotX + lastX) / denom; - leftContY = (smoothing * plotY + lastY) / denom; - rightContX = (smoothing * plotX + nextX) / denom; - rightContY = (smoothing * plotY + nextY) / denom; + } else { // create + series[areaKey] = series.chart.renderer.path(areaPath) + .attr({ + fill: pick( + prop[2], + Color(prop[1]).setOpacity(pick(options.fillOpacity, 0.75)).get() + ), + zIndex: 0 // #1069 + }).add(series.group); + } + }); + }, - // have the two control points make a straight line through main point - correction = ((rightContY - leftContY) * (rightContX - plotX)) / - (rightContX - leftContX) + plotY - rightContY; + drawLegendSymbol: LegendSymbolMixin.drawRectangle + }); - leftContY += correction; - rightContY += correction; + seriesTypes.area = AreaSeries; + /** + * Set the default options for spline + */ + defaultPlotOptions.spline = merge(defaultSeriesOptions); - // to prevent false extremes, check that control points are between - // neighbouring points' y values - if (leftContY > lastY && leftContY > plotY) { - leftContY = mathMax(lastY, plotY); - rightContY = 2 * plotY - leftContY; // mirror of left control point - } else if (leftContY < lastY && leftContY < plotY) { - leftContY = mathMin(lastY, plotY); - rightContY = 2 * plotY - leftContY; - } - if (rightContY > nextY && rightContY > plotY) { - rightContY = mathMax(nextY, plotY); - leftContY = 2 * plotY - rightContY; - } else if (rightContY < nextY && rightContY < plotY) { - rightContY = mathMin(nextY, plotY); - leftContY = 2 * plotY - rightContY; - } + /** + * SplineSeries object + */ + var SplineSeries = extendClass(Series, { + type: 'spline', - // record for drawing in next point - point.rightContX = rightContX; - point.rightContY = rightContY; + /** + * Get the spline segment from a given point's previous neighbour to the given point + */ + getPointSpline: function (segment, 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 + denom = smoothing + 1, + plotX = point.plotX, + plotY = point.plotY, + lastPoint = segment[i - 1], + nextPoint = segment[i + 1], + leftContX, + leftContY, + rightContX, + rightContY, + ret; - } - - // Visualize control points for debugging - /* - if (leftContX) { - this.chart.renderer.circle(leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop, 2) - .attr({ - stroke: 'red', - 'stroke-width': 1, - fill: 'none' - }) - .add(); - this.chart.renderer.path(['M', leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop, - 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop]) - .attr({ - stroke: 'red', - 'stroke-width': 1 - }) - .add(); - this.chart.renderer.circle(rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop, 2) - .attr({ - stroke: 'green', - 'stroke-width': 1, - fill: 'none' - }) - .add(); - this.chart.renderer.path(['M', rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop, - 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop]) - .attr({ - stroke: 'green', - 'stroke-width': 1 - }) - .add(); - } - */ + // find control points + if (lastPoint && nextPoint) { - // moveTo or lineTo - if (!i) { - ret = [M, plotX, plotY]; - } else { // curve from last point to this - ret = [ - 'C', - lastPoint.rightContX || lastPoint.plotX, - lastPoint.rightContY || lastPoint.plotY, - leftContX || plotX, - leftContY || plotY, - plotX, - plotY - ]; - lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later - } - return ret; - } -}); -seriesTypes.spline = SplineSeries; + var lastX = lastPoint.plotX, + lastY = lastPoint.plotY, + nextX = nextPoint.plotX, + nextY = nextPoint.plotY, + correction; -/** - * Set the default options for areaspline - */ -defaultPlotOptions.areaspline = merge(defaultPlotOptions.area); + leftContX = (smoothing * plotX + lastX) / denom; + leftContY = (smoothing * plotY + lastY) / denom; + rightContX = (smoothing * plotX + nextX) / denom; + rightContY = (smoothing * plotY + nextY) / denom; -/** - * AreaSplineSeries object - */ -var areaProto = AreaSeries.prototype, - AreaSplineSeries = extendClass(SplineSeries, { - type: 'areaspline', - closedStacks: true, // instead of following the previous graph back, follow the threshold back - - // Mix in methods from the area series - getSegmentPath: areaProto.getSegmentPath, - closeSegment: areaProto.closeSegment, - drawGraph: areaProto.drawGraph, - drawLegendSymbol: LegendSymbolMixin.drawRectangle - }); + // have the two control points make a straight line through main point + correction = ((rightContY - leftContY) * (rightContX - plotX)) / + (rightContX - leftContX) + plotY - rightContY; -seriesTypes.areaspline = AreaSplineSeries; + leftContY += correction; + rightContY += correction; -/** - * Set the default options for column - */ -defaultPlotOptions.column = merge(defaultSeriesOptions, { - borderColor: '#FFFFFF', - //borderWidth: 1, - borderRadius: 0, - //colorByPoint: undefined, - groupPadding: 0.2, - //grouping: true, - marker: null, // point options are specified in the base options - pointPadding: 0.1, - //pointWidth: null, - minPointLength: 0, - cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes - pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories - states: { - hover: { - brightness: 0.1, - shadow: false, - halo: false - }, - select: { - color: '#C0C0C0', - borderColor: '#000000', - shadow: false - } - }, - dataLabels: { - align: null, // auto - verticalAlign: null, // auto - y: null - }, - softThreshold: false, - startFromThreshold: true, // docs (but false doesn't work well): http://jsfiddle.net/highcharts/hz8fopan/14/ - stickyTracking: false, - tooltip: { - distance: 6 - }, - threshold: 0 -}); + // to prevent false extremes, check that control points are between + // neighbouring points' y values + if (leftContY > lastY && leftContY > plotY) { + leftContY = mathMax(lastY, plotY); + rightContY = 2 * plotY - leftContY; // mirror of left control point + } else if (leftContY < lastY && leftContY < plotY) { + leftContY = mathMin(lastY, plotY); + rightContY = 2 * plotY - leftContY; + } + if (rightContY > nextY && rightContY > plotY) { + rightContY = mathMax(nextY, plotY); + leftContY = 2 * plotY - rightContY; + } else if (rightContY < nextY && rightContY < plotY) { + rightContY = mathMin(nextY, plotY); + leftContY = 2 * plotY - rightContY; + } -/** - * ColumnSeries object - */ -var ColumnSeries = extendClass(Series, { - type: 'column', - pointAttrToOptions: { // mapping between SVG attributes and the corresponding options - stroke: 'borderColor', - fill: 'color', - r: 'borderRadius' - }, - 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 - */ - init: function () { - Series.prototype.init.apply(this, arguments); + // record for drawing in next point + point.rightContX = rightContX; + point.rightContY = rightContY; - var series = this, - chart = series.chart; + } - // if the series is added dynamically, force redraw of other - // series affected by a new column - if (chart.hasRendered) { - each(chart.series, function (otherSeries) { - if (otherSeries.type === series.type) { - otherSeries.isDirty = true; - } - }); - } - }, + // Visualize control points for debugging + /* + if (leftContX) { + this.chart.renderer.circle(leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop, 2) + .attr({ + stroke: 'red', + 'stroke-width': 1, + fill: 'none' + }) + .add(); + this.chart.renderer.path(['M', leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop, + 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop]) + .attr({ + stroke: 'red', + 'stroke-width': 1 + }) + .add(); + this.chart.renderer.circle(rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop, 2) + .attr({ + stroke: 'green', + 'stroke-width': 1, + fill: 'none' + }) + .add(); + this.chart.renderer.path(['M', rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop, + 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop]) + .attr({ + stroke: 'green', + 'stroke-width': 1 + }) + .add(); + } + */ - /** - * Return the width and x offset of the columns adjusted for grouping, groupPadding, pointPadding, - * pointWidth etc. - */ - getColumnMetrics: function () { + // moveTo or lineTo + if (!i) { + ret = [M, plotX, plotY]; + } else { // curve from last point to this + ret = [ + 'C', + lastPoint.rightContX || lastPoint.plotX, + lastPoint.rightContY || lastPoint.plotY, + leftContX || plotX, + leftContY || plotY, + plotX, + plotY + ]; + lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later + } + return ret; + } + }); + seriesTypes.spline = SplineSeries; - var series = this, - options = series.options, - xAxis = series.xAxis, - yAxis = series.yAxis, - reversedXAxis = xAxis.reversed, - stackKey, - stackGroups = {}, - columnIndex, - columnCount = 0; + /** + * Set the default options for areaspline + */ + defaultPlotOptions.areaspline = merge(defaultPlotOptions.area); - // Get the total number of column type series. - // This is called on every series. Consider moving this logic to a - // chart.orderStacks() function and call it on init, addSeries and removeSeries - if (options.grouping === false) { - columnCount = 1; - } else { - each(series.chart.series, function (otherSeries) { - var otherOptions = otherSeries.options, - otherYAxis = otherSeries.yAxis; - if (otherSeries.type === series.type && otherSeries.visible && - yAxis.len === otherYAxis.len && yAxis.pos === otherYAxis.pos) { // #642, #2086 - if (otherOptions.stacking) { - stackKey = otherSeries.stackKey; - if (stackGroups[stackKey] === UNDEFINED) { - stackGroups[stackKey] = columnCount++; - } - columnIndex = stackGroups[stackKey]; - } else if (otherOptions.grouping !== false) { // #1162 - columnIndex = columnCount++; - } - otherSeries.columnIndex = columnIndex; - } - }); - } + /** + * AreaSplineSeries object + */ + var areaProto = AreaSeries.prototype, + AreaSplineSeries = extendClass(SplineSeries, { + type: 'areaspline', + closedStacks: true, // instead of following the previous graph back, follow the threshold back - var categoryWidth = mathMin( - mathAbs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || xAxis.tickInterval || 1), // #2610 - xAxis.len // #1535 - ), - groupPadding = categoryWidth * options.groupPadding, - groupWidth = categoryWidth - 2 * groupPadding, - pointOffsetWidth = groupWidth / columnCount, - pointWidth = mathMin( - options.maxPointWidth || xAxis.len, - pick(options.pointWidth, pointOffsetWidth * (1 - 2 * options.pointPadding)) - ), - pointPadding = (pointOffsetWidth - pointWidth) / 2, - colIndex = (reversedXAxis ? - columnCount - (series.columnIndex || 0) : // #1251 - series.columnIndex) || 0, - pointXOffset = pointPadding + (groupPadding + colIndex * - pointOffsetWidth - (categoryWidth / 2)) * - (reversedXAxis ? -1 : 1); + // Mix in methods from the area series + getSegmentPath: areaProto.getSegmentPath, + closeSegment: areaProto.closeSegment, + drawGraph: areaProto.drawGraph, + drawLegendSymbol: LegendSymbolMixin.drawRectangle + }); - // Save it for reading in linked series (Error bars particularly) - return (series.columnMetrics = { - width: pointWidth, - offset: pointXOffset - }); - - }, + seriesTypes.areaspline = AreaSplineSeries; - /** - * Make the columns crisp. The edges are rounded to the nearest full pixel. - */ - crispCol: function (x, y, w, h) { - var chart = this.chart, - borderWidth = this.borderWidth, - xCrisp = -(borderWidth % 2 ? 0.5 : 0), - yCrisp = borderWidth % 2 ? 0.5 : 1, - right, - bottom, - fromTop; + /** + * Set the default options for column + */ + defaultPlotOptions.column = merge(defaultSeriesOptions, { + borderColor: '#FFFFFF', + //borderWidth: 1, + borderRadius: 0, + //colorByPoint: undefined, + groupPadding: 0.2, + //grouping: true, + marker: null, // point options are specified in the base options + pointPadding: 0.1, + //pointWidth: null, + minPointLength: 0, + cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes + pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories + states: { + hover: { + brightness: 0.1, + shadow: false, + halo: false + }, + select: { + color: '#C0C0C0', + borderColor: '#000000', + shadow: false + } + }, + dataLabels: { + align: null, // auto + verticalAlign: null, // auto + y: null + }, + softThreshold: false, + startFromThreshold: true, // false doesn't work well: http://jsfiddle.net/highcharts/hz8fopan/14/ + stickyTracking: false, + tooltip: { + distance: 6 + }, + threshold: 0 + }); - if (chart.inverted && chart.renderer.isVML) { - yCrisp += 1; - } - - // Horizontal. We need to first compute the exact right edge, then round it - // and compute the width from there. - right = Math.round(x + w) + xCrisp; - x = Math.round(x) + xCrisp; - w = right - x; + /** + * ColumnSeries object + */ + var ColumnSeries = extendClass(Series, { + type: 'column', + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'borderColor', + fill: 'color', + r: 'borderRadius' + }, + 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) - // Vertical - fromTop = mathAbs(y) <= 0.5; // #4504 - bottom = Math.round(y + h) + yCrisp; - y = Math.round(y) + yCrisp; - h = bottom - y; + /** + * Initialize the series + */ + init: function () { + Series.prototype.init.apply(this, arguments); - // Top edges are exceptions - if (fromTop) { - y -= 1; - h += 1; - } + var series = this, + chart = series.chart; - return { - x: x, - y: y, - width: w, - height: h - }; - }, + // if the series is added dynamically, force redraw of other + // series affected by a new column + if (chart.hasRendered) { + each(chart.series, function (otherSeries) { + if (otherSeries.type === series.type) { + otherSeries.isDirty = true; + } + }); + } + }, - /** - * Translate each point to the plot area coordinate system and find shape positions - */ - translate: function () { - var series = this, - chart = series.chart, - options = series.options, - borderWidth = series.borderWidth = pick( - options.borderWidth, - series.closestPointRange * series.xAxis.transA < 2 ? 0 : 1 // #3635 - ), - yAxis = series.yAxis, - threshold = options.threshold, - translatedThreshold = series.translatedThreshold = yAxis.getThreshold(threshold), - minPointLength = pick(options.minPointLength, 5), - metrics = series.getColumnMetrics(), - pointWidth = metrics.width, - seriesBarW = series.barW = mathMax(pointWidth, 1 + 2 * borderWidth), // postprocessed for border width - pointXOffset = series.pointXOffset = metrics.offset; + /** + * Return the width and x offset of the columns adjusted for grouping, groupPadding, pointPadding, + * pointWidth etc. + */ + getColumnMetrics: function () { - if (chart.inverted) { - translatedThreshold -= 0.5; // #3355 - } + var series = this, + options = series.options, + xAxis = series.xAxis, + yAxis = series.yAxis, + reversedXAxis = xAxis.reversed, + stackKey, + stackGroups = {}, + columnIndex, + columnCount = 0; - // When the pointPadding is 0, we want the columns to be packed tightly, so we allow individual - // columns to have individual sizes. When pointPadding is greater, we strive for equal-width - // columns (#2694). - if (options.pointPadding) { - seriesBarW = mathCeil(seriesBarW); - } + // Get the total number of column type series. + // This is called on every series. Consider moving this logic to a + // chart.orderStacks() function and call it on init, addSeries and removeSeries + if (options.grouping === false) { + columnCount = 1; + } else { + each(series.chart.series, function (otherSeries) { + var otherOptions = otherSeries.options, + otherYAxis = otherSeries.yAxis; + if (otherSeries.type === series.type && otherSeries.visible && + yAxis.len === otherYAxis.len && yAxis.pos === otherYAxis.pos) { // #642, #2086 + if (otherOptions.stacking) { + stackKey = otherSeries.stackKey; + if (stackGroups[stackKey] === UNDEFINED) { + stackGroups[stackKey] = columnCount++; + } + columnIndex = stackGroups[stackKey]; + } else if (otherOptions.grouping !== false) { // #1162 + columnIndex = columnCount++; + } + otherSeries.columnIndex = columnIndex; + } + }); + } - Series.prototype.translate.apply(series); + var categoryWidth = mathMin( + mathAbs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || xAxis.tickInterval || 1), // #2610 + xAxis.len // #1535 + ), + groupPadding = categoryWidth * options.groupPadding, + groupWidth = categoryWidth - 2 * groupPadding, + pointOffsetWidth = groupWidth / columnCount, + pointWidth = mathMin( + options.maxPointWidth || xAxis.len, + pick(options.pointWidth, pointOffsetWidth * (1 - 2 * options.pointPadding)) + ), + pointPadding = (pointOffsetWidth - pointWidth) / 2, + colIndex = (series.columnIndex || 0) + (reversedXAxis ? 1 : 0), // #1251, #3737 + pointXOffset = pointPadding + (groupPadding + colIndex * + pointOffsetWidth - (categoryWidth / 2)) * + (reversedXAxis ? -1 : 1); - // Record the new values - each(series.points, function (point) { - var yBottom = mathMin(pick(point.yBottom, translatedThreshold), 9e4), // #3575 - safeDistance = 999 + mathAbs(yBottom), - plotY = mathMin(mathMax(-safeDistance, point.plotY), yAxis.len + safeDistance), // Don't draw too far outside plot area (#1303, #2241, #4264) - barX = point.plotX + pointXOffset, - barW = seriesBarW, - barY = mathMin(plotY, yBottom), - up, - barH = mathMax(plotY, yBottom) - barY; + // Save it for reading in linked series (Error bars particularly) + series.columnMetrics = { + width: pointWidth, + offset: pointXOffset + }; + return series.columnMetrics; - // Handle options.minPointLength - if (mathAbs(barH) < minPointLength) { - if (minPointLength) { - barH = minPointLength; - up = (!yAxis.reversed && !point.negative) || (yAxis.reversed && point.negative); - barY = mathAbs(barY - translatedThreshold) > minPointLength ? // stacked - yBottom - minPointLength : // keep position - translatedThreshold - (up ? minPointLength : 0); // #1485, #4051 - } - } + }, - // Cache for access in polar - point.barX = barX; - point.pointWidth = pointWidth; + /** + * Make the columns crisp. The edges are rounded to the nearest full pixel. + */ + crispCol: function (x, y, w, h) { + var chart = this.chart, + borderWidth = this.borderWidth, + xCrisp = -(borderWidth % 2 ? 0.5 : 0), + yCrisp = borderWidth % 2 ? 0.5 : 1, + right, + bottom, + fromTop; - // Fix the tooltip on center of grouped columns (#1216, #424, #3648) - point.tooltipPos = chart.inverted ? - [yAxis.len + yAxis.pos - chart.plotLeft - plotY, series.xAxis.len - barX - barW / 2, barH] : - [barX + barW / 2, plotY + yAxis.pos - chart.plotTop, barH]; + if (chart.inverted && chart.renderer.isVML) { + yCrisp += 1; + } - // Register shape type and arguments to be used in drawPoints - point.shapeType = 'rect'; - point.shapeArgs = series.crispCol(barX, barY, barW, barH); - }); + // Horizontal. We need to first compute the exact right edge, then round it + // and compute the width from there. + right = Math.round(x + w) + xCrisp; + x = Math.round(x) + xCrisp; + w = right - x; - }, + // Vertical + bottom = Math.round(y + h) + yCrisp; + fromTop = mathAbs(y) <= 0.5 && bottom > 0.5; // #4504, #4656 + y = Math.round(y) + yCrisp; + h = bottom - y; - getSymbol: noop, - - /** - * Use a solid rectangle like the area series types - */ - drawLegendSymbol: LegendSymbolMixin.drawRectangle, - - - /** - * Columns have no graph - */ - drawGraph: noop, + // Top edges are exceptions + if (fromTop) { + y -= 1; + h += 1; + } - /** - * Draw the columns. For bars, the series.group is rotated, so the same coordinates - * apply for columns and bars. This method is inherited by scatter series. - * - */ - drawPoints: function () { - var series = this, - chart = this.chart, - options = series.options, - renderer = chart.renderer, - animationLimit = options.animationLimit || 250, - shapeArgs, - pointAttr; + return { + x: x, + y: y, + width: w, + height: h + }; + }, - // draw the columns - each(series.points, function (point) { - var plotY = point.plotY, - graphic = point.graphic, - borderAttr; + /** + * Translate each point to the plot area coordinate system and find shape positions + */ + translate: function () { + var series = this, + chart = series.chart, + options = series.options, + borderWidth = series.borderWidth = pick( + options.borderWidth, + series.closestPointRange * series.xAxis.transA < 2 ? 0 : 1 // #3635 + ), + yAxis = series.yAxis, + threshold = options.threshold, + translatedThreshold = series.translatedThreshold = yAxis.getThreshold(threshold), + minPointLength = pick(options.minPointLength, 5), + metrics = series.getColumnMetrics(), + pointWidth = metrics.width, + seriesBarW = series.barW = mathMax(pointWidth, 1 + 2 * borderWidth), // postprocessed for border width + pointXOffset = series.pointXOffset = metrics.offset; - if (plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) { - shapeArgs = point.shapeArgs; + if (chart.inverted) { + translatedThreshold -= 0.5; // #3355 + } - borderAttr = defined(series.borderWidth) ? { - 'stroke-width': series.borderWidth - } : {}; + // When the pointPadding is 0, we want the columns to be packed tightly, so we allow individual + // columns to have individual sizes. When pointPadding is greater, we strive for equal-width + // columns (#2694). + if (options.pointPadding) { + seriesBarW = mathCeil(seriesBarW); + } - pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] || series.pointAttr[NORMAL_STATE]; - - if (graphic) { // update - stop(graphic); - graphic.attr(borderAttr)[chart.pointCount < animationLimit ? 'animate' : 'attr'](merge(shapeArgs)); + Series.prototype.translate.apply(series); - } else { - point.graphic = graphic = renderer[point.shapeType](shapeArgs) - .attr(borderAttr) - .attr(pointAttr) - .add(point.group || series.group) - .shadow(options.shadow, null, options.stacking && !options.borderRadius); - } + // Record the new values + each(series.points, function (point) { + var yBottom = mathMin(pick(point.yBottom, translatedThreshold), 9e4), // #3575 + safeDistance = 999 + mathAbs(yBottom), + plotY = mathMin(mathMax(-safeDistance, point.plotY), yAxis.len + safeDistance), // Don't draw too far outside plot area (#1303, #2241, #4264) + barX = point.plotX + pointXOffset, + barW = seriesBarW, + barY = mathMin(plotY, yBottom), + up, + barH = mathMax(plotY, yBottom) - barY; - } else if (graphic) { - point.graphic = graphic.destroy(); // #1269 - } - }); - }, + // Handle options.minPointLength + if (mathAbs(barH) < minPointLength) { + if (minPointLength) { + barH = minPointLength; + up = (!yAxis.reversed && !point.negative) || (yAxis.reversed && point.negative); + barY = mathAbs(barY - translatedThreshold) > minPointLength ? // stacked + yBottom - minPointLength : // keep position + translatedThreshold - (up ? minPointLength : 0); // #1485, #4051 + } + } - /** - * Animate the column heights one by one from zero - * @param {Boolean} init Whether to initialize the animation or run it - */ - animate: function (init) { - var series = this, - yAxis = this.yAxis, - options = series.options, - inverted = this.chart.inverted, - attr = {}, - translatedThreshold; + // Cache for access in polar + point.barX = barX; + point.pointWidth = pointWidth; - if (hasSVG) { // VML is too slow anyway - if (init) { - attr.scaleY = 0.001; - translatedThreshold = mathMin(yAxis.pos + yAxis.len, mathMax(yAxis.pos, yAxis.toPixels(options.threshold))); - if (inverted) { - attr.translateX = translatedThreshold - yAxis.len; - } else { - attr.translateY = translatedThreshold; - } - series.group.attr(attr); + // Fix the tooltip on center of grouped columns (#1216, #424, #3648) + point.tooltipPos = chart.inverted ? + [yAxis.len + yAxis.pos - chart.plotLeft - plotY, series.xAxis.len - barX - barW / 2, barH] : + [barX + barW / 2, plotY + yAxis.pos - chart.plotTop, barH]; - } else { // run the animation - - attr.scaleY = 1; - attr[inverted ? 'translateX' : 'translateY'] = yAxis.pos; - series.group.animate(attr, series.options.animation); + // Register shape type and arguments to be used in drawPoints + point.shapeType = 'rect'; + point.shapeArgs = series.crispCol(barX, barY, barW, barH); + }); - // delete this function to allow it only once - series.animate = null; - } - } - }, - - /** - * Remove this series from the chart - */ - remove: function () { - var series = this, - chart = series.chart; + }, - // column and bar series affects other series of the same type - // as they are either stacked or grouped - if (chart.hasRendered) { - each(chart.series, function (otherSeries) { - if (otherSeries.type === series.type) { - otherSeries.isDirty = true; - } - }); - } + getSymbol: noop, - Series.prototype.remove.apply(series, arguments); - } -}); -seriesTypes.column = ColumnSeries; -/** - * Set the default options for bar - */ -defaultPlotOptions.bar = merge(defaultPlotOptions.column); -/** - * The Bar series class - */ -var BarSeries = extendClass(ColumnSeries, { - type: 'bar', - inverted: true -}); -seriesTypes.bar = BarSeries; + /** + * Use a solid rectangle like the area series types + */ + drawLegendSymbol: LegendSymbolMixin.drawRectangle, -/** - * Set the default options for scatter - */ -defaultPlotOptions.scatter = merge(defaultSeriesOptions, { - lineWidth: 0, - marker: { - enabled: true // Overrides auto-enabling in line series (#3647) - }, - tooltip: { - headerFormat: '<span style="color:{point.color}">\u25CF</span> <span style="font-size: 10px;"> {series.name}</span><br/>', - pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>' - } -}); -/** - * The scatter series class - */ -var ScatterSeries = extendClass(Series, { - type: 'scatter', - sorted: false, - requireSorting: false, - noSharedTooltip: true, - trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'], - takeOrdinalPosition: false, // #2342 - kdDimensions: 2, - drawGraph: function () { - if (this.options.lineWidth) { - Series.prototype.drawGraph.call(this); - } - } -}); + /** + * Columns have no graph + */ + drawGraph: noop, -seriesTypes.scatter = ScatterSeries; + /** + * Draw the columns. For bars, the series.group is rotated, so the same coordinates + * apply for columns and bars. This method is inherited by scatter series. + * + */ + drawPoints: function () { + var series = this, + chart = this.chart, + options = series.options, + renderer = chart.renderer, + animationLimit = options.animationLimit || 250, + shapeArgs, + pointAttr; -/** - * Set the default options for pie - */ -defaultPlotOptions.pie = merge(defaultSeriesOptions, { - borderColor: '#FFFFFF', - borderWidth: 1, - center: [null, null], - clip: false, - colorByPoint: true, // always true for pies - dataLabels: { - // align: null, - // connectorWidth: 1, - // connectorColor: point.color, - // connectorPadding: 5, - distance: 30, - enabled: true, - formatter: function () { // #2945 - return this.y === null ? undefined : this.point.name; - }, - // softConnector: true, - x: 0 - // y: 0 - }, - ignoreHiddenPoint: true, - //innerSize: 0, - legendType: 'point', - marker: null, // point options are specified in the base options - size: null, - showInLegend: false, - slicedOffset: 10, - states: { - hover: { - brightness: 0.1, - shadow: false - } - }, - stickyTracking: false, - tooltip: { - followPointer: true - } -}); + // draw the columns + each(series.points, function (point) { + var plotY = point.plotY, + graphic = point.graphic, + borderAttr; -/** - * Extended point object for pies - */ -var PiePoint = extendClass(Point, { - /** - * Initiate the pie slice - */ - init: function () { + if (plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) { + shapeArgs = point.shapeArgs; - Point.prototype.init.apply(this, arguments); + borderAttr = defined(series.borderWidth) ? { + 'stroke-width': series.borderWidth + } : {}; - var point = this, - toggleSlice; + pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] || series.pointAttr[NORMAL_STATE]; - point.name = pick(point.name, 'Slice'); + if (graphic) { // update + stop(graphic); + graphic.attr(borderAttr).attr(pointAttr)[chart.pointCount < animationLimit ? 'animate' : 'attr'](merge(shapeArgs)); // #4267 - // add event listener for select - toggleSlice = function (e) { - point.slice(e.type === 'select'); - }; - addEvent(point, 'select', toggleSlice); - addEvent(point, 'unselect', toggleSlice); + } else { + point.graphic = graphic = renderer[point.shapeType](shapeArgs) + .attr(borderAttr) + .attr(pointAttr) + .add(point.group || series.group) + .shadow(options.shadow, null, options.stacking && !options.borderRadius); + } - return point; - }, + } else if (graphic) { + point.graphic = graphic.destroy(); // #1269 + } + }); + }, - /** - * Toggle the visibility of the pie slice - * @param {Boolean} vis Whether to show the slice or not. If undefined, the - * visibility is toggled - */ - setVisible: function (vis, redraw) { - var point = this, - series = point.series, - chart = series.chart, - ignoreHiddenPoint = series.options.ignoreHiddenPoint; - - redraw = pick(redraw, ignoreHiddenPoint); + /** + * Animate the column heights one by one from zero + * @param {Boolean} init Whether to initialize the animation or run it + */ + animate: function (init) { + var series = this, + yAxis = this.yAxis, + options = series.options, + inverted = this.chart.inverted, + attr = {}, + translatedThreshold; - if (vis !== point.visible) { + if (hasSVG) { // VML is too slow anyway + if (init) { + attr.scaleY = 0.001; + translatedThreshold = mathMin(yAxis.pos + yAxis.len, mathMax(yAxis.pos, yAxis.toPixels(options.threshold))); + if (inverted) { + attr.translateX = translatedThreshold - yAxis.len; + } else { + attr.translateY = translatedThreshold; + } + series.group.attr(attr); - // If called without an argument, toggle visibility - point.visible = point.options.visible = vis = vis === UNDEFINED ? !point.visible : vis; - series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data + } else { // run the animation - // Show and hide associated elements. This is performed regardless of redraw or not, - // because chart.redraw only handles full series. - each(['graphic', 'dataLabel', 'connector', 'shadowGroup'], function (key) { - if (point[key]) { - point[key][vis ? 'show' : 'hide'](true); - } - }); + attr.scaleY = 1; + attr[inverted ? 'translateX' : 'translateY'] = yAxis.pos; + series.group.animate(attr, series.options.animation); - if (point.legendItem) { - chart.legend.colorizeItem(point, vis); - } + // delete this function to allow it only once + series.animate = null; + } + } + }, - // #4170, hide halo after hiding point - if (!vis && point.state === 'hover') { - point.setState(''); - } - - // Handle ignore hidden slices - if (ignoreHiddenPoint) { - series.isDirty = true; - } + /** + * Remove this series from the chart + */ + remove: function () { + var series = this, + chart = series.chart; - if (redraw) { - chart.redraw(); - } - } - }, + // column and bar series affects other series of the same type + // as they are either stacked or grouped + if (chart.hasRendered) { + each(chart.series, function (otherSeries) { + if (otherSeries.type === series.type) { + otherSeries.isDirty = true; + } + }); + } - /** - * Set or toggle whether the slice is cut out from the pie - * @param {Boolean} sliced When undefined, the slice state is toggled - * @param {Boolean} redraw Whether to redraw the chart. True by default. - */ - slice: function (sliced, redraw, animation) { - var point = this, - series = point.series, - chart = series.chart, - translation; + Series.prototype.remove.apply(series, arguments); + } + }); + seriesTypes.column = ColumnSeries; + /** + * Set the default options for bar + */ + defaultPlotOptions.bar = merge(defaultPlotOptions.column); + /** + * The Bar series class + */ + var BarSeries = extendClass(ColumnSeries, { + type: 'bar', + inverted: true + }); + seriesTypes.bar = BarSeries; - setAnimation(animation, chart); + /** + * Set the default options for scatter + */ + defaultPlotOptions.scatter = merge(defaultSeriesOptions, { + lineWidth: 0, + marker: { + enabled: true // Overrides auto-enabling in line series (#3647) + }, + tooltip: { + headerFormat: '<span style="color:{point.color}">\u25CF</span> <span style="font-size: 10px;"> {series.name}</span><br/>', + pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>' + } + }); - // redraw is true by default - redraw = pick(redraw, true); + /** + * The scatter series class + */ + var ScatterSeries = extendClass(Series, { + type: 'scatter', + sorted: false, + requireSorting: false, + noSharedTooltip: true, + trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'], + takeOrdinalPosition: false, // #2342 + kdDimensions: 2, + drawGraph: function () { + if (this.options.lineWidth) { + Series.prototype.drawGraph.call(this); + } + } + }); - // if called without an argument, toggle - point.sliced = point.options.sliced = sliced = defined(sliced) ? sliced : !point.sliced; - series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data + seriesTypes.scatter = ScatterSeries; - translation = sliced ? point.slicedTranslation : { - translateX: 0, - translateY: 0 - }; + /** + * Set the default options for pie + */ + defaultPlotOptions.pie = merge(defaultSeriesOptions, { + borderColor: '#FFFFFF', + borderWidth: 1, + center: [null, null], + clip: false, + colorByPoint: true, // always true for pies + dataLabels: { + // align: null, + // connectorWidth: 1, + // connectorColor: point.color, + // connectorPadding: 5, + distance: 30, + enabled: true, + formatter: function () { // #2945 + return this.y === null ? undefined : this.point.name; + }, + // softConnector: true, + x: 0 + // y: 0 + }, + ignoreHiddenPoint: true, + //innerSize: 0, + legendType: 'point', + marker: null, // point options are specified in the base options + size: null, + showInLegend: false, + slicedOffset: 10, + states: { + hover: { + brightness: 0.1, + shadow: false + } + }, + stickyTracking: false, + tooltip: { + followPointer: true + } + }); - point.graphic.animate(translation); - - if (point.shadowGroup) { - point.shadowGroup.animate(translation); - } + /** + * Extended point object for pies + */ + var PiePoint = extendClass(Point, { + /** + * Initiate the pie slice + */ + init: function () { - }, + Point.prototype.init.apply(this, arguments); - haloPath: function (size) { - var shapeArgs = this.shapeArgs, - chart = this.series.chart; + var point = this, + toggleSlice; - 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 - }); - } -}); + point.name = pick(point.name, 'Slice'); -/** - * The Pie series class - */ -var PieSeries = { - type: 'pie', - isCartesian: false, - pointClass: PiePoint, - requireSorting: false, - directTouch: true, - noSharedTooltip: true, - trackerGroups: ['group', 'dataLabelsGroup'], - axisTypes: [], - pointAttrToOptions: { // mapping between SVG attributes and the corresponding options - stroke: 'borderColor', - 'stroke-width': 'borderWidth', - fill: 'color' - }, + // add event listener for select + toggleSlice = function (e) { + point.slice(e.type === 'select'); + }; + addEvent(point, 'select', toggleSlice); + addEvent(point, 'unselect', toggleSlice); - /** - * Animate the pies in - */ - animate: function (init) { - var series = this, - points = series.points, - startAngleRad = series.startAngleRad; + return point; + }, - if (!init) { - each(points, function (point) { - var graphic = point.graphic, - args = point.shapeArgs; + /** + * Toggle the visibility of the pie slice + * @param {Boolean} vis Whether to show the slice or not. If undefined, the + * visibility is toggled + */ + setVisible: function (vis, redraw) { + var point = this, + series = point.series, + chart = series.chart, + ignoreHiddenPoint = series.options.ignoreHiddenPoint; - if (graphic) { - // start values - graphic.attr({ - r: point.startR || (series.center[3] / 2), // animate from inner radius (#779) - start: startAngleRad, - end: startAngleRad - }); + redraw = pick(redraw, ignoreHiddenPoint); - // animate - graphic.animate({ - r: args.r, - start: args.start, - end: args.end - }, series.options.animation); - } - }); + if (vis !== point.visible) { - // delete this function to allow it only once - series.animate = null; - } - }, + // If called without an argument, toggle visibility + point.visible = point.options.visible = vis = vis === UNDEFINED ? !point.visible : vis; + series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data - /** - * Recompute total chart sum and update percentages of points. - */ - updateTotals: function () { - var i, - total = 0, - points = this.points, - len = points.length, - point, - ignoreHiddenPoint = this.options.ignoreHiddenPoint; + // Show and hide associated elements. This is performed regardless of redraw or not, + // because chart.redraw only handles full series. + each(['graphic', 'dataLabel', 'connector', 'shadowGroup'], function (key) { + if (point[key]) { + point[key][vis ? 'show' : 'hide'](true); + } + }); - // Get the total sum - for (i = 0; i < len; i++) { - point = points[i]; - total += (ignoreHiddenPoint && !point.visible) ? 0 : point.y; - } - this.total = total; + if (point.legendItem) { + chart.legend.colorizeItem(point, vis); + } - // Set each point's properties - for (i = 0; i < len; i++) { - point = points[i]; - point.percentage = (total > 0 && (point.visible || !ignoreHiddenPoint)) ? point.y / total * 100 : 0; - point.total = total; - } - }, + // #4170, hide halo after hiding point + if (!vis && point.state === 'hover') { + point.setState(''); + } - /** - * Extend the generatePoints method by adding total and percentage properties to each point - */ - generatePoints: function () { - Series.prototype.generatePoints.call(this); - this.updateTotals(); - }, - - /** - * Do translation for pie slices - */ - translate: function (positions) { - this.generatePoints(); - - var series = this, - cumulative = 0, - precision = 1000, // issue #172 - options = series.options, - slicedOffset = options.slicedOffset, - connectorOffset = slicedOffset + options.borderWidth, - start, - end, - angle, - startAngle = options.startAngle || 0, - startAngleRad = series.startAngleRad = mathPI / 180 * (startAngle - 90), - endAngleRad = series.endAngleRad = mathPI / 180 * ((pick(options.endAngle, startAngle + 360)) - 90), - circ = endAngleRad - startAngleRad, //2 * mathPI, - points = series.points, - radiusX, // the x component of the radius vector for a given point - radiusY, - labelDistance = options.dataLabels.distance, - ignoreHiddenPoint = options.ignoreHiddenPoint, - i, - len = points.length, - point; + // Handle ignore hidden slices + if (ignoreHiddenPoint) { + series.isDirty = true; + } - // Get positions - either an integer or a percentage string must be given. - // If positions are passed as a parameter, we're in a recursive loop for adjusting - // space for data labels. - if (!positions) { - series.center = positions = series.getCenter(); - } + if (redraw) { + chart.redraw(); + } + } + }, - // utility for getting the x value from a given y, used for anticollision logic in data labels - series.getX = function (y, left) { + /** + * Set or toggle whether the slice is cut out from the pie + * @param {Boolean} sliced When undefined, the slice state is toggled + * @param {Boolean} redraw Whether to redraw the chart. True by default. + */ + slice: function (sliced, redraw, animation) { + var point = this, + series = point.series, + chart = series.chart, + translation; - angle = math.asin(mathMin((y - positions[1]) / (positions[2] / 2 + labelDistance), 1)); + setAnimation(animation, chart); - return positions[0] + - (left ? -1 : 1) * - (mathCos(angle) * (positions[2] / 2 + labelDistance)); - }; + // redraw is true by default + redraw = pick(redraw, true); - // Calculate the geometry for each point - for (i = 0; i < len; i++) { - - point = points[i]; - - // set start and end angle - start = startAngleRad + (cumulative * circ); - if (!ignoreHiddenPoint || point.visible) { - cumulative += point.percentage / 100; - } - end = startAngleRad + (cumulative * circ); + // if called without an argument, toggle + point.sliced = point.options.sliced = sliced = defined(sliced) ? sliced : !point.sliced; + series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data - // set the shape - point.shapeType = 'arc'; - point.shapeArgs = { - x: positions[0], - y: positions[1], - r: positions[2] / 2, - innerR: positions[3] / 2, - start: mathRound(start * precision) / precision, - end: mathRound(end * precision) / precision - }; + translation = sliced ? point.slicedTranslation : { + translateX: 0, + translateY: 0 + }; - // The angle must stay within -90 and 270 (#2645) - angle = (end + start) / 2; - if (angle > 1.5 * mathPI) { - angle -= 2 * mathPI; - } else if (angle < -mathPI / 2) { - angle += 2 * mathPI; - } + point.graphic.animate(translation); - // Center for the sliced out slice - point.slicedTranslation = { - translateX: mathRound(mathCos(angle) * slicedOffset), - translateY: mathRound(mathSin(angle) * slicedOffset) - }; + if (point.shadowGroup) { + point.shadowGroup.animate(translation); + } - // set the anchor point for tooltips - radiusX = mathCos(angle) * positions[2] / 2; - radiusY = mathSin(angle) * positions[2] / 2; - point.tooltipPos = [ - positions[0] + radiusX * 0.7, - positions[1] + radiusY * 0.7 - ]; - - point.half = angle < -mathPI / 2 || angle > mathPI / 2 ? 1 : 0; - point.angle = angle; + }, - // set the anchor point for data labels - connectorOffset = mathMin(connectorOffset, labelDistance / 2); // #1678 - point.labelPos = [ - positions[0] + radiusX + mathCos(angle) * labelDistance, // first break of connector - positions[1] + radiusY + mathSin(angle) * labelDistance, // a/a - positions[0] + radiusX + mathCos(angle) * connectorOffset, // second break, right outside pie - positions[1] + radiusY + mathSin(angle) * connectorOffset, // a/a - positions[0] + radiusX, // landing point for connector - positions[1] + radiusY, // a/a - labelDistance < 0 ? // alignment - 'center' : - point.half ? 'right' : 'left', // alignment - angle // center angle - ]; + haloPath: function (size) { + var shapeArgs = this.shapeArgs, + chart = this.series.chart; - } - }, - - drawGraph: null, + 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 + }); + } + }); - /** - * Draw the data points - */ - drawPoints: function () { - var series = this, - chart = series.chart, - renderer = chart.renderer, - groupTranslation, - //center, - graphic, - //group, - shadow = series.options.shadow, - shadowGroup, - shapeArgs, - attr; + /** + * The Pie series class + */ + var PieSeries = { + type: 'pie', + isCartesian: false, + pointClass: PiePoint, + requireSorting: false, + directTouch: true, + noSharedTooltip: true, + trackerGroups: ['group', 'dataLabelsGroup'], + axisTypes: [], + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'borderColor', + 'stroke-width': 'borderWidth', + fill: 'color' + }, - if (shadow && !series.shadowGroup) { - series.shadowGroup = renderer.g('shadow') - .add(series.group); - } + /** + * Animate the pies in + */ + animate: function (init) { + var series = this, + points = series.points, + startAngleRad = series.startAngleRad; - // draw the slices - each(series.points, function (point) { - if (point.y !== null) { - graphic = point.graphic; - shapeArgs = point.shapeArgs; - shadowGroup = point.shadowGroup; + if (!init) { + each(points, function (point) { + var graphic = point.graphic, + args = point.shapeArgs; - // put the shadow behind all points - if (shadow && !shadowGroup) { - shadowGroup = point.shadowGroup = renderer.g('shadow') - .add(series.shadowGroup); - } + if (graphic) { + // start values + graphic.attr({ + r: point.startR || (series.center[3] / 2), // animate from inner radius (#779) + start: startAngleRad, + end: startAngleRad + }); - // if the point is sliced, use special translation, else use plot area traslation - groupTranslation = point.sliced ? point.slicedTranslation : { - translateX: 0, - translateY: 0 - }; + // animate + graphic.animate({ + r: args.r, + start: args.start, + end: args.end + }, series.options.animation); + } + }); - //group.translate(groupTranslation[0], groupTranslation[1]); - if (shadowGroup) { - shadowGroup.attr(groupTranslation); - } + // delete this function to allow it only once + series.animate = null; + } + }, - // draw the slice - if (graphic) { - graphic - .setRadialReference(series.center) - .animate(extend(shapeArgs, groupTranslation)); - } else { - attr = { 'stroke-linejoin': 'round' }; - if (!point.visible) { - attr.visibility = 'hidden'; - } + /** + * Recompute total chart sum and update percentages of points. + */ + updateTotals: function () { + var i, + total = 0, + points = this.points, + len = points.length, + point, + ignoreHiddenPoint = this.options.ignoreHiddenPoint; - point.graphic = graphic = renderer[point.shapeType](shapeArgs) - .setRadialReference(series.center) - .attr( - point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] - ) - .attr(attr) - .attr(groupTranslation) - .add(series.group) - .shadow(shadow, shadowGroup); - } - } - }); + // Get the total sum + for (i = 0; i < len; i++) { + point = points[i]; + total += (ignoreHiddenPoint && !point.visible) ? 0 : point.y; + } + this.total = total; - }, + // Set each point's properties + for (i = 0; i < len; i++) { + point = points[i]; + point.percentage = (total > 0 && (point.visible || !ignoreHiddenPoint)) ? point.y / total * 100 : 0; + point.total = total; + } + }, + /** + * Extend the generatePoints method by adding total and percentage properties to each point + */ + generatePoints: function () { + Series.prototype.generatePoints.call(this); + this.updateTotals(); + }, - searchPoint: noop, + /** + * Do translation for pie slices + */ + translate: function (positions) { + this.generatePoints(); - /** - * Utility for sorting data labels - */ - sortByAngle: function (points, sign) { - points.sort(function (a, b) { - return a.angle !== undefined && (b.angle - a.angle) * sign; - }); - }, + var series = this, + cumulative = 0, + precision = 1000, // issue #172 + options = series.options, + slicedOffset = options.slicedOffset, + connectorOffset = slicedOffset + options.borderWidth, + start, + end, + angle, + startAngle = options.startAngle || 0, + startAngleRad = series.startAngleRad = mathPI / 180 * (startAngle - 90), + endAngleRad = series.endAngleRad = mathPI / 180 * ((pick(options.endAngle, startAngle + 360)) - 90), + circ = endAngleRad - startAngleRad, //2 * mathPI, + points = series.points, + radiusX, // the x component of the radius vector for a given point + radiusY, + labelDistance = options.dataLabels.distance, + ignoreHiddenPoint = options.ignoreHiddenPoint, + i, + len = points.length, + point; - /** - * Use a simple symbol from LegendSymbolMixin - */ - drawLegendSymbol: LegendSymbolMixin.drawRectangle, + // Get positions - either an integer or a percentage string must be given. + // If positions are passed as a parameter, we're in a recursive loop for adjusting + // space for data labels. + if (!positions) { + series.center = positions = series.getCenter(); + } - /** - * Use the getCenter method from drawLegendSymbol - */ - getCenter: CenteredSeriesMixin.getCenter, + // utility for getting the x value from a given y, used for anticollision logic in data labels + series.getX = function (y, left) { - /** - * Pies don't have point marker symbols - */ - getSymbol: noop + angle = math.asin(mathMin((y - positions[1]) / (positions[2] / 2 + labelDistance), 1)); -}; -PieSeries = extendClass(Series, PieSeries); -seriesTypes.pie = PieSeries; + return positions[0] + + (left ? -1 : 1) * + (mathCos(angle) * (positions[2] / 2 + labelDistance)); + }; -/** - * Draw the data labels - */ -Series.prototype.drawDataLabels = function () { + // Calculate the geometry for each point + for (i = 0; i < len; i++) { - var series = this, - seriesOptions = series.options, - cursor = seriesOptions.cursor, - options = seriesOptions.dataLabels, - points = series.points, - pointOptions, - generalOptions, - hasRendered = series.hasRendered || 0, - str, - dataLabelsGroup, - renderer = series.chart.renderer; + point = points[i]; - if (options.enabled || series._hasPointLabels) { + // set start and end angle + start = startAngleRad + (cumulative * circ); + if (!ignoreHiddenPoint || point.visible) { + cumulative += point.percentage / 100; + } + end = startAngleRad + (cumulative * circ); - // Process default alignment of data labels for columns - if (series.dlProcessOptions) { - series.dlProcessOptions(options); - } + // set the shape + point.shapeType = 'arc'; + point.shapeArgs = { + x: positions[0], + y: positions[1], + r: positions[2] / 2, + innerR: positions[3] / 2, + start: mathRound(start * precision) / precision, + end: mathRound(end * precision) / precision + }; - // Create a separate group for the data labels to avoid rotation - dataLabelsGroup = series.plotGroup( - 'dataLabelsGroup', - 'data-labels', - options.defer ? HIDDEN : VISIBLE, - options.zIndex || 6 - ); + // The angle must stay within -90 and 270 (#2645) + angle = (end + start) / 2; + if (angle > 1.5 * mathPI) { + angle -= 2 * mathPI; + } else if (angle < -mathPI / 2) { + angle += 2 * mathPI; + } - if (pick(options.defer, true)) { - dataLabelsGroup.attr({ opacity: +hasRendered }); // #3300 - if (!hasRendered) { - addEvent(series, 'afterAnimate', function () { - if (series.visible) { // #3023, #3024 - dataLabelsGroup.show(); - } - dataLabelsGroup[seriesOptions.animation ? 'animate' : 'attr']({ opacity: 1 }, { duration: 200 }); - }); - } - } + // Center for the sliced out slice + point.slicedTranslation = { + translateX: mathRound(mathCos(angle) * slicedOffset), + translateY: mathRound(mathSin(angle) * slicedOffset) + }; - // Make the labels for each point - generalOptions = options; - each(points, function (point) { + // set the anchor point for tooltips + radiusX = mathCos(angle) * positions[2] / 2; + radiusY = mathSin(angle) * positions[2] / 2; + point.tooltipPos = [ + positions[0] + radiusX * 0.7, + positions[1] + radiusY * 0.7 + ]; - var enabled, - dataLabel = point.dataLabel, - labelConfig, - attr, - name, - rotation, - connector = point.connector, - isNew = true, - style, - moreStyle = {}; + point.half = angle < -mathPI / 2 || angle > mathPI / 2 ? 1 : 0; + point.angle = angle; - // Determine if each data label is enabled - pointOptions = point.dlOptions || (point.options && point.options.dataLabels); // dlOptions is used in treemaps - enabled = pick(pointOptions && pointOptions.enabled, generalOptions.enabled); // #2282 + // set the anchor point for data labels + connectorOffset = mathMin(connectorOffset, labelDistance / 2); // #1678 + point.labelPos = [ + positions[0] + radiusX + mathCos(angle) * labelDistance, // first break of connector + positions[1] + radiusY + mathSin(angle) * labelDistance, // a/a + positions[0] + radiusX + mathCos(angle) * connectorOffset, // second break, right outside pie + positions[1] + radiusY + mathSin(angle) * connectorOffset, // a/a + positions[0] + radiusX, // landing point for connector + positions[1] + radiusY, // a/a + labelDistance < 0 ? // alignment + 'center' : + point.half ? 'right' : 'left', // alignment + angle // center angle + ]; + } + }, - // If the point is outside the plot area, destroy it. #678, #820 - if (dataLabel && !enabled) { - point.dataLabel = dataLabel.destroy(); + drawGraph: null, - // Individual labels are disabled if the are explicitly disabled - // in the point options, or if they fall outside the plot area. - } else if (enabled) { + /** + * Draw the data points + */ + drawPoints: function () { + var series = this, + chart = series.chart, + renderer = chart.renderer, + groupTranslation, + //center, + graphic, + //group, + shadow = series.options.shadow, + shadowGroup, + pointAttr, + shapeArgs, + attr; - // Create individual options structure that can be extended without - // affecting others - options = merge(generalOptions, pointOptions); - style = options.style; + if (shadow && !series.shadowGroup) { + series.shadowGroup = renderer.g('shadow') + .add(series.group); + } - rotation = options.rotation; + // draw the slices + each(series.points, function (point) { + if (point.y !== null) { + graphic = point.graphic; + shapeArgs = point.shapeArgs; + shadowGroup = point.shadowGroup; + pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]; + if (!pointAttr.stroke) { + pointAttr.stroke = pointAttr.fill; + } - // Get the string - labelConfig = point.getLabelConfig(); - str = options.format ? - format(options.format, labelConfig) : - options.formatter.call(labelConfig, options); + // put the shadow behind all points + if (shadow && !shadowGroup) { + shadowGroup = point.shadowGroup = renderer.g('shadow') + .add(series.shadowGroup); + } - // Determine the color - style.color = pick(options.color, style.color, series.color, 'black'); + // if the point is sliced, use special translation, else use plot area traslation + groupTranslation = point.sliced ? point.slicedTranslation : { + translateX: 0, + translateY: 0 + }; + //group.translate(groupTranslation[0], groupTranslation[1]); + if (shadowGroup) { + shadowGroup.attr(groupTranslation); + } - // update existing label - if (dataLabel) { + // draw the slice + if (graphic) { + graphic + .setRadialReference(series.center) + .attr(pointAttr) + .animate(extend(shapeArgs, groupTranslation)); + } else { + attr = { 'stroke-linejoin': 'round' }; + if (!point.visible) { + attr.visibility = 'hidden'; + } - if (defined(str)) { - dataLabel - .attr({ - text: str - }); - isNew = false; + point.graphic = graphic = renderer[point.shapeType](shapeArgs) + .setRadialReference(series.center) + .attr(pointAttr) + .attr(attr) + .attr(groupTranslation) + .add(series.group) + .shadow(shadow, shadowGroup); + } + } + }); - } else { // #1437 - the label is shown conditionally - point.dataLabel = dataLabel = dataLabel.destroy(); - if (connector) { - point.connector = connector.destroy(); - } - } + }, - // create new label - } else if (defined(str)) { - attr = { - //align: align, - fill: options.backgroundColor, - stroke: options.borderColor, - 'stroke-width': options.borderWidth, - r: options.borderRadius || 0, - rotation: rotation, - padding: options.padding, - zIndex: 1 - }; - - // Get automated contrast color - if (style.color === 'contrast') { - moreStyle.color = options.inside || options.distance < 0 || !!seriesOptions.stacking ? - renderer.getContrast(point.color || series.color) : - '#000000'; - } - if (cursor) { - moreStyle.cursor = cursor; - } - - // Remove unused attributes (#947) - for (name in attr) { - if (attr[name] === UNDEFINED) { - delete attr[name]; - } - } + searchPoint: noop, - dataLabel = point.dataLabel = renderer[rotation ? 'text' : 'label']( // labels don't support rotation - str, - 0, - -999, - options.shape, - null, - null, - options.useHTML - ) - .attr(attr) - .css(extend(style, moreStyle)) - .add(dataLabelsGroup) - .shadow(options.shadow); + /** + * Utility for sorting data labels + */ + sortByAngle: function (points, sign) { + points.sort(function (a, b) { + return a.angle !== undefined && (b.angle - a.angle) * sign; + }); + }, - } + /** + * Use a simple symbol from LegendSymbolMixin + */ + drawLegendSymbol: LegendSymbolMixin.drawRectangle, - if (dataLabel) { - // Now the data label is created and placed at 0,0, so we need to align it - series.alignDataLabel(point, dataLabel, options, null, isNew); - } - } - }); - } -}; + /** + * Use the getCenter method from drawLegendSymbol + */ + getCenter: CenteredSeriesMixin.getCenter, -/** - * Align each individual data label - */ -Series.prototype.alignDataLabel = function (point, dataLabel, options, alignTo, isNew) { - var chart = this.chart, - inverted = chart.inverted, - plotX = pick(point.plotX, -999), - plotY = pick(point.plotY, -999), - bBox = dataLabel.getBBox(), - baseline = chart.renderer.fontMetrics(options.style.fontSize).b, - rotCorr, // rotation correction - // Math.round for rounding errors (#2683), alignTo to allow column labels (#2700) - visible = this.visible && (point.series.forceDL || chart.isInsidePlot(plotX, mathRound(plotY), inverted) || - (alignTo && chart.isInsidePlot(plotX, inverted ? alignTo.x + 1 : alignTo.y + alignTo.height - 1, inverted))), - alignAttr; // the final position; + /** + * Pies don't have point marker symbols + */ + getSymbol: noop - if (visible) { + }; + PieSeries = extendClass(Series, PieSeries); + seriesTypes.pie = PieSeries; - // The alignment box is a singular point - alignTo = extend({ - x: inverted ? chart.plotWidth - plotY : plotX, - y: mathRound(inverted ? chart.plotHeight - plotX : plotY), - width: 0, - height: 0 - }, alignTo); + /** + * Draw the data labels + */ + Series.prototype.drawDataLabels = function () { - // Add the text size for alignment calculation - extend(options, { - width: bBox.width, - height: bBox.height - }); + var series = this, + seriesOptions = series.options, + cursor = seriesOptions.cursor, + options = seriesOptions.dataLabels, + points = series.points, + pointOptions, + generalOptions, + hasRendered = series.hasRendered || 0, + str, + dataLabelsGroup, + renderer = series.chart.renderer; - // Allow a hook for changing alignment in the last moment, then do the alignment - if (options.rotation) { // Fancy box alignment isn't supported for rotated text - rotCorr = chart.renderer.rotCorr(baseline, options.rotation); // #3723 - dataLabel[isNew ? 'attr' : 'animate']({ - x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x, - y: alignTo.y + options.y + alignTo.height / 2 - }) - .attr({ // #3003 - align: options.align - }); - } else { - dataLabel.align(options, null, alignTo); - alignAttr = dataLabel.alignAttr; + if (options.enabled || series._hasPointLabels) { - // Handle justify or crop - if (pick(options.overflow, 'justify') === 'justify') { - this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew); + // Process default alignment of data labels for columns + if (series.dlProcessOptions) { + series.dlProcessOptions(options); + } - } else if (pick(options.crop, true)) { - // Now check that the data label is within the plot area - visible = chart.isInsidePlot(alignAttr.x, alignAttr.y) && chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height); + // Create a separate group for the data labels to avoid rotation + dataLabelsGroup = series.plotGroup( + 'dataLabelsGroup', + 'data-labels', + options.defer ? HIDDEN : VISIBLE, + options.zIndex || 6 + ); - } + if (pick(options.defer, true)) { + dataLabelsGroup.attr({ opacity: +hasRendered }); // #3300 + if (!hasRendered) { + addEvent(series, 'afterAnimate', function () { + if (series.visible) { // #3023, #3024 + dataLabelsGroup.show(); + } + dataLabelsGroup[seriesOptions.animation ? 'animate' : 'attr']({ opacity: 1 }, { duration: 200 }); + }); + } + } - // When we're using a shape, make it possible with a connector or an arrow pointing to thie point - if (options.shape) { - dataLabel.attr({ - anchorX: point.plotX, - anchorY: point.plotY - }); - } + // Make the labels for each point + generalOptions = options; + each(points, function (point) { - } - } + var enabled, + dataLabel = point.dataLabel, + labelConfig, + attr, + name, + rotation, + connector = point.connector, + isNew = true, + style, + moreStyle = {}; - // Show or hide based on the final aligned position - if (!visible) { - stop(dataLabel); - dataLabel.attr({ y: -999 }); - dataLabel.placed = false; // don't animate back in - } + // Determine if each data label is enabled + pointOptions = point.dlOptions || (point.options && point.options.dataLabels); // dlOptions is used in treemaps + enabled = pick(pointOptions && pointOptions.enabled, generalOptions.enabled) && point.y !== null; // #2282, #4641 -}; -/** - * If data labels fall partly outside the plot area, align them back in, in a way that - * doesn't hide the point. - */ -Series.prototype.justifyDataLabel = function (dataLabel, options, alignAttr, bBox, alignTo, isNew) { - var chart = this.chart, - align = options.align, - verticalAlign = options.verticalAlign, - off, - justified, - padding = dataLabel.box ? 0 : (dataLabel.padding || 0); + // If the point is outside the plot area, destroy it. #678, #820 + if (dataLabel && !enabled) { + point.dataLabel = dataLabel.destroy(); - // Off left - off = alignAttr.x + padding; - if (off < 0) { - if (align === 'right') { - options.align = 'left'; - } else { - options.x = -off; - } - justified = true; - } + // Individual labels are disabled if the are explicitly disabled + // in the point options, or if they fall outside the plot area. + } else if (enabled) { - // Off right - off = alignAttr.x + bBox.width - padding; - if (off > chart.plotWidth) { - if (align === 'left') { - options.align = 'right'; - } else { - options.x = chart.plotWidth - off; - } - justified = true; - } + // Create individual options structure that can be extended without + // affecting others + options = merge(generalOptions, pointOptions); + style = options.style; - // Off top - off = alignAttr.y + padding; - if (off < 0) { - if (verticalAlign === 'bottom') { - options.verticalAlign = 'top'; - } else { - options.y = -off; - } - justified = true; - } + rotation = options.rotation; - // Off bottom - off = alignAttr.y + bBox.height - padding; - if (off > chart.plotHeight) { - if (verticalAlign === 'top') { - options.verticalAlign = 'bottom'; - } else { - options.y = chart.plotHeight - off; - } - justified = true; - } + // Get the string + labelConfig = point.getLabelConfig(); + str = options.format ? + format(options.format, labelConfig) : + options.formatter.call(labelConfig, options); - if (justified) { - dataLabel.placed = !isNew; - dataLabel.align(options, null, alignTo); - } -}; + // Determine the color + style.color = pick(options.color, style.color, series.color, 'black'); -/** - * Override the base drawDataLabels method by pie specific functionality - */ -if (seriesTypes.pie) { - seriesTypes.pie.prototype.drawDataLabels = function () { - var series = this, - data = series.data, - point, - chart = series.chart, - options = series.options.dataLabels, - connectorPadding = pick(options.connectorPadding, 10), - connectorWidth = pick(options.connectorWidth, 1), - plotWidth = chart.plotWidth, - plotHeight = chart.plotHeight, - connector, - connectorPath, - softConnector = pick(options.softConnector, true), - distanceOption = options.distance, - seriesCenter = series.center, - radius = seriesCenter[2] / 2, - centerY = seriesCenter[1], - outside = distanceOption > 0, - dataLabel, - dataLabelWidth, - labelPos, - labelHeight, - halves = [// divide the points into right and left halves for anti collision - [], // right - [] // left - ], - x, - y, - visibility, - rankArr, - i, - j, - overflow = [0, 0, 0, 0], // top, right, bottom, left - sort = function (a, b) { - return b.y - a.y; - }; - // get out if not enabled - if (!series.visible || (!options.enabled && !series._hasPointLabels)) { - return; - } + // update existing label + if (dataLabel) { - // run parent method - Series.prototype.drawDataLabels.apply(series); + if (defined(str)) { + dataLabel + .attr({ + text: str + }); + isNew = false; - // arrange points for detection collision - each(data, function (point) { - if (point.dataLabel && point.visible) { // #407, #2510 - halves[point.half].push(point); - } - }); + } else { // #1437 - the label is shown conditionally + point.dataLabel = dataLabel = dataLabel.destroy(); + if (connector) { + point.connector = connector.destroy(); + } + } - /* Loop over the points in each half, starting from the top and bottom - * of the pie to detect overlapping labels. - */ - i = 2; - while (i--) { + // create new label + } else if (defined(str)) { + attr = { + //align: align, + fill: options.backgroundColor, + stroke: options.borderColor, + 'stroke-width': options.borderWidth, + r: options.borderRadius || 0, + rotation: rotation, + padding: options.padding, + zIndex: 1 + }; - var slots = [], - slotsLength, - usedSlots = [], - points = halves[i], - pos, - bottom, - length = points.length, - slotIndex; + // Get automated contrast color + if (style.color === 'contrast') { + moreStyle.color = options.inside || options.distance < 0 || !!seriesOptions.stacking ? + renderer.getContrast(point.color || series.color) : + '#000000'; + } + if (cursor) { + moreStyle.cursor = cursor; + } - if (!length) { - continue; - } - // Sort by angle - series.sortByAngle(points, i - 0.5); + // Remove unused attributes (#947) + for (name in attr) { + if (attr[name] === UNDEFINED) { + delete attr[name]; + } + } - // Assume equal label heights on either hemisphere (#2630) - j = labelHeight = 0; - while (!labelHeight && points[j]) { // #1569 - labelHeight = points[j] && points[j].dataLabel && (points[j].dataLabel.getBBox().height || 21); // 21 is for #968 - j++; - } + dataLabel = point.dataLabel = renderer[rotation ? 'text' : 'label']( // labels don't support rotation + str, + 0, + -9999, + options.shape, + null, + null, + options.useHTML + ) + .attr(attr) + .css(extend(style, moreStyle)) + .add(dataLabelsGroup) + .shadow(options.shadow); - // Only do anti-collision when we are outside the pie and have connectors (#856) - if (distanceOption > 0) { + } - // Build the slots - bottom = mathMin(centerY + radius + distanceOption, chart.plotHeight); - for (pos = mathMax(0, centerY - radius - distanceOption); pos <= bottom; pos += labelHeight) { - slots.push(pos); - } - slotsLength = slots.length; + if (dataLabel) { + // Now the data label is created and placed at 0,0, so we need to align it + series.alignDataLabel(point, dataLabel, options, null, isNew); + } + } + }); + } + }; + /** + * Align each individual data label + */ + Series.prototype.alignDataLabel = function (point, dataLabel, options, alignTo, isNew) { + var chart = this.chart, + inverted = chart.inverted, + plotX = pick(point.plotX, -9999), + plotY = pick(point.plotY, -9999), + bBox = dataLabel.getBBox(), + baseline = chart.renderer.fontMetrics(options.style.fontSize).b, + rotCorr, // rotation correction + // Math.round for rounding errors (#2683), alignTo to allow column labels (#2700) + visible = this.visible && (point.series.forceDL || chart.isInsidePlot(plotX, mathRound(plotY), inverted) || + (alignTo && chart.isInsidePlot(plotX, inverted ? alignTo.x + 1 : alignTo.y + alignTo.height - 1, inverted))), + alignAttr; // the final position; - /* Visualize the slots - if (!series.slotElements) { - series.slotElements = []; - } - if (i === 1) { - series.slotElements.forEach(function (elem) { - elem.destroy(); - }); - series.slotElements.length = 0; - } - - slots.forEach(function (pos, no) { - var slotX = series.getX(pos, i) + chart.plotLeft - (i ? 100 : 0), - slotY = pos + chart.plotTop; - - if (!isNaN(slotX)) { - series.slotElements.push(chart.renderer.rect(slotX, slotY - 7, 100, labelHeight, 1) - .attr({ - 'stroke-width': 1, - stroke: 'silver', - fill: 'rgba(0,0,255,0.1)' - }) - .add()); - series.slotElements.push(chart.renderer.text('Slot '+ no, slotX, slotY + 4) - .attr({ - fill: 'silver' - }).add()); - } - }); - // */ + if (visible) { - // if there are more values than available slots, remove lowest values - if (length > slotsLength) { - // create an array for sorting and ranking the points within each quarter - rankArr = [].concat(points); - rankArr.sort(sort); - j = length; - while (j--) { - rankArr[j].rank = j; - } - j = length; - while (j--) { - if (points[j].rank >= slotsLength) { - points.splice(j, 1); - } - } - length = points.length; - } + // The alignment box is a singular point + alignTo = extend({ + x: inverted ? chart.plotWidth - plotY : plotX, + y: mathRound(inverted ? chart.plotHeight - plotX : plotY), + width: 0, + height: 0 + }, alignTo); - // The label goes to the nearest open slot, but not closer to the edge than - // the label's index. - for (j = 0; j < length; j++) { + // Add the text size for alignment calculation + extend(options, { + width: bBox.width, + height: bBox.height + }); - point = points[j]; - labelPos = point.labelPos; + // Allow a hook for changing alignment in the last moment, then do the alignment + if (options.rotation) { // Fancy box alignment isn't supported for rotated text + rotCorr = chart.renderer.rotCorr(baseline, options.rotation); // #3723 + dataLabel[isNew ? 'attr' : 'animate']({ + x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x, + y: alignTo.y + options.y + alignTo.height / 2 + }) + .attr({ // #3003 + align: options.align + }); + } else { + dataLabel.align(options, null, alignTo); + alignAttr = dataLabel.alignAttr; - var closest = 9999, - distance, - slotI; + // Handle justify or crop + if (pick(options.overflow, 'justify') === 'justify') { + this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew); - // find the closest slot index - for (slotI = 0; slotI < slotsLength; slotI++) { - distance = mathAbs(slots[slotI] - labelPos[1]); - if (distance < closest) { - closest = distance; - slotIndex = slotI; - } - } + } else if (pick(options.crop, true)) { + // Now check that the data label is within the plot area + visible = chart.isInsidePlot(alignAttr.x, alignAttr.y) && chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height); - // if that slot index is closer to the edges of the slots, move it - // to the closest appropriate slot - if (slotIndex < j && slots[j] !== null) { // cluster at the top - slotIndex = j; - } else if (slotsLength < length - j + slotIndex && slots[j] !== null) { // cluster at the bottom - slotIndex = slotsLength - length + j; - while (slots[slotIndex] === null) { // make sure it is not taken - slotIndex++; - } - } else { - // Slot is taken, find next free slot below. In the next run, the next slice will find the - // slot above these, because it is the closest one - while (slots[slotIndex] === null) { // make sure it is not taken - slotIndex++; - } - } + } - usedSlots.push({ i: slotIndex, y: slots[slotIndex] }); - slots[slotIndex] = null; // mark as taken - } - // sort them in order to fill in from the top - usedSlots.sort(sort); - } + // When we're using a shape, make it possible with a connector or an arrow pointing to thie point + if (options.shape) { + dataLabel.attr({ + anchorX: point.plotX, + anchorY: point.plotY + }); + } - // now the used slots are sorted, fill them up sequentially - for (j = 0; j < length; j++) { + } + } - var slot, naturalY; + // Show or hide based on the final aligned position + if (!visible) { + stop(dataLabel); + dataLabel.attr({ y: -9999 }); + dataLabel.placed = false; // don't animate back in + } - point = points[j]; - labelPos = point.labelPos; - dataLabel = point.dataLabel; - visibility = point.visible === false ? HIDDEN : 'inherit'; - naturalY = labelPos[1]; + }; - if (distanceOption > 0) { - slot = usedSlots.pop(); - slotIndex = slot.i; + /** + * If data labels fall partly outside the plot area, align them back in, in a way that + * doesn't hide the point. + */ + Series.prototype.justifyDataLabel = function (dataLabel, options, alignAttr, bBox, alignTo, isNew) { + var chart = this.chart, + align = options.align, + verticalAlign = options.verticalAlign, + off, + justified, + padding = dataLabel.box ? 0 : (dataLabel.padding || 0); - // if the slot next to currrent slot is free, the y value is allowed - // to fall back to the natural position - y = slot.y; - if ((naturalY > y && slots[slotIndex + 1] !== null) || - (naturalY < y && slots[slotIndex - 1] !== null)) { - y = mathMin(mathMax(0, naturalY), chart.plotHeight); - } + // Off left + off = alignAttr.x + padding; + if (off < 0) { + if (align === 'right') { + options.align = 'left'; + } else { + options.x = -off; + } + justified = true; + } - } else { - y = naturalY; - } + // Off right + off = alignAttr.x + bBox.width - padding; + if (off > chart.plotWidth) { + if (align === 'left') { + options.align = 'right'; + } else { + options.x = chart.plotWidth - off; + } + justified = true; + } - // get the x - use the natural x position for first and last slot, to prevent the top - // and botton slice connectors from touching each other on either side - x = options.justify ? - seriesCenter[0] + (i ? -1 : 1) * (radius + distanceOption) : - series.getX(y === centerY - radius - distanceOption || y === centerY + radius + distanceOption ? naturalY : y, i); + // Off top + off = alignAttr.y + padding; + if (off < 0) { + if (verticalAlign === 'bottom') { + options.verticalAlign = 'top'; + } else { + options.y = -off; + } + justified = true; + } + // Off bottom + off = alignAttr.y + bBox.height - padding; + if (off > chart.plotHeight) { + if (verticalAlign === 'top') { + options.verticalAlign = 'bottom'; + } else { + options.y = chart.plotHeight - off; + } + justified = true; + } - // Record the placement and visibility - dataLabel._attr = { - visibility: visibility, - align: labelPos[6] - }; - dataLabel._pos = { - x: x + options.x + - ({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0), - y: y + options.y - 10 // 10 is for the baseline (label vs text) - }; - dataLabel.connX = x; - dataLabel.connY = y; + if (justified) { + dataLabel.placed = !isNew; + dataLabel.align(options, null, alignTo); + } + }; + /** + * Override the base drawDataLabels method by pie specific functionality + */ + if (seriesTypes.pie) { + seriesTypes.pie.prototype.drawDataLabels = function () { + var series = this, + data = series.data, + point, + chart = series.chart, + options = series.options.dataLabels, + connectorPadding = pick(options.connectorPadding, 10), + connectorWidth = pick(options.connectorWidth, 1), + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + connector, + connectorPath, + softConnector = pick(options.softConnector, true), + distanceOption = options.distance, + seriesCenter = series.center, + radius = seriesCenter[2] / 2, + centerY = seriesCenter[1], + outside = distanceOption > 0, + dataLabel, + dataLabelWidth, + labelPos, + labelHeight, + halves = [// divide the points into right and left halves for anti collision + [], // right + [] // left + ], + x, + y, + visibility, + rankArr, + i, + j, + overflow = [0, 0, 0, 0], // top, right, bottom, left + sort = function (a, b) { + return b.y - a.y; + }; - // Detect overflowing data labels - if (this.options.size === null) { - dataLabelWidth = dataLabel.width; - // Overflow left - if (x - dataLabelWidth < connectorPadding) { - overflow[3] = mathMax(mathRound(dataLabelWidth - x + connectorPadding), overflow[3]); + // get out if not enabled + if (!series.visible || (!options.enabled && !series._hasPointLabels)) { + return; + } - // Overflow right - } else if (x + dataLabelWidth > plotWidth - connectorPadding) { - overflow[1] = mathMax(mathRound(x + dataLabelWidth - plotWidth + connectorPadding), overflow[1]); - } + // run parent method + Series.prototype.drawDataLabels.apply(series); - // Overflow top - if (y - labelHeight / 2 < 0) { - overflow[0] = mathMax(mathRound(-y + labelHeight / 2), overflow[0]); + // arrange points for detection collision + each(data, function (point) { + if (point.dataLabel && point.visible) { // #407, #2510 + halves[point.half].push(point); + } + }); - // Overflow left - } else if (y + labelHeight / 2 > plotHeight) { - overflow[2] = mathMax(mathRound(y + labelHeight / 2 - plotHeight), overflow[2]); - } - } - } // for each point - } // for each half + /* Loop over the points in each half, starting from the top and bottom + * of the pie to detect overlapping labels. + */ + i = 2; + while (i--) { - // Do not apply the final placement and draw the connectors until we have verified - // that labels are not spilling over. - if (arrayMax(overflow) === 0 || this.verifyDataLabelOverflow(overflow)) { + var slots = [], + slotsLength, + usedSlots = [], + points = halves[i], + pos, + bottom, + length = points.length, + slotIndex; - // Place the labels in the final position - this.placeDataLabels(); + if (!length) { + continue; + } - // Draw the connectors - if (outside && connectorWidth) { - each(this.points, function (point) { - connector = point.connector; - labelPos = point.labelPos; - dataLabel = point.dataLabel; + // Sort by angle + series.sortByAngle(points, i - 0.5); - if (dataLabel && dataLabel._pos && point.visible) { - visibility = dataLabel._attr.visibility; - x = dataLabel.connX; - y = dataLabel.connY; - connectorPath = softConnector ? [ - 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], - labelPos[2], labelPos[3], // second break - L, - labelPos[4], labelPos[5] // base - ] : [ - M, - x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label - L, - labelPos[2], labelPos[3], // second break - L, - labelPos[4], labelPos[5] // base - ]; + // Assume equal label heights on either hemisphere (#2630) + j = labelHeight = 0; + while (!labelHeight && points[j]) { // #1569 + labelHeight = points[j] && points[j].dataLabel && (points[j].dataLabel.getBBox().height || 21); // 21 is for #968 + j++; + } - if (connector) { - connector.animate({ d: connectorPath }); - connector.attr('visibility', visibility); + // Only do anti-collision when we are outside the pie and have connectors (#856) + if (distanceOption > 0) { - } else { - point.connector = connector = series.chart.renderer.path(connectorPath).attr({ - 'stroke-width': connectorWidth, - stroke: options.connectorColor || point.color || '#606060', - visibility: visibility - //zIndex: 0 // #2722 (reversed) - }) - .add(series.dataLabelsGroup); - } - } else if (connector) { - point.connector = connector.destroy(); - } - }); - } - } - }; - /** - * Perform the final placement of the data labels after we have verified that they - * fall within the plot area. - */ - seriesTypes.pie.prototype.placeDataLabels = function () { - each(this.points, function (point) { - var dataLabel = point.dataLabel, - _pos; + // Build the slots + bottom = mathMin(centerY + radius + distanceOption, chart.plotHeight); + for (pos = mathMax(0, centerY - radius - distanceOption); pos <= bottom; pos += labelHeight) { + slots.push(pos); + } + slotsLength = slots.length; - if (dataLabel && point.visible) { - _pos = dataLabel._pos; - if (_pos) { - dataLabel.attr(dataLabel._attr); - dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos); - dataLabel.moved = true; - } else if (dataLabel) { - dataLabel.attr({ y: -999 }); - } - } - }); - }; - seriesTypes.pie.prototype.alignDataLabel = noop; + /* Visualize the slots + if (!series.slotElements) { + series.slotElements = []; + } + if (i === 1) { + series.slotElements.forEach(function (elem) { + elem.destroy(); + }); + series.slotElements.length = 0; + } - /** - * Verify whether the data labels are allowed to draw, or we should run more translation and data - * label positioning to keep them inside the plot area. Returns true when data labels are ready - * to draw. - */ - seriesTypes.pie.prototype.verifyDataLabelOverflow = function (overflow) { + slots.forEach(function (pos, no) { + var slotX = series.getX(pos, i) + chart.plotLeft - (i ? 100 : 0), + slotY = pos + chart.plotTop; - var center = this.center, - options = this.options, - centerOption = options.center, - minSize = options.minSize || 80, - newSize = minSize, - ret; + if (!isNaN(slotX)) { + series.slotElements.push(chart.renderer.rect(slotX, slotY - 7, 100, labelHeight, 1) + .attr({ + 'stroke-width': 1, + stroke: 'silver', + fill: 'rgba(0,0,255,0.1)' + }) + .add()); + series.slotElements.push(chart.renderer.text('Slot '+ no, slotX, slotY + 4) + .attr({ + fill: 'silver' + }).add()); + } + }); + // */ - // Handle horizontal size and center - if (centerOption[0] !== null) { // Fixed center - newSize = mathMax(center[2] - mathMax(overflow[1], overflow[3]), minSize); + // if there are more values than available slots, remove lowest values + if (length > slotsLength) { + // create an array for sorting and ranking the points within each quarter + rankArr = [].concat(points); + rankArr.sort(sort); + j = length; + while (j--) { + rankArr[j].rank = j; + } + j = length; + while (j--) { + if (points[j].rank >= slotsLength) { + points.splice(j, 1); + } + } + length = points.length; + } - } else { // Auto center - newSize = mathMax( - center[2] - overflow[1] - overflow[3], // horizontal overflow - minSize - ); - center[0] += (overflow[3] - overflow[1]) / 2; // horizontal center - } + // The label goes to the nearest open slot, but not closer to the edge than + // the label's index. + for (j = 0; j < length; j++) { - // Handle vertical size and center - if (centerOption[1] !== null) { // Fixed center - newSize = mathMax(mathMin(newSize, center[2] - mathMax(overflow[0], overflow[2])), minSize); + point = points[j]; + labelPos = point.labelPos; - } else { // Auto center - newSize = mathMax( - mathMin( - newSize, - center[2] - overflow[0] - overflow[2] // vertical overflow - ), - minSize - ); - center[1] += (overflow[0] - overflow[2]) / 2; // vertical center - } + var closest = 9999, + distance, + slotI; - // If the size must be decreased, we need to run translate and drawDataLabels again - if (newSize < center[2]) { - center[2] = newSize; - center[3] = Math.min(relativeLength(options.innerSize || 0, newSize), newSize); // #3632 - this.translate(center); - each(this.points, function (point) { - if (point.dataLabel) { - point.dataLabel._pos = null; // reset - } - }); + // find the closest slot index + for (slotI = 0; slotI < slotsLength; slotI++) { + distance = mathAbs(slots[slotI] - labelPos[1]); + if (distance < closest) { + closest = distance; + slotIndex = slotI; + } + } - if (this.drawDataLabels) { - this.drawDataLabels(); - } - // Else, return true to indicate that the pie and its labels is within the plot area - } else { - ret = true; - } - return ret; - }; -} + // if that slot index is closer to the edges of the slots, move it + // to the closest appropriate slot + if (slotIndex < j && slots[j] !== null) { // cluster at the top + slotIndex = j; + } else if (slotsLength < length - j + slotIndex && slots[j] !== null) { // cluster at the bottom + slotIndex = slotsLength - length + j; + while (slots[slotIndex] === null) { // make sure it is not taken + slotIndex++; + } + } else { + // Slot is taken, find next free slot below. In the next run, the next slice will find the + // slot above these, because it is the closest one + while (slots[slotIndex] === null) { // make sure it is not taken + slotIndex++; + } + } -if (seriesTypes.column) { + usedSlots.push({ i: slotIndex, y: slots[slotIndex] }); + slots[slotIndex] = null; // mark as taken + } + // sort them in order to fill in from the top + usedSlots.sort(sort); + } - /** - * Override the basic data label alignment by adjusting for the position of the column - */ - seriesTypes.column.prototype.alignDataLabel = function (point, dataLabel, options, alignTo, isNew) { - var inverted = this.chart.inverted, - series = point.series, - dlBox = point.dlBox || point.shapeArgs, // data label box for alignment - below = pick(point.below, point.plotY > pick(this.translatedThreshold, series.yAxis.len)), // point.below is used in range series - inside = pick(options.inside, !!this.options.stacking); // draw it inside the box? + // now the used slots are sorted, fill them up sequentially + for (j = 0; j < length; j++) { - // Align to the column itself, or the top of it - if (dlBox) { // Area range uses this method but not alignTo - alignTo = merge(dlBox); + var slot, naturalY; - if (inverted) { - alignTo = { - x: series.yAxis.len - alignTo.y - alignTo.height, - y: series.xAxis.len - alignTo.x - alignTo.width, - width: alignTo.height, - height: alignTo.width - }; - } + point = points[j]; + labelPos = point.labelPos; + dataLabel = point.dataLabel; + visibility = point.visible === false ? HIDDEN : 'inherit'; + naturalY = labelPos[1]; - // Compute the alignment box - if (!inside) { - if (inverted) { - alignTo.x += below ? 0 : alignTo.width; - alignTo.width = 0; - } else { - alignTo.y += below ? alignTo.height : 0; - alignTo.height = 0; - } - } - } + if (distanceOption > 0) { + slot = usedSlots.pop(); + slotIndex = slot.i; + // if the slot next to currrent slot is free, the y value is allowed + // to fall back to the natural position + y = slot.y; + if ((naturalY > y && slots[slotIndex + 1] !== null) || + (naturalY < y && slots[slotIndex - 1] !== null)) { + y = mathMin(mathMax(0, naturalY), chart.plotHeight); + } - // When alignment is undefined (typically columns and bars), display the individual - // point below or above the point depending on the threshold - options.align = pick( - options.align, - !inverted || inside ? 'center' : below ? 'right' : 'left' - ); - options.verticalAlign = pick( - options.verticalAlign, - inverted || inside ? 'middle' : below ? 'top' : 'bottom' - ); + } else { + y = naturalY; + } - // Call the parent method - Series.prototype.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew); - }; -} + // get the x - use the natural x position for first and last slot, to prevent the top + // and botton slice connectors from touching each other on either side + x = options.justify ? + seriesCenter[0] + (i ? -1 : 1) * (radius + distanceOption) : + series.getX(y === centerY - radius - distanceOption || y === centerY + radius + distanceOption ? naturalY : y, i); + // Record the placement and visibility + dataLabel._attr = { + visibility: visibility, + align: labelPos[6] + }; + dataLabel._pos = { + x: x + options.x + + ({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0), + y: y + options.y - 10 // 10 is for the baseline (label vs text) + }; + dataLabel.connX = x; + dataLabel.connY = y; -/** - * Highcharts JS v4.1.9 (2015-10-07) - * Highcharts module to hide overlapping data labels. This module is included by default in Highmaps. - * - * (c) 2010-2014 Torstein Honsi - * - * License: www.highcharts.com/license - */ -/*global Highcharts, HighchartsAdapter */ -(function (H) { - var Chart = H.Chart, - each = H.each, - pick = H.pick, - addEvent = HighchartsAdapter.addEvent; + // Detect overflowing data labels + if (this.options.size === null) { + dataLabelWidth = dataLabel.width; + // Overflow left + if (x - dataLabelWidth < connectorPadding) { + overflow[3] = mathMax(mathRound(dataLabelWidth - x + connectorPadding), overflow[3]); - // Collect potensial overlapping data labels. Stack labels probably don't need to be - // considered because they are usually accompanied by data labels that lie inside the columns. - Chart.prototype.callbacks.push(function (chart) { - function collectAndHide() { - var labels = []; + // Overflow right + } else if (x + dataLabelWidth > plotWidth - connectorPadding) { + overflow[1] = mathMax(mathRound(x + dataLabelWidth - plotWidth + connectorPadding), overflow[1]); + } - each(chart.series, function (series) { - var dlOptions = series.options.dataLabels, - collections = series.dataLabelCollections || ['dataLabel']; // Range series have two collections - if ((dlOptions.enabled || series._hasPointLabels) && !dlOptions.allowOverlap && series.visible) { // #3866 - each(collections, function (coll) { - each(series.points, function (point) { - if (point[coll]) { - point[coll].labelrank = pick(point.labelrank, point.shapeArgs && point.shapeArgs.height); // #4118 - labels.push(point[coll]); - } - }); - }); - } - }); - chart.hideOverlappingLabels(labels); - } + // Overflow top + if (y - labelHeight / 2 < 0) { + overflow[0] = mathMax(mathRound(-y + labelHeight / 2), overflow[0]); - // Do it now ... - collectAndHide(); + // Overflow left + } else if (y + labelHeight / 2 > plotHeight) { + overflow[2] = mathMax(mathRound(y + labelHeight / 2 - plotHeight), overflow[2]); + } + } + } // for each point + } // for each half - // ... and after each chart redraw - addEvent(chart, 'redraw', collectAndHide); + // Do not apply the final placement and draw the connectors until we have verified + // that labels are not spilling over. + if (arrayMax(overflow) === 0 || this.verifyDataLabelOverflow(overflow)) { - }); + // Place the labels in the final position + this.placeDataLabels(); - /** - * Hide overlapping labels. Labels are moved and faded in and out on zoom to provide a smooth - * visual imression. - */ - Chart.prototype.hideOverlappingLabels = function (labels) { + // Draw the connectors + if (outside && connectorWidth) { + each(this.points, function (point) { + connector = point.connector; + labelPos = point.labelPos; + dataLabel = point.dataLabel; - var len = labels.length, - label, - i, - j, - label1, - label2, - isIntersecting, - pos1, - pos2, - padding, - intersectRect = function (x1, y1, w1, h1, x2, y2, w2, h2) { - return !( - x2 > x1 + w1 || - x2 + w2 < x1 || - y2 > y1 + h1 || - y2 + h2 < y1 - ); - }; - - // Mark with initial opacity - for (i = 0; i < len; i++) { - label = labels[i]; - if (label) { - label.oldOpacity = label.opacity; - label.newOpacity = 1; - } - } + if (dataLabel && dataLabel._pos && point.visible) { + visibility = dataLabel._attr.visibility; + x = dataLabel.connX; + y = dataLabel.connY; + connectorPath = softConnector ? [ + 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], + labelPos[2], labelPos[3], // second break + L, + labelPos[4], labelPos[5] // base + ] : [ + M, + x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label + L, + labelPos[2], labelPos[3], // second break + L, + labelPos[4], labelPos[5] // base + ]; - // Prevent a situation in a gradually rising slope, that each label - // will hide the previous one because the previous one always has - // lower rank. - labels.sort(function (a, b) { - return (b.labelrank || 0) - (a.labelrank || 0); - }); + if (connector) { + connector.animate({ d: connectorPath }); + connector.attr('visibility', visibility); - // Detect overlapping labels - for (i = 0; i < len; i++) { - label1 = labels[i]; + } else { + point.connector = connector = series.chart.renderer.path(connectorPath).attr({ + 'stroke-width': connectorWidth, + stroke: options.connectorColor || point.color || '#606060', + visibility: visibility + //zIndex: 0 // #2722 (reversed) + }) + .add(series.dataLabelsGroup); + } + } else if (connector) { + point.connector = connector.destroy(); + } + }); + } + } + }; + /** + * Perform the final placement of the data labels after we have verified that they + * fall within the plot area. + */ + seriesTypes.pie.prototype.placeDataLabels = function () { + each(this.points, function (point) { + var dataLabel = point.dataLabel, + _pos; - for (j = i + 1; j < len; ++j) { - label2 = labels[j]; - if (label1 && label2 && label1.placed && label2.placed && label1.newOpacity !== 0 && label2.newOpacity !== 0) { - pos1 = label1.alignAttr; - pos2 = label2.alignAttr; - padding = 2 * (label1.box ? 0 : label1.padding); // Substract the padding if no background or border (#4333) - isIntersecting = intersectRect( - pos1.x, - pos1.y, - label1.width - padding, - label1.height - padding, - pos2.x, - pos2.y, - label2.width - padding, - label2.height - padding - ); + if (dataLabel && point.visible) { + _pos = dataLabel._pos; + if (_pos) { + dataLabel.attr(dataLabel._attr); + dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos); + dataLabel.moved = true; + } else if (dataLabel) { + dataLabel.attr({ y: -9999 }); + } + } + }); + }; - if (isIntersecting) { - (label1.labelrank < label2.labelrank ? label1 : label2).newOpacity = 0; - } - } - } - } + seriesTypes.pie.prototype.alignDataLabel = noop; - // Hide or show - each(labels, function (label) { - var complete, - newOpacity; + /** + * Verify whether the data labels are allowed to draw, or we should run more translation and data + * label positioning to keep them inside the plot area. Returns true when data labels are ready + * to draw. + */ + seriesTypes.pie.prototype.verifyDataLabelOverflow = function (overflow) { - if (label) { - newOpacity = label.newOpacity; + var center = this.center, + options = this.options, + centerOption = options.center, + minSize = options.minSize || 80, + newSize = minSize, + ret; - if (label.oldOpacity !== newOpacity && label.placed) { + // Handle horizontal size and center + if (centerOption[0] !== null) { // Fixed center + newSize = mathMax(center[2] - mathMax(overflow[1], overflow[3]), minSize); - // Make sure the label is completely hidden to avoid catching clicks (#4362) - if (newOpacity) { - label.show(true); - } else { - complete = function () { - label.hide(); - }; - } + } else { // Auto center + newSize = mathMax( + center[2] - overflow[1] - overflow[3], // horizontal overflow + minSize + ); + center[0] += (overflow[3] - overflow[1]) / 2; // horizontal center + } - // Animate or set the opacity - label.alignAttr.opacity = newOpacity; - label[label.isOld ? 'animate' : 'attr'](label.alignAttr, null, complete); - - } - label.isOld = true; - } - }); - }; + // Handle vertical size and center + if (centerOption[1] !== null) { // Fixed center + newSize = mathMax(mathMin(newSize, center[2] - mathMax(overflow[0], overflow[2])), minSize); -}(Highcharts));/** - * TrackerMixin for points and graphs - */ + } else { // Auto center + newSize = mathMax( + mathMin( + newSize, + center[2] - overflow[0] - overflow[2] // vertical overflow + ), + minSize + ); + center[1] += (overflow[0] - overflow[2]) / 2; // vertical center + } -var TrackerMixin = Highcharts.TrackerMixin = { + // If the size must be decreased, we need to run translate and drawDataLabels again + if (newSize < center[2]) { + center[2] = newSize; + center[3] = Math.min(relativeLength(options.innerSize || 0, newSize), newSize); // #3632 + this.translate(center); + each(this.points, function (point) { + if (point.dataLabel) { + point.dataLabel._pos = null; // reset + } + }); - drawTrackerPoint: function () { - var series = this, - chart = series.chart, - pointer = chart.pointer, - cursor = series.options.cursor, - css = cursor && { cursor: cursor }, - onMouseOver = function (e) { - var target = e.target, - point; + if (this.drawDataLabels) { + this.drawDataLabels(); + } + // Else, return true to indicate that the pie and its labels is within the plot area + } else { + ret = true; + } + return ret; + }; + } - while (target && !point) { - point = target.point; - target = target.parentNode; - } + if (seriesTypes.column) { - if (point !== UNDEFINED && point !== chart.hoverPoint) { // undefined on graph in scatterchart - point.onMouseOver(e); - } - }; + /** + * Override the basic data label alignment by adjusting for the position of the column + */ + seriesTypes.column.prototype.alignDataLabel = function (point, dataLabel, options, alignTo, isNew) { + var inverted = this.chart.inverted, + series = point.series, + dlBox = point.dlBox || point.shapeArgs, // data label box for alignment + below = pick(point.below, point.plotY > pick(this.translatedThreshold, series.yAxis.len)), // point.below is used in range series + inside = pick(options.inside, !!this.options.stacking), // draw it inside the box? + overshoot; - // Add reference to the point - each(series.points, function (point) { - if (point.graphic) { - point.graphic.element.point = point; - } - if (point.dataLabel) { - point.dataLabel.element.point = point; - } - }); + // Align to the column itself, or the top of it + if (dlBox) { // Area range uses this method but not alignTo + alignTo = merge(dlBox); - // Add the event listeners, we need to do this only once - if (!series._hasTracking) { - each(series.trackerGroups, function (key) { - if (series[key]) { // we don't always have dataLabelsGroup - series[key] - .addClass(PREFIX + 'tracker') - .on('mouseover', onMouseOver) - .on('mouseout', function (e) { pointer.onTrackerMouseOut(e); }) - .css(css); - if (hasTouch) { - series[key].on('touchstart', onMouseOver); - } - } - }); - series._hasTracking = true; - } - }, + if (alignTo.y < 0) { + alignTo.height += alignTo.y; + alignTo.y = 0; + } + overshoot = alignTo.y + alignTo.height - series.yAxis.len; + if (overshoot > 0) { + alignTo.height -= overshoot; + } - /** - * Draw the tracker object that sits above all data labels and markers to - * track mouse events on the graph or points. For the line type charts - * the tracker uses the same graphPath, but with a greater stroke width - * for better control. - */ - drawTrackerGraph: function () { - var series = this, - options = series.options, - trackByArea = options.trackByArea, - trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath), - trackerPathLength = trackerPath.length, - chart = series.chart, - pointer = chart.pointer, - renderer = chart.renderer, - snap = chart.options.tooltip.snap, - tracker = series.tracker, - cursor = options.cursor, - css = cursor && { cursor: cursor }, - singlePoints = series.singlePoints, - singlePoint, - i, - onMouseOver = function () { - if (chart.hoverSeries !== series) { - series.onMouseOver(); - } - }, - /* - * Empirical lowest possible opacities for TRACKER_FILL for an element to stay invisible but clickable - * IE6: 0.002 - * IE7: 0.002 - * IE8: 0.002 - * IE9: 0.00000000001 (unlimited) - * IE10: 0.0001 (exporting only) - * FF: 0.00000000001 (unlimited) - * Chrome: 0.000001 - * Safari: 0.000001 - * Opera: 0.00000000001 (unlimited) - */ - TRACKER_FILL = 'rgba(192,192,192,' + (hasSVG ? 0.0001 : 0.002) + ')'; + if (inverted) { + alignTo = { + x: series.yAxis.len - alignTo.y - alignTo.height, + y: series.xAxis.len - alignTo.x - alignTo.width, + width: alignTo.height, + height: alignTo.width + }; + } - // Extend end points. A better way would be to use round linecaps, - // but those are not clickable in VML. - if (trackerPathLength && !trackByArea) { - i = trackerPathLength + 1; - while (i--) { - if (trackerPath[i] === M) { // extend left side - trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L); - } - if ((i && trackerPath[i] === M) || i === trackerPathLength) { // extend right side - trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]); - } - } - } + // Compute the alignment box + if (!inside) { + if (inverted) { + alignTo.x += below ? 0 : alignTo.width; + alignTo.width = 0; + } else { + alignTo.y += below ? alignTo.height : 0; + alignTo.height = 0; + } + } + } - // handle single points - for (i = 0; i < singlePoints.length; i++) { - singlePoint = singlePoints[i]; - trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY, - L, singlePoint.plotX + snap, singlePoint.plotY); - } - // draw the tracker - if (tracker) { - tracker.attr({ d: trackerPath }); - } else { // create + // When alignment is undefined (typically columns and bars), display the individual + // point below or above the point depending on the threshold + options.align = pick( + options.align, + !inverted || inside ? 'center' : below ? 'right' : 'left' + ); + options.verticalAlign = pick( + options.verticalAlign, + inverted || inside ? 'middle' : below ? 'top' : 'bottom' + ); - series.tracker = renderer.path(trackerPath) - .attr({ - 'stroke-linejoin': 'round', // #1225 - visibility: series.visible ? VISIBLE : HIDDEN, - stroke: TRACKER_FILL, - fill: trackByArea ? TRACKER_FILL : NONE, - 'stroke-width' : options.lineWidth + (trackByArea ? 0 : 2 * snap), - zIndex: 2 - }) - .add(series.group); + // Call the parent method + Series.prototype.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew); + }; + } - // The tracker is added to the series group, which is clipped, but is covered - // by the marker group. So the marker group also needs to capture events. - each([series.tracker, series.markerGroup], function (tracker) { - tracker.addClass(PREFIX + 'tracker') - .on('mouseover', onMouseOver) - .on('mouseout', function (e) { pointer.onTrackerMouseOut(e); }) - .css(css); - if (hasTouch) { - tracker.on('touchstart', onMouseOver); - } - }); - } - } -}; -/* End TrackerMixin */ + /** + * Highcharts module to hide overlapping data labels. This module is included in Highcharts. + */ + (function (H) { + var Chart = H.Chart, + each = H.each, + pick = H.pick, + addEvent = H.addEvent; -/** - * Add tracking event listener to the series group, so the point graphics - * themselves act as trackers - */ + // Collect potensial overlapping data labels. Stack labels probably don't need to be + // considered because they are usually accompanied by data labels that lie inside the columns. + Chart.prototype.callbacks.push(function (chart) { + function collectAndHide() { + var labels = []; -if (seriesTypes.column) { - ColumnSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint; -} + each(chart.series, function (series) { + var dlOptions = series.options.dataLabels, + collections = series.dataLabelCollections || ['dataLabel']; // Range series have two collections + if ((dlOptions.enabled || series._hasPointLabels) && !dlOptions.allowOverlap && series.visible) { // #3866 + each(collections, function (coll) { + each(series.points, function (point) { + if (point[coll]) { + point[coll].labelrank = pick(point.labelrank, point.shapeArgs && point.shapeArgs.height); // #4118 + labels.push(point[coll]); + } + }); + }); + } + }); + chart.hideOverlappingLabels(labels); + } -if (seriesTypes.pie) { - seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint; -} + // Do it now ... + collectAndHide(); -if (seriesTypes.scatter) { - ScatterSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint; -} + // ... and after each chart redraw + addEvent(chart, 'redraw', collectAndHide); -/* - * Extend Legend for item events - */ -extend(Legend.prototype, { + }); - setItemEvents: function (item, legendItem, useHTML, itemStyle, itemHiddenStyle) { - var legend = this; - // Set the events on the item group, or in case of useHTML, the item itself (#1249) - (useHTML ? legendItem : item.legendGroup).on('mouseover', function () { - item.setState(HOVER_STATE); - legendItem.css(legend.options.itemHoverStyle); - }) - .on('mouseout', function () { - legendItem.css(item.visible ? itemStyle : itemHiddenStyle); - item.setState(); - }) - .on('click', function (event) { - var strLegendItemClick = 'legendItemClick', - fnLegendItemClick = function () { - if (item.setVisible) { - item.setVisible(); - } - }; - - // Pass over the click/touch event. #4. - event = { - browserEvent: event - }; + /** + * Hide overlapping labels. Labels are moved and faded in and out on zoom to provide a smooth + * visual imression. + */ + Chart.prototype.hideOverlappingLabels = function (labels) { - // click the name or symbol - if (item.firePointEvent) { // point - item.firePointEvent(strLegendItemClick, event, fnLegendItemClick); - } else { - fireEvent(item, strLegendItemClick, event, fnLegendItemClick); - } - }); - }, + var len = labels.length, + label, + i, + j, + label1, + label2, + isIntersecting, + pos1, + pos2, + padding, + intersectRect = function (x1, y1, w1, h1, x2, y2, w2, h2) { + return !( + x2 > x1 + w1 || + x2 + w2 < x1 || + y2 > y1 + h1 || + y2 + h2 < y1 + ); + }; + + // Mark with initial opacity + for (i = 0; i < len; i++) { + label = labels[i]; + if (label) { + label.oldOpacity = label.opacity; + label.newOpacity = 1; + } + } - createCheckboxForItem: function (item) { - var legend = this; + // Prevent a situation in a gradually rising slope, that each label + // will hide the previous one because the previous one always has + // lower rank. + labels.sort(function (a, b) { + return (b.labelrank || 0) - (a.labelrank || 0); + }); - item.checkbox = createElement('input', { - type: 'checkbox', - checked: item.selected, - defaultChecked: item.selected // required by IE7 - }, legend.options.itemCheckboxStyle, legend.chart.container); + // Detect overlapping labels + for (i = 0; i < len; i++) { + label1 = labels[i]; - addEvent(item.checkbox, 'click', function (event) { - var target = event.target; - fireEvent(item.series || item, 'checkboxClick', { // #3712 - checked: target.checked, - item: item - }, - function () { - item.select(); - } - ); - }); - } -}); + for (j = i + 1; j < len; ++j) { + label2 = labels[j]; + if (label1 && label2 && label1.placed && label2.placed && label1.newOpacity !== 0 && label2.newOpacity !== 0) { + pos1 = label1.alignAttr; + pos2 = label2.alignAttr; + padding = 2 * (label1.box ? 0 : label1.padding); // Substract the padding if no background or border (#4333) + isIntersecting = intersectRect( + pos1.x, + pos1.y, + label1.width - padding, + label1.height - padding, + pos2.x, + pos2.y, + label2.width - padding, + label2.height - padding + ); -/* - * Add pointer cursor to legend itemstyle in defaultOptions - */ -defaultOptions.legend.itemStyle.cursor = 'pointer'; + if (isIntersecting) { + (label1.labelrank < label2.labelrank ? label1 : label2).newOpacity = 0; + } + } + } + } + // Hide or show + each(labels, function (label) { + var complete, + newOpacity; -/* - * Extend the Chart object with interaction - */ + if (label) { + newOpacity = label.newOpacity; -extend(Chart.prototype, { - /** - * Display the zoom button - */ - showResetZoom: function () { - var chart = this, - lang = defaultOptions.lang, - btnOptions = chart.options.chart.resetZoomButton, - theme = btnOptions.theme, - states = theme.states, - alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox'; - - this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, function () { chart.zoomOut(); }, theme, states && states.hover) - .attr({ - align: btnOptions.position.align, - title: lang.resetZoomTitle - }) - .add() - .align(btnOptions.position, false, alignTo); - - }, + if (label.oldOpacity !== newOpacity && label.placed) { - /** - * Zoom out to 1:1 - */ - zoomOut: function () { - var chart = this; - fireEvent(chart, 'selection', { resetSelection: true }, function () { - chart.zoom(); - }); - }, + // Make sure the label is completely hidden to avoid catching clicks (#4362) + if (newOpacity) { + label.show(true); + } else { + complete = function () { + label.hide(); + }; + } - /** - * Zoom into a given portion of the chart given by axis coordinates - * @param {Object} event - */ - zoom: function (event) { - var chart = this, - hasZoomed, - pointer = chart.pointer, - displayButton = false, - resetZoomButton; + // Animate or set the opacity + label.alignAttr.opacity = newOpacity; + label[label.isOld ? 'animate' : 'attr'](label.alignAttr, null, complete); + + } + label.isOld = true; + } + }); + }; + }(Highcharts)); + /** + * TrackerMixin for points and graphs + */ - // If zoom is called with no arguments, reset the axes - if (!event || event.resetSelection) { - each(chart.axes, function (axis) { - hasZoomed = axis.zoom(); - }); - } else { // else, zoom in on all axes - each(event.xAxis.concat(event.yAxis), function (axisData) { - var axis = axisData.axis, - isXAxis = axis.isXAxis; + var TrackerMixin = Highcharts.TrackerMixin = { - // don't zoom more than minRange - if (pointer[isXAxis ? 'zoomX' : 'zoomY'] || pointer[isXAxis ? 'pinchX' : 'pinchY']) { - hasZoomed = axis.zoom(axisData.min, axisData.max); - if (axis.displayBtn) { - displayButton = true; - } - } - }); - } - - // Show or hide the Reset zoom button - resetZoomButton = chart.resetZoomButton; - if (displayButton && !resetZoomButton) { - chart.showResetZoom(); - } else if (!displayButton && isObject(resetZoomButton)) { - chart.resetZoomButton = resetZoomButton.destroy(); - } - + drawTrackerPoint: function () { + var series = this, + chart = series.chart, + pointer = chart.pointer, + cursor = series.options.cursor, + css = cursor && { cursor: cursor }, + onMouseOver = function (e) { + var target = e.target, + point; - // Redraw - if (hasZoomed) { - chart.redraw( - pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation - ); - } - }, + while (target && !point) { + point = target.point; + target = target.parentNode; + } - /** - * Pan the chart by dragging the mouse across the pane. This function is called - * on mouse move, and the distance to pan is computed from chartX compared to - * the first chartX position in the dragging operation. - */ - pan: function (e, panning) { + if (point !== UNDEFINED && point !== chart.hoverPoint) { // undefined on graph in scatterchart + point.onMouseOver(e); + } + }; - var chart = this, - hoverPoints = chart.hoverPoints, - doRedraw; + // Add reference to the point + each(series.points, function (point) { + if (point.graphic) { + point.graphic.element.point = point; + } + if (point.dataLabel) { + point.dataLabel.element.point = point; + } + }); - // remove active points for shared tooltip - if (hoverPoints) { - each(hoverPoints, function (point) { - point.setState(); - }); - } + // Add the event listeners, we need to do this only once + if (!series._hasTracking) { + each(series.trackerGroups, function (key) { + if (series[key]) { // we don't always have dataLabelsGroup + series[key] + .addClass(PREFIX + 'tracker') + .on('mouseover', onMouseOver) + .on('mouseout', function (e) { + pointer.onTrackerMouseOut(e); + }) + .css(css); + if (hasTouch) { + series[key].on('touchstart', onMouseOver); + } + } + }); + series._hasTracking = true; + } + }, - each(panning === 'xy' ? [1, 0] : [1], function (isX) { // xy is used in maps - var mousePos = e[isX ? 'chartX' : 'chartY'], - axis = chart[isX ? 'xAxis' : 'yAxis'][0], - startPos = chart[isX ? 'mouseDownX' : 'mouseDownY'], - halfPointRange = (axis.pointRange || 0) / 2, - extremes = axis.getExtremes(), - newMin = axis.toValue(startPos - mousePos, true) + halfPointRange, - newMax = axis.toValue(startPos + chart[isX ? 'plotWidth' : 'plotHeight'] - mousePos, true) - halfPointRange, - goingLeft = startPos > mousePos; // #3613 + /** + * Draw the tracker object that sits above all data labels and markers to + * track mouse events on the graph or points. For the line type charts + * the tracker uses the same graphPath, but with a greater stroke width + * for better control. + */ + drawTrackerGraph: function () { + var series = this, + options = series.options, + trackByArea = options.trackByArea, + trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath), + trackerPathLength = trackerPath.length, + chart = series.chart, + pointer = chart.pointer, + renderer = chart.renderer, + snap = chart.options.tooltip.snap, + tracker = series.tracker, + cursor = options.cursor, + css = cursor && { cursor: cursor }, + singlePoints = series.singlePoints, + singlePoint, + i, + onMouseOver = function () { + if (chart.hoverSeries !== series) { + series.onMouseOver(); + } + }, + /* + * Empirical lowest possible opacities for TRACKER_FILL for an element to stay invisible but clickable + * IE6: 0.002 + * IE7: 0.002 + * IE8: 0.002 + * IE9: 0.00000000001 (unlimited) + * IE10: 0.0001 (exporting only) + * FF: 0.00000000001 (unlimited) + * Chrome: 0.000001 + * Safari: 0.000001 + * Opera: 0.00000000001 (unlimited) + */ + TRACKER_FILL = 'rgba(192,192,192,' + (hasSVG ? 0.0001 : 0.002) + ')'; - if (axis.series.length && - (goingLeft || newMin > mathMin(extremes.dataMin, extremes.min)) && - (!goingLeft || newMax < mathMax(extremes.dataMax, extremes.max))) { - axis.setExtremes(newMin, newMax, false, false, { trigger: 'pan' }); - doRedraw = true; - } + // Extend end points. A better way would be to use round linecaps, + // but those are not clickable in VML. + if (trackerPathLength && !trackByArea) { + i = trackerPathLength + 1; + while (i--) { + if (trackerPath[i] === M) { // extend left side + trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L); + } + if ((i && trackerPath[i] === M) || i === trackerPathLength) { // extend right side + trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]); + } + } + } - chart[isX ? 'mouseDownX' : 'mouseDownY'] = mousePos; // set new reference for next run - }); + // handle single points + for (i = 0; i < singlePoints.length; i++) { + singlePoint = singlePoints[i]; + trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY, + L, singlePoint.plotX + snap, singlePoint.plotY); + } - if (doRedraw) { - chart.redraw(false); - } - css(chart.container, { cursor: 'move' }); - } -}); + // draw the tracker + if (tracker) { + tracker.attr({ d: trackerPath }); + } else { // create -/* - * Extend the Point object with interaction - */ -extend(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. - */ - select: function (selected, accumulate) { - var point = this, - series = point.series, - chart = series.chart; + series.tracker = renderer.path(trackerPath) + .attr({ + 'stroke-linejoin': 'round', // #1225 + visibility: series.visible ? VISIBLE : HIDDEN, + stroke: TRACKER_FILL, + fill: trackByArea ? TRACKER_FILL : NONE, + 'stroke-width': options.lineWidth + (trackByArea ? 0 : 2 * snap), + zIndex: 2 + }) + .add(series.group); - selected = pick(selected, !point.selected); + // The tracker is added to the series group, which is clipped, but is covered + // by the marker group. So the marker group also needs to capture events. + each([series.tracker, series.markerGroup], function (tracker) { + tracker.addClass(PREFIX + 'tracker') + .on('mouseover', onMouseOver) + .on('mouseout', function (e) { + pointer.onTrackerMouseOut(e); + }) + .css(css); - // fire the event with the defalut handler - point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () { - point.selected = point.options.selected = selected; - series.options.data[inArray(point, series.data)] = point.options; + if (hasTouch) { + tracker.on('touchstart', onMouseOver); + } + }); + } + } + }; + /* End TrackerMixin */ - point.setState(selected && SELECT_STATE); - // unselect all other points unless Ctrl or Cmd + click - if (!accumulate) { - each(chart.getSelectedPoints(), function (loopPoint) { - if (loopPoint.selected && loopPoint !== point) { - loopPoint.selected = loopPoint.options.selected = false; - series.options.data[inArray(loopPoint, series.data)] = loopPoint.options; - loopPoint.setState(NORMAL_STATE); - loopPoint.firePointEvent('unselect'); - } - }); - } - }); - }, + /** + * Add tracking event listener to the series group, so the point graphics + * themselves act as trackers + */ - /** - * Runs on mouse over the point - * - * @param {Object} e The event arguments - * @param {Boolean} byProximity Falsy for kd points that are closest to the mouse, or to - * actually hovered points. True for other points in shared tooltip. - */ - onMouseOver: function (e, byProximity) { - var point = this, - series = point.series, - chart = series.chart, - tooltip = chart.tooltip, - hoverPoint = chart.hoverPoint; + if (seriesTypes.column) { + ColumnSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint; + } - if (chart.hoverSeries !== series) { - series.onMouseOver(); - } + if (seriesTypes.pie) { + seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint; + } - // set normal state to previous series - if (hoverPoint && hoverPoint !== point) { - hoverPoint.onMouseOut(); - } + if (seriesTypes.scatter) { + ScatterSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint; + } - if (point.series) { // It may have been destroyed, #4130 + /* + * Extend Legend for item events + */ + extend(Legend.prototype, { - // trigger the event - point.firePointEvent('mouseOver'); + setItemEvents: function (item, legendItem, useHTML, itemStyle, itemHiddenStyle) { + var legend = this; + // Set the events on the item group, or in case of useHTML, the item itself (#1249) + (useHTML ? legendItem : item.legendGroup).on('mouseover', function () { + item.setState(HOVER_STATE); + legendItem.css(legend.options.itemHoverStyle); + }) + .on('mouseout', function () { + legendItem.css(item.visible ? itemStyle : itemHiddenStyle); + item.setState(); + }) + .on('click', function (event) { + var strLegendItemClick = 'legendItemClick', + fnLegendItemClick = function () { + if (item.setVisible) { + item.setVisible(); + } + }; - // update the tooltip - if (tooltip && (!tooltip.shared || series.noSharedTooltip)) { - tooltip.refresh(point, e); - } + // Pass over the click/touch event. #4. + event = { + browserEvent: event + }; - // hover this - point.setState(HOVER_STATE); - if (!byProximity) { - chart.hoverPoint = point; - } - } - }, + // click the name or symbol + if (item.firePointEvent) { // point + item.firePointEvent(strLegendItemClick, event, fnLegendItemClick); + } else { + fireEvent(item, strLegendItemClick, event, fnLegendItemClick); + } + }); + }, - /** - * Runs on mouse out from the point - */ - onMouseOut: function () { - var chart = this.series.chart, - hoverPoints = chart.hoverPoints; + createCheckboxForItem: function (item) { + var legend = this; - this.firePointEvent('mouseOut'); + item.checkbox = createElement('input', { + type: 'checkbox', + checked: item.selected, + defaultChecked: item.selected // required by IE7 + }, legend.options.itemCheckboxStyle, legend.chart.container); - if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887, #2240 - this.setState(); - chart.hoverPoint = null; - } - }, + addEvent(item.checkbox, 'click', function (event) { + var target = event.target; + fireEvent( + item.series || item, + 'checkboxClick', + { // #3712 + checked: target.checked, + item: item + }, + function () { + item.select(); + } + ); + }); + } + }); - /** - * Import events from the series' and point's options. Only do it on - * demand, to save processing time on hovering. - */ - importEvents: function () { - if (!this.hasImportedEvents) { - var point = this, - options = merge(point.series.options.point, point.options), - events = options.events, - eventType; + /* + * Add pointer cursor to legend itemstyle in defaultOptions + */ + defaultOptions.legend.itemStyle.cursor = 'pointer'; - point.events = events; - for (eventType in events) { - addEvent(point, eventType, events[eventType]); - } - this.hasImportedEvents = true; + /* + * Extend the Chart object with interaction + */ - } - }, + extend(Chart.prototype, { + /** + * Display the zoom button + */ + showResetZoom: function () { + var chart = this, + lang = defaultOptions.lang, + btnOptions = chart.options.chart.resetZoomButton, + theme = btnOptions.theme, + states = theme.states, + alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox'; - /** - * Set the point's state - * @param {String} state - */ - setState: function (state, move) { - var point = this, - plotX = mathFloor(point.plotX), // #4586 - plotY = point.plotY, - series = point.series, - stateOptions = series.options.states, - markerOptions = defaultPlotOptions[series.type].marker && series.options.marker, - normalDisabled = markerOptions && !markerOptions.enabled, - markerStateOptions = markerOptions && markerOptions.states[state], - stateDisabled = markerStateOptions && markerStateOptions.enabled === false, - stateMarkerGraphic = series.stateMarkerGraphic, - pointMarker = point.marker || {}, - chart = series.chart, - radius, - halo = series.halo, - haloOptions, - newSymbol, - pointAttr; + function zoomOut() { + chart.zoomOut(); + } - state = state || NORMAL_STATE; // empty string - pointAttr = point.pointAttr[state] || series.pointAttr[state]; + this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, zoomOut, theme, states && states.hover) + .attr({ + align: btnOptions.position.align, + title: lang.resetZoomTitle + }) + .add() + .align(btnOptions.position, false, alignTo); - if ( - // already has this state - (state === point.state && !move) || - // selected points don't respond to hover - (point.selected && state !== SELECT_STATE) || - // series' state options is disabled - (stateOptions[state] && stateOptions[state].enabled === false) || - // general point marker's state options is disabled - (state && (stateDisabled || (normalDisabled && markerStateOptions.enabled === false))) || - // individual point marker's state options is disabled - (state && pointMarker.states && pointMarker.states[state] && pointMarker.states[state].enabled === false) // #1610 + }, - ) { - return; - } + /** + * Zoom out to 1:1 + */ + zoomOut: function () { + var chart = this; + fireEvent(chart, 'selection', { resetSelection: true }, function () { + chart.zoom(); + }); + }, - // apply hover styles to the existing point - if (point.graphic) { - radius = markerOptions && point.graphic.symbolName && pointAttr.r; - point.graphic.attr(merge( - pointAttr, - radius ? { // new symbol attributes (#507, #612) - x: plotX - radius, - y: plotY - radius, - width: 2 * radius, - height: 2 * radius - } : {} - )); + /** + * Zoom into a given portion of the chart given by axis coordinates + * @param {Object} event + */ + zoom: function (event) { + var chart = this, + hasZoomed, + pointer = chart.pointer, + displayButton = false, + resetZoomButton; - // Zooming in from a range with no markers to a range with markers - if (stateMarkerGraphic) { - stateMarkerGraphic.hide(); - } - } else { - // if a graphic is not applied to each point in the normal state, create a shared - // graphic for the hover state - if (state && markerStateOptions) { - radius = markerStateOptions.radius; - newSymbol = pointMarker.symbol || series.symbol; + // If zoom is called with no arguments, reset the axes + if (!event || event.resetSelection) { + each(chart.axes, function (axis) { + hasZoomed = axis.zoom(); + }); + } else { // else, zoom in on all axes + each(event.xAxis.concat(event.yAxis), function (axisData) { + var axis = axisData.axis, + isXAxis = axis.isXAxis; - // If the point has another symbol than the previous one, throw away the - // state marker graphic and force a new one (#1459) - if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) { - stateMarkerGraphic = stateMarkerGraphic.destroy(); - } + // don't zoom more than minRange + if (pointer[isXAxis ? 'zoomX' : 'zoomY'] || pointer[isXAxis ? 'pinchX' : 'pinchY']) { + hasZoomed = axis.zoom(axisData.min, axisData.max); + if (axis.displayBtn) { + displayButton = true; + } + } + }); + } - // 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 - ) - .attr(pointAttr) - .add(series.markerGroup); - stateMarkerGraphic.currentSymbol = newSymbol; - } + // Show or hide the Reset zoom button + resetZoomButton = chart.resetZoomButton; + if (displayButton && !resetZoomButton) { + chart.showResetZoom(); + } else if (!displayButton && isObject(resetZoomButton)) { + chart.resetZoomButton = resetZoomButton.destroy(); + } - // Move the existing graphic - } else { - stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054 - x: plotX - radius, - y: plotY - radius - }); - } - } - if (stateMarkerGraphic) { - stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? 'show' : 'hide'](); // #2450 - stateMarkerGraphic.element.point = point; // #4310 - } - } + // Redraw + if (hasZoomed) { + chart.redraw( + pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation + ); + } + }, - // Show me your halo - haloOptions = stateOptions[state] && stateOptions[state].halo; - if (haloOptions && haloOptions.size) { - if (!halo) { - series.halo = halo = chart.renderer.path() - .add(chart.seriesGroup); - } - halo.attr(extend({ - fill: Color(point.color || series.color).setOpacity(haloOptions.opacity).get() - }, haloOptions.attributes))[move ? 'animate' : 'attr']({ - d: point.haloPath(haloOptions.size) - }); - } else if (halo) { - halo.attr({ d: [] }); - } + /** + * Pan the chart by dragging the mouse across the pane. This function is called + * on mouse move, and the distance to pan is computed from chartX compared to + * the first chartX position in the dragging operation. + */ + pan: function (e, panning) { - point.state = state; - }, + var chart = this, + hoverPoints = chart.hoverPoints, + doRedraw; - haloPath: function (size) { - var series = this.series, - chart = series.chart, - plotBox = series.getPlotBox(), - inverted = chart.inverted; + // remove active points for shared tooltip + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } - return chart.renderer.symbols.circle( - plotBox.translateX + (inverted ? series.yAxis.len - this.plotY : this.plotX) - size, - plotBox.translateY + (inverted ? series.xAxis.len - this.plotX : this.plotY) - size, - size * 2, - size * 2 - ); - } -}); + each(panning === 'xy' ? [1, 0] : [1], function (isX) { // xy is used in maps + var mousePos = e[isX ? 'chartX' : 'chartY'], + axis = chart[isX ? 'xAxis' : 'yAxis'][0], + startPos = chart[isX ? 'mouseDownX' : 'mouseDownY'], + halfPointRange = (axis.pointRange || 0) / 2, + extremes = axis.getExtremes(), + newMin = axis.toValue(startPos - mousePos, true) + halfPointRange, + newMax = axis.toValue(startPos + chart[isX ? 'plotWidth' : 'plotHeight'] - mousePos, true) - halfPointRange, + goingLeft = startPos > mousePos; // #3613 -/* - * Extend the Series object with interaction - */ + if (axis.series.length && + (goingLeft || newMin > mathMin(extremes.dataMin, extremes.min)) && + (!goingLeft || newMax < mathMax(extremes.dataMax, extremes.max))) { + axis.setExtremes(newMin, newMax, false, false, { trigger: 'pan' }); + doRedraw = true; + } -extend(Series.prototype, { - /** - * Series mouse over handler - */ - onMouseOver: function () { - var series = this, - chart = series.chart, - hoverSeries = chart.hoverSeries; + chart[isX ? 'mouseDownX' : 'mouseDownY'] = mousePos; // set new reference for next run + }); - // set normal state to previous series - if (hoverSeries && hoverSeries !== series) { - hoverSeries.onMouseOut(); - } + if (doRedraw) { + chart.redraw(false); + } + css(chart.container, { cursor: 'move' }); + } + }); - // trigger the event, but to save processing time, - // only if defined - if (series.options.events.mouseOver) { - fireEvent(series, 'mouseOver'); - } + /* + * Extend the Point object with interaction + */ + extend(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. + */ + select: function (selected, accumulate) { + var point = this, + series = point.series, + chart = series.chart; - // hover this - series.setState(HOVER_STATE); - chart.hoverSeries = series; - }, + selected = pick(selected, !point.selected); - /** - * Series mouse out handler - */ - onMouseOut: function () { - // trigger the event only if listeners exist - var series = this, - options = series.options, - chart = series.chart, - tooltip = chart.tooltip, - hoverPoint = chart.hoverPoint; + // fire the event with the defalut handler + point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () { + point.selected = point.options.selected = selected; + series.options.data[inArray(point, series.data)] = point.options; - chart.hoverSeries = null; // #182, set to null before the mouseOut event fires + point.setState(selected && SELECT_STATE); - // trigger mouse out on the point, which must be in this series - if (hoverPoint) { - hoverPoint.onMouseOut(); - } + // unselect all other points unless Ctrl or Cmd + click + if (!accumulate) { + each(chart.getSelectedPoints(), function (loopPoint) { + if (loopPoint.selected && loopPoint !== point) { + loopPoint.selected = loopPoint.options.selected = false; + series.options.data[inArray(loopPoint, series.data)] = loopPoint.options; + loopPoint.setState(NORMAL_STATE); + loopPoint.firePointEvent('unselect'); + } + }); + } + }); + }, - // fire the mouse out event - if (series && options.events.mouseOut) { - fireEvent(series, 'mouseOut'); - } + /** + * Runs on mouse over the point + * + * @param {Object} e The event arguments + * @param {Boolean} byProximity Falsy for kd points that are closest to the mouse, or to + * actually hovered points. True for other points in shared tooltip. + */ + onMouseOver: function (e, byProximity) { + var point = this, + series = point.series, + chart = series.chart, + tooltip = chart.tooltip, + hoverPoint = chart.hoverPoint; + if (chart.hoverSeries !== series) { + series.onMouseOver(); + } - // hide the tooltip - if (tooltip && !options.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) { - tooltip.hide(); - } + // set normal state to previous series + if (hoverPoint && hoverPoint !== point) { + hoverPoint.onMouseOut(); + } - // set normal state - series.setState(); - }, + if (point.series) { // It may have been destroyed, #4130 - /** - * Set the state of the graph - */ - setState: function (state) { - var series = this, - options = series.options, - graph = series.graph, - stateOptions = options.states, - lineWidth = options.lineWidth, - attribs, - i = 0; + // trigger the event + point.firePointEvent('mouseOver'); - state = state || NORMAL_STATE; + // update the tooltip + if (tooltip && (!tooltip.shared || series.noSharedTooltip)) { + tooltip.refresh(point, e); + } - if (series.state !== state) { - series.state = state; + // hover this + point.setState(HOVER_STATE); + if (!byProximity) { + chart.hoverPoint = point; + } + } + }, - if (stateOptions[state] && stateOptions[state].enabled === false) { - return; - } + /** + * Runs on mouse out from the point + */ + onMouseOut: function () { + var chart = this.series.chart, + hoverPoints = chart.hoverPoints; - if (state) { - lineWidth = stateOptions[state].lineWidth || lineWidth + (stateOptions[state].lineWidthPlus || 0); // #4035 - } + this.firePointEvent('mouseOut'); - if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML - attribs = { - 'stroke-width': lineWidth - }; - // use attr because animate will cause any other animation on the graph to stop - graph.attr(attribs); - while (series['zoneGraph' + i]) { - series['zoneGraph' + i].attr(attribs); - i = i + 1; - } - } - } - }, + if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887, #2240 + this.setState(); + chart.hoverPoint = null; + } + }, - /** - * Set the visibility of the graph - * - * @param vis {Boolean} True to show the series, false to hide. If UNDEFINED, - * the visibility is toggled. - */ - setVisible: function (vis, redraw) { - var series = this, - chart = series.chart, - legendItem = series.legendItem, - showOrHide, - ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries, - oldVisibility = series.visible; + /** + * Import events from the series' and point's options. Only do it on + * demand, to save processing time on hovering. + */ + importEvents: function () { + if (!this.hasImportedEvents) { + var point = this, + options = merge(point.series.options.point, point.options), + events = options.events, + eventType; - // if called without an argument, toggle visibility - series.visible = vis = series.userOptions.visible = vis === UNDEFINED ? !oldVisibility : vis; - showOrHide = vis ? 'show' : 'hide'; + point.events = events; - // show or hide elements - each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker'], function (key) { - if (series[key]) { - series[key][showOrHide](); - } - }); + for (eventType in events) { + addEvent(point, eventType, events[eventType]); + } + this.hasImportedEvents = true; + } + }, - // hide tooltip (#1361) - if (chart.hoverSeries === series || (chart.hoverPoint && chart.hoverPoint.series) === series) { - series.onMouseOut(); - } + /** + * Set the point's state + * @param {String} state + */ + setState: function (state, move) { + var point = this, + plotX = mathFloor(point.plotX), // #4586 + plotY = point.plotY, + series = point.series, + stateOptions = series.options.states, + markerOptions = defaultPlotOptions[series.type].marker && series.options.marker, + normalDisabled = markerOptions && !markerOptions.enabled, + markerStateOptions = markerOptions && markerOptions.states[state], + stateDisabled = markerStateOptions && markerStateOptions.enabled === false, + stateMarkerGraphic = series.stateMarkerGraphic, + pointMarker = point.marker || {}, + chart = series.chart, + radius, + halo = series.halo, + haloOptions, + newSymbol, + pointAttr; + state = state || NORMAL_STATE; // empty string + pointAttr = point.pointAttr[state] || series.pointAttr[state]; - if (legendItem) { - chart.legend.colorizeItem(series, vis); - } + if ( + // already has this state + (state === point.state && !move) || + // selected points don't respond to hover + (point.selected && state !== SELECT_STATE) || + // series' state options is disabled + (stateOptions[state] && stateOptions[state].enabled === false) || + // general point marker's state options is disabled + (state && (stateDisabled || (normalDisabled && markerStateOptions.enabled === false))) || + // individual point marker's state options is disabled + (state && pointMarker.states && pointMarker.states[state] && pointMarker.states[state].enabled === false) // #1610 + ) { + return; + } - // rescale or adapt to resized chart - series.isDirty = true; - // in a stack, all other series are affected - if (series.options.stacking) { - each(chart.series, function (otherSeries) { - if (otherSeries.options.stacking && otherSeries.visible) { - otherSeries.isDirty = true; - } - }); - } + // apply hover styles to the existing point + if (point.graphic) { + radius = markerOptions && point.graphic.symbolName && pointAttr.r; + point.graphic.attr(merge( + pointAttr, + radius ? { // new symbol attributes (#507, #612) + x: plotX - radius, + y: plotY - radius, + width: 2 * radius, + height: 2 * radius + } : {} + )); - // show or hide linked series - each(series.linkedSeries, function (otherSeries) { - otherSeries.setVisible(vis, false); - }); + // Zooming in from a range with no markers to a range with markers + if (stateMarkerGraphic) { + stateMarkerGraphic.hide(); + } + } else { + // if a graphic is not applied to each point in the normal state, create a shared + // graphic for the hover state + if (state && markerStateOptions) { + radius = markerStateOptions.radius; + newSymbol = pointMarker.symbol || series.symbol; - if (ignoreHiddenSeries) { - chart.isDirtyBox = true; - } - if (redraw !== false) { - chart.redraw(); - } + // If the point has another symbol than the previous one, throw away the + // state marker graphic and force a new one (#1459) + if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) { + stateMarkerGraphic = stateMarkerGraphic.destroy(); + } - fireEvent(series, showOrHide); - }, + // 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 + ) + .attr(pointAttr) + .add(series.markerGroup); + stateMarkerGraphic.currentSymbol = newSymbol; + } - /** - * Show the graph - */ - show: function () { - this.setVisible(true); - }, + // Move the existing graphic + } else { + stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054 + x: plotX - radius, + y: plotY - radius + }); + } + } - /** - * Hide the graph - */ - hide: function () { - this.setVisible(false); - }, + if (stateMarkerGraphic) { + stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? 'show' : 'hide'](); // #2450 + stateMarkerGraphic.element.point = point; // #4310 + } + } + // Show me your halo + haloOptions = stateOptions[state] && stateOptions[state].halo; + if (haloOptions && haloOptions.size) { + if (!halo) { + series.halo = halo = chart.renderer.path() + .add(chart.seriesGroup); + } + halo.attr(extend(hasSVG ? { + fill: point.color || series.color, + 'fill-opacity': haloOptions.opacity + } : { + // Old IE doesn't take fill-opacity + fill: Color(point.color || series.color).setOpacity(haloOptions.opacity).get() + }, + haloOptions.attributes))[move ? 'animate' : 'attr']({ + d: point.haloPath(haloOptions.size) + }); + } else if (halo) { + halo.attr({ d: [] }); + } - /** - * Set the selected state of the graph - * - * @param selected {Boolean} True to select the series, false to unselect. If - * UNDEFINED, the selection state is toggled. - */ - select: function (selected) { - var series = this; - // if called without an argument, toggle - series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected; + point.state = state; + }, - if (series.checkbox) { - series.checkbox.checked = selected; - } + /** + * Get the circular path definition for the 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); - fireEvent(series, selected ? 'select' : 'unselect'); - }, + 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, + size * 2, + size * 2 + ); + } + }); - drawTracker: TrackerMixin.drawTrackerGraph -}); -// global variables -extend(Highcharts, { - - // Constructors - Color: Color, - Point: Point, - Tick: Tick, - Renderer: Renderer, - SVGElement: SVGElement, - SVGRenderer: SVGRenderer, - - // Various - arrayMin: arrayMin, - arrayMax: arrayMax, - charts: charts, - dateFormat: dateFormat, - error: error, - format: format, - pathAnim: pathAnim, - getOptions: getOptions, - hasBidiBug: hasBidiBug, - isTouchDevice: isTouchDevice, - setOptions: setOptions, - addEvent: addEvent, - removeEvent: removeEvent, - createElement: createElement, - discardElement: discardElement, - css: css, - each: each, - map: map, - merge: merge, - splat: splat, - extendClass: extendClass, - pInt: pInt, - svg: hasSVG, - canvas: useCanVG, - vml: !hasSVG && !useCanVG, - product: PRODUCT, - version: VERSION -}); + /* + * Extend the Series object with interaction + */ -}()); + extend(Series.prototype, { + /** + * Series mouse over handler + */ + onMouseOver: function () { + var series = this, + chart = series.chart, + hoverSeries = chart.hoverSeries; + + // set normal state to previous series + if (hoverSeries && hoverSeries !== series) { + hoverSeries.onMouseOut(); + } + + // trigger the event, but to save processing time, + // only if defined + if (series.options.events.mouseOver) { + fireEvent(series, 'mouseOver'); + } + + // hover this + series.setState(HOVER_STATE); + chart.hoverSeries = series; + }, + + /** + * Series mouse out handler + */ + onMouseOut: function () { + // trigger the event only if listeners exist + var series = this, + options = series.options, + chart = series.chart, + tooltip = chart.tooltip, + hoverPoint = chart.hoverPoint; + + chart.hoverSeries = null; // #182, set to null before the mouseOut event fires + + // trigger mouse out on the point, which must be in this series + if (hoverPoint) { + hoverPoint.onMouseOut(); + } + + // fire the mouse out event + if (series && options.events.mouseOut) { + fireEvent(series, 'mouseOut'); + } + + + // hide the tooltip + if (tooltip && !options.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) { + tooltip.hide(); + } + + // set normal state + series.setState(); + }, + + /** + * Set the state of the graph + */ + setState: function (state) { + var series = this, + options = series.options, + graph = series.graph, + stateOptions = options.states, + lineWidth = options.lineWidth, + attribs, + i = 0; + + state = state || NORMAL_STATE; + + if (series.state !== state) { + series.state = state; + + if (stateOptions[state] && stateOptions[state].enabled === false) { + return; + } + + if (state) { + lineWidth = stateOptions[state].lineWidth || lineWidth + (stateOptions[state].lineWidthPlus || 0); // #4035 + } + + if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML + attribs = { + 'stroke-width': lineWidth + }; + // use attr because animate will cause any other animation on the graph to stop + graph.attr(attribs); + while (series['zoneGraph' + i]) { + series['zoneGraph' + i].attr(attribs); + i = i + 1; + } + } + } + }, + + /** + * Set the visibility of the graph + * + * @param vis {Boolean} True to show the series, false to hide. If UNDEFINED, + * the visibility is toggled. + */ + setVisible: function (vis, redraw) { + var series = this, + chart = series.chart, + legendItem = series.legendItem, + showOrHide, + ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries, + oldVisibility = series.visible; + + // if called without an argument, toggle visibility + series.visible = vis = series.userOptions.visible = vis === UNDEFINED ? !oldVisibility : vis; + showOrHide = vis ? 'show' : 'hide'; + + // show or hide elements + each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker'], function (key) { + if (series[key]) { + series[key][showOrHide](); + } + }); + + + // hide tooltip (#1361) + if (chart.hoverSeries === series || (chart.hoverPoint && chart.hoverPoint.series) === series) { + series.onMouseOut(); + } + + + if (legendItem) { + chart.legend.colorizeItem(series, vis); + } + + + // rescale or adapt to resized chart + series.isDirty = true; + // in a stack, all other series are affected + if (series.options.stacking) { + each(chart.series, function (otherSeries) { + if (otherSeries.options.stacking && otherSeries.visible) { + otherSeries.isDirty = true; + } + }); + } + + // show or hide linked series + each(series.linkedSeries, function (otherSeries) { + otherSeries.setVisible(vis, false); + }); + + if (ignoreHiddenSeries) { + chart.isDirtyBox = true; + } + if (redraw !== false) { + chart.redraw(); + } + + fireEvent(series, showOrHide); + }, + + /** + * Show the graph + */ + show: function () { + this.setVisible(true); + }, + + /** + * Hide the graph + */ + hide: function () { + this.setVisible(false); + }, + + + /** + * Set the selected state of the graph + * + * @param selected {Boolean} True to select the series, false to unselect. If + * UNDEFINED, the selection state is toggled. + */ + select: function (selected) { + var series = this; + // if called without an argument, toggle + series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected; + + if (series.checkbox) { + series.checkbox.checked = selected; + } + + fireEvent(series, selected ? 'select' : 'unselect'); + }, + + drawTracker: TrackerMixin.drawTrackerGraph + }); + + // global variables + extend(Highcharts, { + + // Constructors + Color: Color, + Point: Point, + Tick: Tick, + Renderer: Renderer, + SVGElement: SVGElement, + SVGRenderer: SVGRenderer, + + // Various + arrayMin: arrayMin, + arrayMax: arrayMax, + charts: charts, + dateFormat: dateFormat, + error: error, + format: format, + pathAnim: pathAnim, + getOptions: getOptions, + hasBidiBug: hasBidiBug, + isTouchDevice: isTouchDevice, + setOptions: setOptions, + addEvent: addEvent, + removeEvent: removeEvent, + createElement: createElement, + discardElement: discardElement, + css: css, + each: each, + map: map, + merge: merge, + splat: splat, + stableSort: stableSort, + extendClass: extendClass, + pInt: pInt, + svg: hasSVG, + canvas: useCanVG, + vml: !hasSVG && !useCanVG, + product: PRODUCT, + version: VERSION + }); + + return Highcharts; +}));