app/assets/javascripts/highcharts.js in highcharts-rails-4.0.4.1 vs app/assets/javascripts/highcharts.js in highcharts-rails-4.1.0

- old
+ new

@@ -1,10 +1,10 @@ // ==ClosureCompiler== // @compilation_level SIMPLE_OPTIMIZATIONS /** - * @license Highcharts JS v4.0.4 (2014-09-02) + * @license Highcharts JS v4.1.0 (2015-02-16) * * (c) 2009-2014 Torstein Honsi * * License: www.highcharts.com/license */ @@ -31,11 +31,11 @@ // some variables userAgent = navigator.userAgent, isOpera = win.opera, - isIE = /msie/i.test(userAgent) && !isOpera, + isIE = /(msie|trident)/i.test(userAgent) && !isOpera, docMode8 = doc.documentMode === 8, isWebKit = /AppleWebKit/.test(userAgent), isFirefox = /Firefox/.test(userAgent), isTouchDevice = /(Mobile|Android|Windows Phone)/.test(userAgent), SVG_NS = 'http://www.w3.org/2000/svg', @@ -50,16 +50,15 @@ defaultOptions, dateFormat, // function globalAnimation, pathAnim, timeUnits, - error, noop = function () { return UNDEFINED; }, charts = [], chartCount = 0, PRODUCT = 'Highcharts', - VERSION = '4.0.4', + VERSION = '4.1.0', // some constants for frequently used strings DIV = 'div', ABSOLUTE = 'absolute', RELATIVE = 'relative', @@ -72,10 +71,11 @@ L = 'L', numRegex = /^[0-9]+$/, NORMAL_STATE = '', HOVER_STATE = 'hover', SELECT_STATE = 'select', + marginNames = ['plotTop', 'marginRight', 'marginBottom', 'plotLeft'], // Object for extending Axis AxisPlotLineOrBandExtension, // constants for attributes @@ -83,10 +83,11 @@ // 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, @@ -101,30 +102,29 @@ // lookup over the types and the associated classes seriesTypes = {}, Highcharts; // The Highcharts namespace -if (win.Highcharts) { - error(16, true); -} else { - Highcharts = win.Highcharts = {}; -} +Highcharts = win.Highcharts = win.Highcharts ? error(16, true) : {}; + +Highcharts.seriesTypes = seriesTypes; + /** * 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 */ -function extend(a, b) { +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. @@ -290,22 +290,22 @@ /** * Return the first value that is defined. Like MooTools' $.pick. */ -function 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; } } -} +}; /** * Set CSS on a given element * @param {Object} el * @param {Object} styles Style object with camel case property names @@ -355,37 +355,10 @@ extend(object.prototype, members); return object; } /** - * 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 - */ -function numberFormat(number, decimals, decPoint, thousandsSep) { - var externalFn = Highcharts.numberFormat, - lang = defaultOptions.lang, - // http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_number_format/ - n = +number || 0, - c = decimals === -1 ? - (n.toString().split('.')[1] || '').length : // preserve decimals - (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; - - return externalFn !== numberFormat ? - externalFn(number, decimals, decPoint, thousandsSep) : - (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) : "")); -} - -/** * Pad a string to a given length by adding 0 to the beginning * @param {Number} number * @param {Number} length */ function pad(number, length) { @@ -399,17 +372,22 @@ * @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. */ -function wrap(obj, method, func) { +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); }; +}; + + +function getTZOffset(timestamp) { + return ((getTimezoneOffset && getTimezoneOffset(timestamp)) || timezoneOffset || 0) * 60000; } /** * Based on http://www.php.net/manual/en/function.strftime.php * @param {String} format @@ -420,11 +398,11 @@ if (!defined(timestamp) || isNaN(timestamp)) { return 'Invalid date'; } format = pick(format, '%Y-%m-%d %H:%M:%S'); - var date = new Date(timestamp - timezoneOffset), + 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](), @@ -439,10 +417,11 @@ // 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(), // Month @@ -488,11 +467,11 @@ if (floatRegex.test(format)) { // float decimals = format.match(decRegex); decimals = decimals ? decimals[1] : -1; if (val !== null) { - val = numberFormat( + val = Highcharts.numberFormat( val, decimals, lang.decimalPoint, format.indexOf(',') > -1 ? lang.thousandsSep : '' ); @@ -565,12 +544,14 @@ * @param {Number} interval * @param {Array} multiples * @param {Number} magnitude * @param {Object} options */ -function normalizeTickInterval(interval, multiples, magnitude, allowDecimals) { - var normalized, i; +function normalizeTickInterval(interval, multiples, magnitude, allowDecimals, preventExceed) { + var normalized, + i, + retInterval = interval; // round to a tenfold of 1, 2, 2.5 or 5 magnitude = pick(magnitude, 1); normalized = interval / magnitude; @@ -588,20 +569,21 @@ } } // normalize the interval to the nearest multiple for (i = 0; i < multiples.length; i++) { - interval = multiples[i]; - if (normalized <= (multiples[i] + (multiples[i + 1] || multiples[i])) / 2) { + 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; } } // multiply back to the correct magnitude - interval *= magnitude; - - return interval; + retInterval *= magnitude; + + return retInterval; } /** * Utility method that sorts an object array and keeping the order of equal items. @@ -702,20 +684,20 @@ } /** * Provide error messages for debugging, with links to online explanation */ -error = function (code, stop) { +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); } -}; +} /** * Fix JS round off float errors * @param {Number} num */ @@ -743,14 +725,39 @@ second: 1000, minute: 60000, hour: 3600000, day: 24 * 3600000, week: 7 * 24 * 3600000, - month: 31 * 24 * 3600000, - year: 31556952000 + month: 28 * 24 * 3600000, + year: 364 * 24 * 3600000 }; + + /** + * Format a number and return a string based on input settings + * @param {Number} number The input number to format + * @param {Number} decimals The amount of decimals + * @param {String} 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 ? + (n.toString().split('.')[1] || '').length : // preserve decimals + (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; + + 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 @@ -1215,11 +1222,11 @@ // 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 = adapter.each, + each = Highcharts.each = adapter.each, grep = adapter.grep, offset = adapter.offset, map = adapter.map, addEvent = adapter.addEvent, removeEvent = adapter.removeEvent, @@ -1231,31 +1238,13 @@ /* **************************************************************************** * Handle the options * *****************************************************************************/ -var - -defaultLabelOptions = { - enabled: true, - // rotation: 0, - // align: 'center', - x: 0, - y: 15, - /*formatter: function () { - return this.value; - },*/ - style: { - color: '#606060', - cursor: 'default', - fontSize: '11px' - } -}; - defaultOptions = { colors: ['#7cb5ec', '#434348', '#90ed7d', '#f7a35c', - '#8085e9', '#f15c80', '#e4d354', '#8085e8', '#8d4653', '#91e8e1'], + '#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'], @@ -1263,17 +1252,17 @@ weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], decimalPoint: '.', numericSymbols: ['k', 'M', 'G', 'T', 'P', 'E'], // SI prefixes used in axis labels resetZoom: 'Reset zoom', resetZoomTitle: 'Reset zoom level 1:1', - thousandsSep: ',' + thousandsSep: ' ' }, global: { useUTC: true, //timezoneOffset: 0, - canvasToolsURL: 'http://code.highcharts.com/4.0.4/modules/canvas-tools.js', - VMLRadialGradientURL: 'http://code.highcharts.com/4.0.4/gfx/vml-radial-gradient.png' + canvasToolsURL: 'http://code.highcharts.com/4.1.0/modules/canvas-tools.js', + VMLRadialGradientURL: 'http://code.highcharts.com/4.1.0/gfx/vml-radial-gradient.png' }, chart: { //animation: true, //alignTicks: false, //reflow: true, @@ -1384,26 +1373,33 @@ } }, point: { events: {} }, - dataLabels: merge(defaultLabelOptions, { + dataLabels: { align: 'center', - //defer: true, - enabled: false, + // defer: true, + // enabled: false, formatter: function () { - return this.y === null ? '' : numberFormat(this.y, -1); + 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 - y: 0 + x: 0, + y: 0, // backgroundColor: undefined, // borderColor: undefined, // borderRadius: undefined, // borderWidth: undefined, - // padding: 3, + 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 @@ -1424,11 +1420,11 @@ marker: {} } }, stickyTracking: true, //tooltip: { - //pointFormat: '<span style="color:{series.color}">\u25CF</span> {series.name}: <b>{point.y}</b>' + //pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b>' //valueDecimals: null, //xDateFormat: '%A, %b %e, %Y', //valuePrefix: '', //ySuffix: '' //} @@ -1533,13 +1529,14 @@ 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:{series.color}">\u25CF</span> {series.name}: <b>{point.y}</b><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: { @@ -1588,26 +1585,35 @@ /** * Set the time methods globally based on the useUTC option. Time method can be either * local time or UTC (default). */ function setTimeMethods() { - var useUTC = defaultOptions.global.useUTC, + var globalOptions = defaultOptions.global, + useUTC = globalOptions.useUTC, GET = useUTC ? 'getUTC' : 'get', SET = useUTC ? 'setUTC' : 'set'; - Date = defaultOptions.global.Date || window.Date; - timezoneOffset = ((useUTC && defaultOptions.global.timezoneOffset) || 0) * 60000; - makeTime = useUTC ? Date.UTC : function (year, month, date, hours, minutes, seconds) { - return new Date( - year, - month, - pick(date, 1), - pick(hours, 0), - pick(minutes, 0), - pick(seconds, 0) - ).getTime(); + 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'; @@ -1776,11 +1782,11 @@ // Default base for animation opacity: 1, // For labels, these CSS properties are applied to the <text> node directly textProps: ['fontSize', 'fontWeight', 'fontFamily', 'color', - 'lineHeight', 'width', 'textDecoration', 'textShadow', 'HcTextStroke'], + 'lineHeight', 'width', 'textDecoration', 'textShadow'], /** * Initialize the SVG renderer * @param {Object} renderer * @param {String} nodeName @@ -1920,10 +1926,94 @@ elem.setAttribute(prop, 'url(' + renderer.url + '#' + id + ')'); } }, /** + * Apply a polyfill to the text-stroke CSS property, by copying the text element + * and apply strokes to the copy. + * + * docs: update default, document the polyfill and the limitations on hex colors and pixel values, document contrast pseudo-color + * TODO: + * - update defaults + */ + applyTextShadow: function (textShadow) { + var elem = this.element, + tspans, + hasContrast = textShadow.indexOf('contrast') !== -1, + // Safari suffers from the double display bug (#3649) + isSafari = userAgent.indexOf('Safari') > 0 && userAgent.indexOf('Chrome') === -1, + // IE10 and IE11 report textShadow in elem.style even though it doesn't work. Check + // this again with new IE release. + supports = elem.style.textShadow !== UNDEFINED && !isIE && !isSafari; + + // When the text shadow is set to contrast, use dark stroke for light text and vice versa + if (hasContrast) { + textShadow = textShadow.replace(/contrast/g, this.renderer.getContrast(elem.style.fill)); + } + + /* 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; + } + // */ + + // No reason to polyfill, we've got native support + if (supports) { + if (hasContrast) { // Apply the altered style + css(elem, { + textShadow: textShadow + }); + } + } else { + + // 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]; + + 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); + } + } + + // Create the clone and apply shadow properties + clone = tspan.cloneNode(1); + attr(clone, { + '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) { @@ -2118,11 +2208,13 @@ hasNew = true; } } } if (hasNew) { - textWidth = elemWrapper.textWidth = styles && styles.width && elem.nodeName.toLowerCase() === 'text' && pInt(styles.width); + textWidth = elemWrapper.textWidth = + (styles && styles.width && elem.nodeName.toLowerCase() === 'text' && pInt(styles.width)) || + elemWrapper.textWidth; // #3501 // Merge the new styles with the old ones if (oldStyles) { styles = extend( oldStyles, @@ -2248,10 +2340,13 @@ // 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; } // apply scale if (defined(scaleX) || defined(scaleY)) { transform.push('scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')'); @@ -2276,13 +2371,13 @@ * 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. + * 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, @@ -2344,35 +2439,41 @@ }, /** * Get the bounding box (width, height, x and y) for the element */ - getBBox: function () { + getBBox: function (reload) { var wrapper = this, - bBox = wrapper.bBox, + bBox,// = wrapper.bBox, renderer = wrapper.renderer, width, height, rotation = wrapper.rotation, element = wrapper.element, styles = wrapper.styles, rad = rotation * deg2rad, textStr = wrapper.textStr, cacheKey; - // 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 + (styles ? ('|' + styles.fontSize + '|' + styles.fontFamily) : ''); + if (textStr !== UNDEFINED) { - } //else { // This code block made demo/waterfall fail, related to buildText - // Caching all strings reduces rendering time by 4-5%. - // TODO: Check how this affects places where bBox is found on the element - //cacheKey = textStr + (styles ? ('|' + styles.fontSize + '|' + styles.fontFamily) : ''); - //} - if (cacheKey) { + // Properties that affect bounding box + cacheKey = ['', rotation || 0, styles && styles.fontSize, element.style.width].join(','); + + // 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; + + // Caching all strings reduces rendering time by 4-5%. + } else { + cacheKey = textStr + cacheKey; + } + } + + if (cacheKey && !reload) { bBox = renderer.cache[cacheKey]; } // No cache found if (!bBox) { @@ -2423,14 +2524,11 @@ bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad)); } } // Cache it - wrapper.bBox = bBox; - if (cacheKey) { - renderer.cache[cacheKey] = bBox; - } + renderer.cache[cacheKey] = bBox; } return bBox; }, /** @@ -2466,23 +2564,16 @@ }, /** * Add the element * @param {Object|Undefined} parent Can be an element, an element wrapper or undefined - * to append the element to the renderer.box. + * to append the element to the renderer.box. */ add: function (parent) { var renderer = this.renderer, - parentWrapper = parent || renderer, - parentNode = parentWrapper.element || renderer.box, - childNodes, element = this.element, - zIndex = this.zIndex, - otherElement, - otherZIndex, - i, inserted; if (parent) { this.parentGroup = parent; } @@ -2493,44 +2584,24 @@ // build formatted text if (this.textStr !== undefined) { renderer.buildText(this); } - // mark the container as having z indexed children - if (zIndex) { - parentWrapper.handleZ = true; - zIndex = pInt(zIndex); - } + // Mark as added + this.added = true; - // insert according to this and other elements' zIndex - if (parentWrapper.handleZ) { // this element or any of its siblings has a z index - childNodes = parentNode.childNodes; - for (i = 0; i < childNodes.length; i++) { - otherElement = childNodes[i]; - otherZIndex = attr(otherElement, 'zIndex'); - if (otherElement !== element && ( - // insert before the first element with a higher zIndex - pInt(otherZIndex) > zIndex || - // if no zIndex given, insert before the first element with a zIndex - (!defined(zIndex) && defined(otherZIndex)) - - )) { - parentNode.insertBefore(element, otherElement); - inserted = true; - break; - } - } + // 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(); } - // default: append at the end + // If zIndex is not handled, append at the end if (!inserted) { - parentNode.appendChild(element); + (parent ? parent.element : renderer.box).appendChild(element); } - // mark as added - this.added = true; - // fire an event for internal hooks if (this.onAdd) { this.onAdd(); } @@ -2747,13 +2818,57 @@ element.setAttribute(key, value); } else if (value) { this.colorGradient(value, key, element); } }, - zIndexSetter: function (value, key, element) { - element.setAttribute(key, value); - this[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, + i; + + if (defined(value)) { + element.setAttribute(key, value); // So we can read it for other elements in the group + this[key] = +value; + } + + // 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 (this.added) { + 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)) + + )) { + parentNode.insertBefore(element, otherElement); + inserted = true; + } + } + if (!inserted) { + parentNode.appendChild(element); + } + } + return inserted; }, _defaultSetter: function (value, key, element) { element.setAttribute(key, value); } }; @@ -2951,42 +3066,47 @@ hrefRegex, parentX = attr(textNode, 'x'), textStyles = wrapper.styles, width = wrapper.textWidth, textLineHeight = textStyles && textStyles.lineHeight, - textStroke = textStyles && textStyles.HcTextStroke, + 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, '>'); }; /// remove old text while (i--) { textNode.removeChild(childNodes[i]); } // Skip tspans, add text directly to text node. The forceTSpan is a hook // used in text outline hack. - if (!hasMarkup && !textStroke && textStr.indexOf(' ') === -1) { - textNode.appendChild(doc.createTextNode(textStr)); + if (!hasMarkup && !textShadow && !ellipsis && textStr.indexOf(' ') === -1) { + textNode.appendChild(doc.createTextNode(unescapeAngleBrackets(textStr))); return; // Complex strings, add more logic } else { styleRegex = /<.*style="([^"]+)".*>/; hrefRegex = /<.*href="(http[^"]+)".*>/; - if (width && !wrapper.added) { - this.box.appendChild(textNode); // attach it to the DOM to read offset width + if (tempParent) { + tempParent.appendChild(textNode); // attach it to the DOM to read offset width } if (hasMarkup) { lines = textStr .replace(/<(b|strong)>/g, '<span style="font-weight:bold">') @@ -3025,13 +3145,11 @@ if (hrefRegex.test(span) && !forExport) { // Not for export - #1529 attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"'); css(tspan, { cursor: 'pointer' }); } - span = (span.replace(/<(.|\n)*?>/g, '') || ' ') - .replace(/&lt;/g, '<') - .replace(/&gt;/g, '>'); + span = unescapeAngleBrackets(span.replace(/<(.|\n)*?>/g, '') || ' '); // Nested tags aren't supported, and cause crash in Safari (#1596) if (span !== ' ') { // add the text node @@ -3066,53 +3184,77 @@ 'dy', getLineHeight(tspan) ); } - // check width and apply soft breaks + /*if (width) { + renderer.breakText(wrapper, width); + }*/ + + // Check width and apply soft breaks or ellipsis if (width) { var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273 - hasWhiteSpace = spans.length > 1 || (words.length > 1 && textStyles.whiteSpace !== 'nowrap'), + hasWhiteSpace = spans.length > 1 || lineNo || (words.length > 1 && textStyles.whiteSpace !== 'nowrap'), tooLong, + wasTooLong, actualWidth, - hcHeight = textStyles.HcHeight, rest = [], dy = getLineHeight(tspan), softLineNo = 1, + rotation = wrapper.rotation, + wordStr = span, // for ellipsis + cursor = wordStr.length, // binary search cursor bBox; - while (hasWhiteSpace && (words.length || rest.length)) { - delete wrapper.bBox; // delete cache - bBox = wrapper.getBBox(); + while ((hasWhiteSpace || ellipsis) && (words.length || rest.length)) { + wrapper.rotation = 0; // discard rotation when computing box + bBox = wrapper.getBBox(true); actualWidth = bBox.width; // Old IE cannot measure the actualWidth for SVG elements (#2314) if (!hasSVG && renderer.forExport) { actualWidth = renderer.measureSpanWidth(tspan.firstChild.data, wrapper.styles); } tooLong = actualWidth > width; - if (!tooLong || words.length === 1) { // new line needed + + // 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 + '\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++; - if (hcHeight && softLineNo * dy > hcHeight) { - words = ['...']; - wrapper.attr('title', wrapper.textStr); - } else { - - tspan = doc.createElementNS(SVG_NS, 'tspan'); - attr(tspan, { - dy: dy, - x: parentX - }); - if (spanStyle) { // #390 - attr(tspan, 'style', spanStyle); - } - textNode.appendChild(tspan); + + 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 @@ -3121,20 +3263,75 @@ } if (words.length) { tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-'))); } } + if (wasTooLong) { + wrapper.attr('title', wrapper.textStr); + } + wrapper.rotation = rotation; } spanNo++; } } }); }); + if (tempParent) { + tempParent.removeChild(textNode); // attach it to the DOM to read offset width + } + + // Apply the text shadow + if (textShadow && wrapper.applyTextShadow) { + wrapper.applyTextShadow(textShadow); + } } }, + + + /* + 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; + + if (bBox.width > width) { + while (finalPos === undefined) { + textLength = node.getSubStringLength(0, pos); + + 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)) + }, + */ + + /** + * 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 ? '#000' : '#FFF'; + }, + /** * Create a button with preset states * @param {String} text * @param {Number} x * @param {Number} y @@ -3409,11 +3606,11 @@ }, /** * 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. + * 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; }, @@ -3721,10 +3918,11 @@ }).add(this.defs); wrapper = this.rect(x, y, width, height, 0).add(clipPath); wrapper.id = id; wrapper.clipPath = clipPath; + wrapper.count = 0; return wrapper; }, @@ -3799,32 +3997,46 @@ } fontSize = /px/.test(fontSize) ? pInt(fontSize) : /em/.test(fontSize) ? parseFloat(fontSize) * 12 : 12; // Empirical values found by comparing font size and bounding box height. // Applies to the default font family. http://jsfiddle.net/highcharts/7xvn7/ - var lineHeight = fontSize < 24 ? fontSize + 4 : mathRound(fontSize * 1.2), + var lineHeight = fontSize < 24 ? fontSize + 3 : mathRound(fontSize * 1.2), baseline = mathRound(lineHeight * 0.8); return { h: lineHeight, b: baseline, f: fontSize }; }, /** + * 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 + }; + }, + + /** * 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 + * 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. + * 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) { var renderer = this, @@ -3856,12 +4068,12 @@ function updateBoxSize() { var boxX, boxY, style = text.element.style; - bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) && text.textStr && - text.getBBox(); + 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; // update the label-scoped y offset baselineOffset = padding + renderer.fontMetrics(style && style.fontSize, text).b; @@ -3965,11 +4177,11 @@ wrapper.heightSetter = function (value) { height = value; }; wrapper.paddingSetter = function (value) { if (defined(value) && value !== padding) { - padding = value; + padding = wrapper.padding = value; updateTextPadding(); } }; wrapper.paddingLeftSetter = function (value) { if (defined(value) && value !== paddingLeft) { @@ -4112,11 +4324,14 @@ 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); return wrapper; }, @@ -4129,29 +4344,24 @@ * @return {Object} A hash containing values for x, y, width and height */ htmlGetBBox: function () { var wrapper = this, - element = wrapper.element, - bBox = wrapper.bBox; + element = wrapper.element; // faking getBBox in exported SVG in legacy IE - if (!bBox) { - // 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; - } - - bBox = wrapper.bBox = { - x: element.offsetLeft, - y: element.offsetTop, - width: element.offsetWidth, - height: element.offsetHeight - }; + // 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; } - return bBox; + return { + x: element.offsetLeft, + y: element.offsetTop, + width: element.offsetWidth, + height: element.offsetHeight + }; }, /** * VML override private method to update elements based on internal * properties based on SVG transform @@ -4170,11 +4380,12 @@ 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; + shadows = wrapper.shadows, + styles = wrapper.styles; // apply translate css(elem, { marginLeft: translateX, marginTop: translateY @@ -4218,11 +4429,11 @@ // Update textWidth if (width > textWidth && /[ \-]/.test(elem.textContent || elem.innerText)) { // #983, #1254 css(elem, { width: textWidth + PX, display: 'block', - whiteSpace: 'normal' + whiteSpace: (styles && styles.whiteSpace) || 'normal' // #3331 }); width = textWidth; } wrapper.getSpanCorrection(width, baseline, alignCorrection, rotation, align); @@ -4303,15 +4514,17 @@ x: mathRound(x), y: mathRound(y) }) .css({ position: ABSOLUTE, - whiteSpace: 'nowrap', fontFamily: this.style.fontFamily, fontSize: this.style.fontSize }); + // Keep the whiteSpace style outside the wrapper.styles collection + element.style.whiteSpace = 'nowrap'; + // Use the HTML specific .css method wrapper.css = wrapper.htmlCss; // This is specific for HTML within SVG if (renderer.isSVG) { @@ -4967,10 +5180,11 @@ isObj = isObject(x); // 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) { @@ -5551,27 +5765,18 @@ addLabel: function () { var tick = this, axis = tick.axis, options = axis.options, chart = axis.chart, - horiz = axis.horiz, categories = axis.categories, names = axis.names, pos = tick.pos, labelOptions = options.labels, - rotation = labelOptions.rotation, str, tickPositions = axis.tickPositions, - width = (horiz && categories && - !labelOptions.step && !labelOptions.staggerLines && - !labelOptions.rotation && - chart.plotWidth / tickPositions.length) || - (!horiz && (chart.margin[3] || chart.chartWidth * 0.33)), // #1580, #1931 isFirst = pos === tickPositions[0], isLast = pos === tickPositions[tickPositions.length - 1], - css, - attr, value = categories ? pick(categories[pos], names[pos], pos) : pos, label = tick.label, tickPositionInfo = tickPositions.info, @@ -5595,166 +5800,100 @@ dateTimeLabelFormat: dateTimeLabelFormat, value: axis.isLog ? correctFloat(lin2log(value)) : value }); // prepare CSS - css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) + PX }; + //css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) + PX }; // first call if (!defined(label)) { - attr = { - align: axis.labelAlign - }; - if (isNumber(rotation)) { - attr.rotation = rotation; - } - if (width && labelOptions.ellipsis) { - css.HcHeight = axis.len / tickPositions.length; - } tick.label = label = defined(str) && labelOptions.enabled ? chart.renderer.text( str, 0, 0, labelOptions.useHTML ) - .attr(attr) + //.attr(attr) // without position absolute, IE export sometimes is wrong - .css(extend(css, labelOptions.style)) + .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 - // Set the tick baseline and correct for rotation (#1764) - axis.tickBaseline = chart.renderer.fontMetrics(labelOptions.style.fontSize, label).b; - if (rotation && axis.side === 2) { - axis.tickBaseline *= mathCos(rotation * deg2rad); - } - - // update } else if (label) { - label.attr({ - text: str - }) - .css(css); + label.attr({ text: str }); } - tick.yOffset = label ? pick(labelOptions.y, axis.tickBaseline + (axis.side === 2 ? 8 : -(label.getBBox().height / 2))) : 0; }, /** * Get the offset height or width of the label */ getLabelSize: function () { - var label = this.label, - axis = this.axis; - return label ? - label.getBBox()[axis.horiz ? 'height' : 'width'] : + return this.label ? + this.label.getBBox()[this.axis.horiz ? 'height' : 'width'] : 0; }, /** - * Find how far the labels extend to the right and left of the tick's x position. Used for anti-collision - * detection with overflow logic. - */ - getLabelSides: function () { - var bBox = this.label.getBBox(), - axis = this.axis, - horiz = axis.horiz, - options = axis.options, - labelOptions = options.labels, - size = horiz ? bBox.width : bBox.height, - leftSide = horiz ? - labelOptions.x - size * { left: 0, center: 0.5, right: 1 }[axis.labelAlign] : - 0, - rightSide = horiz ? - size + leftSide : - size; - - return [leftSide, rightSide]; - }, - - /** * 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 (index, xy) { - var show = true, - axis = this.axis, - isFirst = this.isFirst, - isLast = this.isLast, - horiz = axis.horiz, - pxPos = horiz ? xy.x : xy.y, - reversed = axis.reversed, - tickPositions = axis.tickPositions, - sides = this.getLabelSides(), - leftSide = sides[0], - rightSide = sides[1], - axisLeft, - axisRight, - neighbour, - neighbourEdge, - line = this.label.line, - lineIndex = line || 0, - labelEdge = axis.labelEdge, - justifyLabel = axis.justifyLabels && (isFirst || isLast), - justifyToPlot; + handleOverflow: function (xy) { + var axis = this.axis, + pxPos = xy.x, + chartWidth = axis.chart.chartWidth, + spacing = axis.chart.spacing, + leftBound = pick(axis.labelLeft, spacing[3]), + rightBound = pick(axis.labelRight, 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, + leftPos, + rightPos, + textWidth; - // Hide it if it now overlaps the neighbour label - if (labelEdge[lineIndex] === UNDEFINED || pxPos + leftSide > labelEdge[lineIndex]) { - labelEdge[lineIndex] = pxPos + rightSide; + // 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 + factor * labelWidth; - } else if (!justifyLabel) { - show = false; - } + if (leftPos < leftBound) { + slotWidth -= leftBound - leftPos; + xy.x = leftBound; + label.attr({ align: 'left' }); + } else if (rightPos > rightBound) { + slotWidth -= rightPos - rightBound; + xy.x = rightBound; + label.attr({ align: 'right' }); + } - if (justifyLabel) { - justifyToPlot = axis.justifyToPlot; - axisLeft = justifyToPlot ? axis.pos : 0; - axisRight = justifyToPlot ? axisLeft + axis.len : axis.chart.chartWidth; - - // Find the firsth neighbour on the same line - do { - index += (isFirst ? 1 : -1); - neighbour = axis.ticks[tickPositions[index]]; - } while (tickPositions[index] && (!neighbour || !neighbour.label || neighbour.label.line !== line)); // #3044 - - neighbourEdge = neighbour && neighbour.label.xy && neighbour.label.xy.x + neighbour.getLabelSides()[isFirst ? 0 : 1]; - - if ((isFirst && !reversed) || (isLast && reversed)) { - // Is the label spilling out to the left of the plot area? - if (pxPos + leftSide < axisLeft) { - - // Align it to plot left - pxPos = axisLeft - leftSide; - - // Hide it if it now overlaps the neighbour label - if (neighbour && pxPos + rightSide > neighbourEdge) { - show = false; - } - } - - } else { - // Is the label spilling out to the right of the plot area? - if (pxPos + rightSide > axisRight) { - - // Align it to plot right - pxPos = axisRight - rightSide; - - // Hide it if it now overlaps the neighbour label - if (neighbour && pxPos + leftSide < neighbourEdge) { - show = false; - } - - } + if (labelWidth > slotWidth) { + textWidth = slotWidth; } + - // Set the modified x position of the label - xy.x = pxPos; + // 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)); } - return show; + + if (textWidth) { + label.css({ + width: textWidth, + textOverflow: 'ellipsis' + }); + } }, /** * Get the x and y position for ticks and labels */ @@ -5780,26 +5919,29 @@ */ getLabelPosition: function (x, y, label, horiz, labelOptions, tickmarkOffset, index, step) { var axis = this.axis, transA = axis.transA, reversed = axis.reversed, - staggerLines = axis.staggerLines; + 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; - x = x + labelOptions.x - (tickmarkOffset && horiz ? + x = x + labelOptions.x + rotCorr.x - (tickmarkOffset && horiz ? tickmarkOffset * transA * (reversed ? -1 : 1) : 0); - y = y + this.yOffset - (tickmarkOffset && !horiz ? + y = y + yOffset - (tickmarkOffset && !horiz ? tickmarkOffset * transA * (reversed ? 1 : -1) : 0); // Correct for staggered labels if (staggerLines) { - label.line = (index / (step || 1) % staggerLines); - y += label.line * (axis.labelOffset / staggerLines); + line = (index / (step || 1) % staggerLines); + y += line * (axis.labelOffset / staggerLines); } return { x: x, - y: y + y: mathRound(y) }; }, /** * Extendible method to return the path of the marker @@ -5843,11 +5985,11 @@ tickColor = options[tickPrefix + 'Color'], tickPosition = options[tickPrefix + 'Position'], gridLinePath, mark = tick.mark, markPath, - step = labelOptions.step, + step = /*axis.labelStep || */labelOptions.step, attribs, show = true, tickmarkOffset = axis.tickmarkOffset, xy = tick.getPosition(horiz, pos, tickmarkOffset, old), x = xy.x, @@ -5929,12 +6071,12 @@ if ((tick.isFirst && !tick.isLast && !pick(options.showFirstLabel, 1)) || (tick.isLast && !tick.isFirst && !pick(options.showLastLabel, 1))) { show = false; // Handle label overflow and show or hide accordingly - } else if (!axis.isRadial && !labelOptions.step && !labelOptions.rotation && !old && opacity !== 0) { - show = tick.handleOverflow(index, xy); + } else if (horiz && !axis.isRadial && !labelOptions.step && !labelOptions.rotation && !old && opacity !== 0) { + tick.handleOverflow(xy); } // apply step if (step && index % step) { // show those indices dividable by step @@ -5981,11 +6123,10 @@ */ render: function () { var plotLine = this, axis = plotLine.axis, horiz = axis.horiz, - halfPointRange = (axis.pointRange || 0) / 2, options = plotLine.options, optionsLabel = options.label, label = plotLine.label, width = options.width, to = options.to, @@ -6023,15 +6164,11 @@ }; if (dashStyle) { attribs.dashstyle = dashStyle; } } else if (isBand) { // plot band - - // keep within plot area - from = mathMax(from, axis.min - halfPointRange); - to = mathMin(to, axis.max + halfPointRange); - + path = axis.getPlotBandPath(from, to, options); if (color) { attribs.fill = color; } if (options.borderWidth) { @@ -6152,12 +6289,12 @@ /** * Create the path for a plot band */ getPlotBandPath: function (from, to) { - var toPath = this.getPlotLinePath(to), - path = this.getPlotLinePath(from); + var toPath = this.getPlotLinePath(to, null, null, true), + path = this.getPlotLinePath(from, null, null, true); if (path && toPath) { path.push( toPath[4], toPath[5], @@ -6228,13 +6365,13 @@ /** * Create a new axis object * @param {Object} chart * @param {Object} options */ -function Axis() { +var Axis = Highcharts.Axis = function () { this.init.apply(this, arguments); -} +}; Axis.prototype = { /** * Default options for the X axis - the Y axis has extended defaults @@ -6252,17 +6389,31 @@ week: '%e. %b', month: '%b \'%y', year: '%Y' }, endOnTick: false, - gridLineColor: '#C0C0C0', + gridLineColor: '#D8D8D8', // gridLineDashStyle: 'solid', // gridLineWidth: 0, // reversed: false, - labels: defaultLabelOptions, - // { step: null }, + 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, @@ -6345,13 +6496,13 @@ //x: dynamic, //verticalAlign: dynamic, //textAlign: dynamic, //rotation: 0, formatter: function () { - return numberFormat(this.total, -1); + return Highcharts.numberFormat(this.total, -1); }, - style: defaultLabelOptions.style + style: defaultPlotOptions.line.dataLabels.style } }, /** * These options extend the defaultOptions for left axes @@ -6382,24 +6533,26 @@ /** * 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 left axes + * These options extend the defaultOptions for top axes */ defaultTopAxisOptions: { labels: { + autoRotation: [-45], x: 0, y: -15 // overflow: undefined // staggerLines: null }, @@ -6474,19 +6627,16 @@ //axis.tickPositions = UNDEFINED; // array containing predefined positions // Tick intervals //axis.tickInterval = UNDEFINED; //axis.minorTickInterval = UNDEFINED; - axis.tickmarkOffset = (axis.categories && options.tickmarkPlacement === 'between' && - pick(options.tickInterval, 1) === 1) ? 0.5 : 0; // #3202 - + // Major ticks axis.ticks = {}; axis.labelEdge = []; // Minor ticks axis.minorTicks = {}; - //axis.tickAmount = UNDEFINED; // List of plotLines/Bands axis.plotLinesAndBands = []; // Alternate bands @@ -6617,21 +6767,21 @@ // 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 && numericSymbols[i] !== null) { - ret = numberFormat(value / multi, -1) + numericSymbols[i]; + ret = Highcharts.numberFormat(value / multi, -1) + numericSymbols[i]; } } } if (ret === UNDEFINED) { if (mathAbs(value) >= 10000) { // add thousands separators - ret = numberFormat(value, 0); + ret = Highcharts.numberFormat(value, 0); } else { // small numbers - ret = numberFormat(value, -1, UNDEFINED, ''); // #2466 + ret = Highcharts.numberFormat(value, -1, UNDEFINED, ''); // #2466 } } return ret; }, @@ -6719,11 +6869,11 @@ cvsOffset = 0, localA = old ? axis.oldTransA : axis.transA, localMin = old ? axis.oldMin : axis.min, returnValue, minPixelPadding = axis.minPixelPadding, - postTranslate = (axis.options.ordinal || (axis.isLog && handleLog)) && axis.lin2val; + postTranslate = (axis.postTranslate || (axis.isLog && handleLog)) && axis.lin2val; if (!localA) { localA = axis.transA; } @@ -6802,11 +6952,25 @@ x2, y2, cHeight = (old && chart.oldChartHeight) || chart.chartHeight, cWidth = (old && chart.oldChartWidth) || chart.chartWidth, skip, - transB = axis.transB; + 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; + }; translatedValue = pick(translatedValue, axis.translate(value, null, null, old)); x1 = x2 = mathRound(translatedValue + transB); y1 = y2 = mathRound(cHeight - translatedValue - transB); @@ -6814,20 +6978,15 @@ skip = true; } else if (axis.horiz) { y1 = axisTop; y2 = cHeight - axis.bottom; - if (x1 < axisLeft || x1 > axisLeft + axis.width) { - skip = true; - } + x1 = x2 = between(x1, axisLeft, axisLeft + axis.width); } else { x1 = axisLeft; x2 = cWidth - axis.right; - - if (y1 < axisTop || y1 > axisTop + axis.height) { - skip = true; - } + y1 = y2 = between(y1, axisTop, axisTop + axis.height); } return skip && !force ? null : chart.renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 1); }, @@ -6879,36 +7038,51 @@ tickPositions = axis.tickPositions, minorTickInterval = axis.minorTickInterval, minorTickPositions = [], pos, i, + min = axis.min, + max = axis.max, len; - if (axis.isLog) { - len = tickPositions.length; - for (i = 1; i < len; i++) { + // If minor ticks get too dense, they are hard to read, and may cause long running script. So we don't draw them. + if ((max - min) / minorTickInterval < axis.len / 3) { + + 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.getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true) + axis.getTimeTicks( + axis.normalizeTimeTickInterval(minorTickInterval), + min, + max, + options.startOfWeek + ) ); + + } else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314 + minorTickPositions = minorTickPositions.concat( + axis.getTimeTicks( + axis.normalizeTimeTickInterval(minorTickInterval), + axis.min, + axis.max, + options.startOfWeek + ) + ); + } else { + for (pos = axis.min + (tickPositions[0] - axis.min) % minorTickInterval; pos <= axis.max; pos += minorTickInterval) { + minorTickPositions.push(pos); + } } - } else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314 - minorTickPositions = minorTickPositions.concat( - axis.getTimeTicks( - axis.normalizeTimeTickInterval(minorTickInterval), - axis.min, - axis.max, - options.startOfWeek - ) - ); - if (minorTickPositions[0] < axis.min) { - minorTickPositions.shift(); - } - } else { - for (pos = axis.min + (tickPositions[0] - axis.min) % minorTickInterval; pos <= axis.max; pos += minorTickInterval) { - minorTickPositions.push(pos); - } } + + axis.trimTicks(minorTickPositions); // #3652 #3743 return minorTickPositions; }, /** * Adjust the min and max for the minimum range. Keep in mind that the series data is @@ -7017,24 +7191,26 @@ if (seriesPointRange > range) { // #1446 seriesPointRange = 0; } pointRange = mathMax(pointRange, seriesPointRange); - // 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 (!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 + ); - // 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 - ); + // 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 + ); + } // Set the closestPointRange if (!series.noSharedTooltip && defined(seriesClosestPointRange)) { closestPointRange = defined(closestPointRange) ? mathMin(closestPointRange, seriesClosestPointRange) : @@ -7068,32 +7244,31 @@ /** * Set the tick positions to round values and optionally extend the extremes * to the nearest tick */ - setTickPositions: function (secondPass) { + setTickInterval: function (secondPass) { var axis = this, chart = axis.chart, options = axis.options, - startOnTick = options.startOnTick, - endOnTick = options.endOnTick, isLog = axis.isLog, isDatetimeAxis = axis.isDatetimeAxis, isXAxis = axis.isXAxis, isLinked = axis.isLinked, - tickPositioner = axis.options.tickPositioner, maxPadding = options.maxPadding, minPadding = options.minPadding, length, linkedParentExtremes, tickIntervalOption = options.tickInterval, - minTickIntervalOption = options.minTickInterval, + minTickInterval, tickPixelIntervalOption = options.tickPixelInterval, - tickPositions, - keepTwoTicksOnly, categories = axis.categories; + if (!isDatetimeAxis && !categories && !isLinked) { + this.getTickAmount(); + } + // 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); @@ -7159,21 +7334,16 @@ tickPixelIntervalOption === axis.linkedParent.options.tickPixelInterval) { axis.tickInterval = 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) ); - // For squished axes, set only two ticks - if (!defined(tickIntervalOption) && axis.len < tickPixelIntervalOption && !this.isRadial && - !this.isLog && !categories && startOnTick && endOnTick) { - keepTwoTicksOnly = true; - axis.tickInterval /= 4; // tick extremes closer to the real values - } } // 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) { @@ -7199,151 +7369,225 @@ if (axis.pointRange) { axis.tickInterval = mathMax(axis.pointRange, axis.tickInterval); } // Before normalizing the tick interval, handle minimum tick interval. This applies only if tickInterval is not defined. - if (!tickIntervalOption && axis.tickInterval < minTickIntervalOption) { - axis.tickInterval = minTickIntervalOption; + minTickInterval = pick(options.minTickInterval, axis.isDatetimeAxis && axis.closestPointRange); + if (!tickIntervalOption && axis.tickInterval < minTickInterval) { + axis.tickInterval = minTickInterval; } // for linear axes, get magnitude and normalize the interval if (!isDatetimeAxis && !isLog) { // linear if (!tickIntervalOption) { axis.tickInterval = normalizeTickInterval( axis.tickInterval, null, getMagnitude(axis.tickInterval), - // If the tick interval is between 1 and 5 and the axis max is in the order of + // 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 > 1 && axis.tickInterval < 5 && axis.max > 1000 && axis.max < 9999)) + 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(); + } + + this.setTickPositions(); + }, + + /** + * Now we have computed the normalized tickInterval, get the tick positions + */ + setTickPositions: function () { + + var options = this.options, + tickPositions, + tickPositionsOption = options.tickPositions, + tickPositioner = options.tickPositioner, + startOnTick = options.startOnTick, + endOnTick = options.endOnTick, + single; + + // Set the tickmarkOffset + this.tickmarkOffset = (this.categories && options.tickmarkPlacement === 'between' && + this.tickInterval === 1) ? 0.5 : 0; // #3202 + + // get minorTickInterval - axis.minorTickInterval = options.minorTickInterval === 'auto' && axis.tickInterval ? - axis.tickInterval / 5 : options.minorTickInterval; + this.minorTickInterval = options.minorTickInterval === 'auto' && this.tickInterval ? + this.tickInterval / 5 : options.minorTickInterval; - // find the tick positions - axis.tickPositions = tickPositions = options.tickPositions ? - [].concat(options.tickPositions) : // Work on a copy (#1565) - (tickPositioner && tickPositioner.apply(axis, [axis.min, axis.max])); + // Find the tick positions + this.tickPositions = tickPositions = options.tickPositions && options.tickPositions.slice(); // Work on a copy (#1565) if (!tickPositions) { - // Too many ticks - if (!axis.ordinalPositions && (axis.max - axis.min) / axis.tickInterval > mathMax(2 * axis.len, 200)) { - error(19, true); - } - - if (isDatetimeAxis) { - tickPositions = axis.getTimeTicks( - axis.normalizeTimeTickInterval(axis.tickInterval, options.units), - axis.min, - axis.max, + if (this.isDatetimeAxis) { + tickPositions = this.getTimeTicks( + this.normalizeTimeTickInterval(this.tickInterval, options.units), + this.min, + this.max, options.startOfWeek, - axis.ordinalPositions, - axis.closestPointRange, + this.ordinalPositions, + this.closestPointRange, true ); - } else if (isLog) { - tickPositions = axis.getLogTickPositions(axis.tickInterval, axis.min, axis.max); + } else if (this.isLog) { + tickPositions = this.getLogTickPositions(this.tickInterval, this.min, this.max); } else { - tickPositions = axis.getLinearTickPositions(axis.tickInterval, axis.min, axis.max); + tickPositions = this.getLinearTickPositions(this.tickInterval, this.min, this.max); } - if (keepTwoTicksOnly) { - tickPositions.splice(1, tickPositions.length - 2); + this.tickPositions = tickPositions; + + // 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; + } } - axis.tickPositions = tickPositions; } - if (!isLinked) { + if (!this.isLinked) { // reset min/max or remove extremes based on start/end on tick - var roundedMin = tickPositions[0], - roundedMax = tickPositions[tickPositions.length - 1], - minPointOffset = axis.minPointOffset || 0, - singlePad; + this.trimTicks(tickPositions, startOnTick, endOnTick); - if (startOnTick) { - axis.min = roundedMin; - } else if (axis.min - minPointOffset > roundedMin) { - tickPositions.shift(); + // 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; - if (endOnTick) { - axis.max = roundedMax; - } else if (axis.max + minPointOffset < roundedMax) { - tickPositions.pop(); + if (!tickPositionsOption && !tickPositioner) { + this.adjustTickAmount(); } + } + }, - // If no tick are left, set one tick in the middle (#3195) - if (tickPositions.length === 0 && defined(roundedMin)) { - tickPositions.push((roundedMax + roundedMin) / 2); - } + /** + * 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(); + } - // 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 (tickPositions.length === 1) { - singlePad = mathAbs(axis.max) > 10e12 ? 1 : 0.001; // The lowest possible number to avoid extra padding on columns (#2619, #2846) - axis.min -= singlePad; - axis.max += singlePad; - } + if (endOnTick) { + this.max = roundedMax; + } else if (this.max + minPointOffset < roundedMax) { + tickPositions.pop(); } + + // If no tick are left, set one tick in the middle (#3195) + if (tickPositions.length === 0 && defined(roundedMin)) { + tickPositions.push((roundedMax + roundedMin) / 2); + } }, /** * Set the max ticks of either the x and y axis collection */ - setMaxTicks: function () { + getTickAmount: function () { + var others = {}, // Whether there is another axis to pair with this one + hasOther, + options = this.options, + tickAmount = options.tickAmount, + tickPixelInterval = options.tickPixelInterval; - var chart = this.chart, - maxTicks = chart.maxTicks || {}, - tickPositions = this.tickPositions, - key = this._maxTicksKey = [this.coll, this.pos, this.len].join('-'); + if (!defined(options.tickInterval) && this.len < tickPixelInterval && !this.isRadial && + !this.isLog && options.startOnTick && options.endOnTick) { + tickAmount = 2; + } - if (!this.isLinked && !this.isDatetimeAxis && tickPositions && tickPositions.length > (maxTicks[key] || 0) && this.options.alignTicks !== false) { - maxTicks[key] = tickPositions.length; + 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 (others[key]) { + hasOther = true; + } else { + others[key] = 1; + } + }); + + if (hasOther) { + // Add 1 because 4 tick intervals require 5 ticks (including first and last) + tickAmount = mathCeil(this.len / tickPixelInterval) + 1; + } } - chart.maxTicks = maxTicks; + + // 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; }, /** * When using multiple axes, adjust the number of ticks to match the highest * number of ticks in that group */ adjustTickAmount: function () { - var axis = this, - chart = axis.chart, - key = axis._maxTicksKey, - tickPositions = axis.tickPositions, - maxTicks = chart.maxTicks; + var tickInterval = this.tickInterval, + tickPositions = this.tickPositions, + tickAmount = this.tickAmount, + finalTickAmt = this.finalTickAmt, + currentTickAmount = tickPositions && tickPositions.length, + i, + len; - if (maxTicks && maxTicks[key] && !axis.isDatetimeAxis && !axis.categories && !axis.isLinked && - axis.options.alignTicks !== false && this.min !== UNDEFINED) { - var oldTickAmount = axis.tickAmount, - calculatedTickAmount = tickPositions.length, - tickAmount; + 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]; - // set the axis-level tickAmount to use below - axis.tickAmount = tickAmount = maxTicks[key]; + // We have too many ticks, run second pass to try to reduce ticks + } else if (currentTickAmount > tickAmount) { + this.tickInterval *= 2; + this.setTickPositions(); + } - if (calculatedTickAmount < tickAmount) { - while (tickPositions.length < tickAmount) { - tickPositions.push(correctFloat( - tickPositions[tickPositions.length - 1] + axis.tickInterval - )); - } - axis.transA *= (calculatedTickAmount - 1) / (tickAmount - 1); - axis.max = tickPositions[tickPositions.length - 1]; - + // 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); + } } - if (defined(oldTickAmount) && tickAmount !== oldTickAmount) { - axis.isDirty = true; - } + this.finalTickAmt = UNDEFINED; } }, /** * Set the scale based on data min and max, user set min and max or options @@ -7392,11 +7636,11 @@ // get data extremes if needed axis.getSeriesExtremes(); // get fixed positions based on tickInterval - axis.setTickPositions(); + axis.setTickInterval(); // record old values to decide whether a rescale is necessary later on (#540) axis.oldUserMin = axis.userMin; axis.oldUserMax = axis.userMax; @@ -7414,13 +7658,10 @@ for (i in stacks[type]) { stacks[type][i].cum = stacks[type][i].total; } } } - - // Set the maximum tick amount - axis.setMaxTicks(); }, /** * Set the extremes and optionally redraw * @param {Number} newMin @@ -7508,14 +7749,14 @@ left = pick(options.left, chart.plotLeft + offsetLeft), percentRegex = /%$/; // Check for percentage based input values if (percentRegex.test(height)) { - height = parseInt(height, 10) / 100 * chart.plotHeight; + height = parseFloat(height) / 100 * chart.plotHeight; } if (percentRegex.test(top)) { - top = parseInt(top, 10) / 100 * chart.plotHeight + chart.plotTop; + top = parseFloat(top) / 100 * chart.plotHeight + chart.plotTop; } // Expose basic values to use in Series object and navigator this.left = left; this.top = top; @@ -7583,10 +7824,165 @@ } return ret; }, /** + * 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 = defined(rotationOption) ? + [rotationOption] : + slotSize < 80 && !labelOptions.staggerLines && !labelOptions.step && labelOptions.autoRotation; + + 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 (rot && rot >= -90 && rot <= 90) { + + step = getStep(mathAbs(labelMetrics.h / mathSin(deg2rad * rot))); + + score = step + mathAbs(rot / 360); + + if (score < bestScore) { + bestScore = score; + rotation = rot; + newTickInterval = step; + } + } + }); + } + + } else { + newTickInterval = getStep(labelMetrics.h); + } + + this.autoRotation = autoRotation; + this.labelRotation = rotation; + + return newTickInterval; + }, + + 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, + slotWidth = this.slotWidth = (horiz && !labelOptions.step && !labelOptions.rotation && + ((this.staggerLines || 1) * chart.plotWidth) / tickPositions.length) || + (!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), + css, + labelLength = 0, + label, + i, + pos; + + // Set rotation option unless it is "auto", like in gauges + if (!isString(labelOptions.rotation)) { + attr.rotation = labelOptions.rotation; + } + + // Handle auto rotation on horizontal axis + if (this.autoRotation) { + + // 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; + } + + // Handle word-wrap or ellipsis on vertical axis + } else if (slotWidth) { + // For word-wrap or ellipsis + css = { width: innerWidth + PX, textOverflow: 'clip' }; + + // 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) { + if (this.len / tickPositions.length - 4 < label.getBBox().height) { + label.specCss = { textOverflow: 'ellipsis' }; + } + } + } + } + + + // 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, + textOverflow: 'ellipsis' + }; + } + + // Set the explicit or automatic label alignment + this.labelAlign = attr.align = labelOptions.align || this.autoLabelAlign(this.labelRotation); + + // Apply general and specific CSS + each(tickPositions, function (pos) { + var tick = ticks[pos], + label = tick && tick.label; + if (label) { + if (css) { + label.css(merge(css, label.specCss)); + } + delete label.specCss; + label.attr(attr); + tick.rotation = attr.rotation; + } + }); + + // TODO: Why not part of getLabelPosition? + this.tickRotCorr = renderer.rotCorr(labelMetrics.b, this.labelRotation || 0, this.side === 2); + }, + + /** * Render the tick labels to a preliminary position to get their sizes */ getOffset: function () { var axis = this, chart = axis.chart, @@ -7608,21 +8004,10 @@ labelOffsetPadded, axisOffset = chart.axisOffset, clipOffset = chart.clipOffset, directionFactor = [-1, 1, 1, -1][side], n, - i, - autoStaggerLines = 1, - maxStaggerLines = pick(labelOptions.maxStaggerLines, 5), - sortedPositions, - lastRight, - overlap, - pos, - bBox, - x, - w, - lineNo, lineHeightCorrection; // For reuse in Axis.render axis.hasData = hasData = (axis.hasVisibleSeries || (defined(axis.min) && defined(axis.max) && !!tickPositions)); axis.showAxis = showAxis = hasData || pick(options.showEmpty, true); @@ -7643,57 +8028,22 @@ .addClass(PREFIX + axis.coll.toLowerCase() + '-labels') .add(); } if (hasData || axis.isLinked) { - - // Set the explicit or automatic label alignment - axis.labelAlign = pick(labelOptions.align || axis.autoLabelAlign(labelOptions.rotation)); - + // 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 } }); - // Handle automatic stagger lines - if (axis.horiz && !axis.staggerLines && maxStaggerLines && !labelOptions.rotation) { - sortedPositions = axis.reversed ? [].concat(tickPositions).reverse() : tickPositions; - while (autoStaggerLines < maxStaggerLines) { - lastRight = []; - overlap = false; + axis.renderUnsquish(); - for (i = 0; i < sortedPositions.length; i++) { - pos = sortedPositions[i]; - bBox = ticks[pos].label && ticks[pos].label.getBBox(); - w = bBox ? bBox.width : 0; - lineNo = i % autoStaggerLines; - - if (w) { - x = axis.translate(pos); // don't handle log - if (lastRight[lineNo] !== UNDEFINED && x < lastRight[lineNo]) { - overlap = true; - } - lastRight[lineNo] = x + w; - } - } - if (overlap) { - autoStaggerLines++; - } else { - break; - } - } - - if (autoStaggerLines > 1) { - axis.staggerLines = autoStaggerLines; - } - } - - 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) { // get the highest offset @@ -7749,13 +8099,14 @@ } // handle automatic or user set offset axis.offset = directionFactor * pick(options.offset, axisOffset[side]); - lineHeightCorrection = side === 2 ? axis.tickBaseline : 0; + 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.tickBaseline + 8) : labelOptions.x) - lineHeightCorrection)); + (labelOffset && (directionFactor * (horiz ? pick(labelOptions.y, axis.tickRotCorr.y + 8) : labelOptions.x) - lineHeightCorrection)); axis.axisTitleMargin = pick(titleOffsetOption, labelOffsetPadded); axisOffset[side] = mathMax( axisOffset[side], axis.axisTitleMargin + titleOffset + directionFactor * axis.offset, @@ -7840,19 +8191,16 @@ /** * Render the axis */ render: function () { var axis = this, - horiz = axis.horiz, - reversed = axis.reversed, chart = axis.chart, renderer = chart.renderer, options = axis.options, isLog = axis.isLog, isLinked = axis.isLinked, tickPositions = axis.tickPositions, - sortedPositions, axisTitle = axis.axisTitle, ticks = axis.ticks, minorTicks = axis.minorTicks, alternateBands = axis.alternateBands, stackLabelOptions = options.stackLabels, @@ -7863,17 +8211,16 @@ hasRendered = chart.hasRendered, slideInTicks = hasRendered && defined(axis.oldMin) && !isNaN(axis.oldMin), hasData = axis.hasData, showAxis = axis.showAxis, from, - overflow = options.labels.overflow, - justifyLabels = axis.justifyLabels = horiz && overflow !== false, to; // Reset axis.labelEdge.length = 0; - axis.justifyToPlot = overflow === 'justify'; + //axis.justifyToPlot = overflow === 'justify'; + axis.overlap = false; // 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) { @@ -7901,24 +8248,12 @@ } // 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 - sortedPositions = tickPositions.slice(); - if ((horiz && reversed) || (!horiz && !reversed)) { - sortedPositions.reverse(); - } - if (justifyLabels) { - sortedPositions = sortedPositions.slice(1).concat([sortedPositions[0]]); - } - each(sortedPositions, function (pos, i) { + each(tickPositions, function (pos, i) { - // Reorganize the indices - if (justifyLabels) { - i = (i === sortedPositions.length - 1) ? 0 : i + 1; - } - // 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); @@ -7933,11 +8268,11 @@ } }); // 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) { + if (tickmarkOffset && (axis.min === 0 || axis.single)) { if (!ticks[-1]) { ticks[-1] = new Tick(axis, -1, null, true); } ticks[-1].render(-1); } @@ -8114,58 +8449,70 @@ }, /** * Draw the crosshair */ - drawCrosshair: function (e, point) { - if (!this.crosshair) { return; }// Do not draw crosshairs if you don't have too. + drawCrosshair: function (e, point) { // docs: Missing docs for Axis.crosshair. Also for properties. - if ((defined(point) || !pick(this.crosshair.snap, true)) === false) { - this.hideCrosshair(); - return; - } - var path, options = this.crosshair, animation = options.animation, - pos; + pos, + attribs, + categorized; + + if ( + // Disabled in options + !this.crosshair || + // snap + ((defined(point) || !pick(this.crosshair.snap, true)) === false) || + // Do not draw the crosshair if this axis is not part of the point + (defined(point) && pick(this.crosshair.snap, true) && (!point.series || point.series[this.isXAxis ? 'xAxis' : 'yAxis'] !== 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)) { - /*jslint eqeq: true*/ - pos = (this.chart.inverted != this.horiz) ? point.plotX : this.len - point.plotY; - /*jslint eqeq: false*/ - } + // 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.chart.inverted != this.horiz) ? point.plotX : this.len - point.plotY; + /*jslint eqeq: false*/ + } - if (this.isRadial) { - path = this.getPlotLinePath(this.isXAxis ? point.x : pick(point.stackY, point.y)); - } else { - path = this.getPlotLinePath(null, null, null, null, pos); - } + 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; - } + if (path === null) { + this.hideCrosshair(); + return; + } - // Draw the cross - if (this.cross) { - this.cross - .attr({ visibility: VISIBLE })[animation ? 'animate' : 'attr']({ d: path }, animation); - } else { - var attribs = { - 'stroke-width': options.width || 1, - stroke: options.color || '#C0C0C0', - zIndex: options.zIndex || 2 - }; - if (options.dashStyle) { - attribs.dashstyle = options.dashStyle; + // 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(); } - this.cross = this.chart.renderer.path(path).attr(attribs).add(); + } + }, /** * Hide the crosshair. */ @@ -8193,17 +8540,19 @@ var tickPositions = [], i, higherRanks = {}, useUTC = defaultOptions.global.useUTC, minYear, // used in months and years as a basis for Date.UTC() - minDate = new Date(min - timezoneOffset), + minDate = new Date(min - getTZOffset(min)), interval = normalizedInterval.unitRange, count = normalizedInterval.count; if (defined(min)) { // #1300 + minDate.setMilliseconds(interval >= timeUnits.second ? 0 : + count * mathFloor(minDate.getMilliseconds() / count)); // #3652, #3654 + if (interval >= timeUnits.second) { // second - minDate.setMilliseconds(0); minDate.setSeconds(interval >= timeUnits.minute ? 0 : count * mathFloor(minDate.getSeconds() / count)); } if (interval >= timeUnits.minute) { // minute @@ -8240,19 +8589,20 @@ } // get tick positions i = 1; - if (timezoneOffset) { - minDate = new Date(minDate.getTime() + timezoneOffset); + 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 ? timezoneOffset : minDate.getTimezoneOffset() * 60 * 1000) + (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); @@ -8508,11 +8858,11 @@ // The tooltip is initially hidden this.isHidden = true; - // create the label + // 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, @@ -8612,10 +8962,11 @@ point.setState(); }); } this.chart.hoverPoints = null; + this.chart.hoverSeries = null; } }, /** * Extendable method to get the anchor position of the tooltip @@ -8624,13 +8975,15 @@ getAnchor: function (points, mouseEvent) { var ret, chart = this.chart, inverted = chart.inverted, plotTop = chart.plotTop, + plotLeft = chart.plotLeft, plotX = 0, plotY = 0, - yAxis; + yAxis, + xAxis; points = splat(points); // Pie uses a special tooltipPos ret = points[0].tooltipPos; @@ -8647,11 +9000,12 @@ } // When shared, use the average position if (!ret) { each(points, function (point) { yAxis = point.series.yAxis; - plotX += point.plotX; + 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; @@ -8679,11 +9033,11 @@ ret = {}, swapped, first = ['y', chart.chartHeight, boxHeight, point.plotY + chart.plotTop], second = ['x', chart.chartWidth, boxWidth, point.plotX + chart.plotLeft], // The far side is right or bottom - preferFarSide = point.ttBelow || (chart.inverted && !point.negative) || (!chart.inverted && point.negative), + 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) { @@ -8763,25 +9117,20 @@ * 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), - series = items[0].series, s; // build the header - s = [tooltip.tooltipHeaderFormatter(items[0])]; + s = [tooltip.tooltipFooterHeaderFormatter(items[0])]; //#3397: abstraction to enable formatting of footer and header // build the values - each(items, function (item) { - series = item.series; - s.push((series.tooltipFormatter && series.tooltipFormatter(item)) || - item.point.tooltipFormatter(series.tooltipOptions.pointFormat)); - }); + s = s.concat(tooltip.bodyFormatter(items)); // footer - s.push(tooltip.options.footerFormat || ''); + s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true)); //#3397: abstraction to enable formatting of footer and header return s.join(''); }, /** @@ -8903,56 +9252,106 @@ point.plotX + chart.plotLeft, point.plotY + chart.plotTop ); }, + /** + * 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; + if (closestPointRange) { + date = dateFormat('%m-%d %H:%M:%S.%L', point.x); + for (n in timeUnits) { + + // 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; + + // 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; + } + + // 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; + } + + return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581 + }, + /** - * Format the header of the tooltip + * Format the footer/header of the tooltip + * #3397: abstraction to enable formatting of footer and header */ - tooltipHeaderFormatter: function (point) { - var series = point.series, + tooltipFooterHeaderFormatter: function (point, isFooter) { + var footOrHead = isFooter ? 'footer' : 'header', + series = point.series, tooltipOptions = series.tooltipOptions, - dateTimeLabelFormats = tooltipOptions.dateTimeLabelFormats, xDateFormat = tooltipOptions.xDateFormat, xAxis = series.xAxis, isDateTime = xAxis && xAxis.options.type === 'datetime' && isNumber(point.key), - headerFormat = tooltipOptions.headerFormat, - closestPointRange = xAxis && xAxis.closestPointRange, - n; + formatString = tooltipOptions[footOrHead+'Format']; - // Guess the best date format based on the closest point distance (#568) + // Guess the best date format based on the closest point distance (#568, #3418) if (isDateTime && !xDateFormat) { - if (closestPointRange) { - for (n in timeUnits) { - if (timeUnits[n] >= closestPointRange || - // If the point is placed every day at 23:59, we need to show - // the minutes as well. This logic only works for time units less than - // a day, since all higher time units are dividable by those. #2637. - (timeUnits[n] <= timeUnits.day && point.key % timeUnits[n] > 0)) { - xDateFormat = dateTimeLabelFormats[n]; - break; - } - } - } else { - xDateFormat = dateTimeLabelFormats.day; - } - - xDateFormat = xDateFormat || dateTimeLabelFormats.year; // #2546, 2581 - + xDateFormat = this.getXDateFormat(point, tooltipOptions, xAxis); } - // Insert the header date format if any + // Insert the footer date format if any if (isDateTime && xDateFormat) { - headerFormat = headerFormat.replace('{point.key}', '{point.key:' + xDateFormat + '}'); + formatString = formatString.replace('{point.key}', '{point.key:' + xDateFormat + '}'); } - return format(headerFormat, { + return format(formatString, { point: point, series: series }); - } + }, + + /** + * 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); + }); + } + }; var hoverChartIndex; // Global flag for touch support @@ -8998,11 +9397,11 @@ this.pinchDown = []; this.lastValidTouch = {}; if (Highcharts.Tooltip && options.tooltip.enabled) { chart.tooltip = new Tooltip(chart, options.tooltip); - this.followTouchMove = options.tooltip.followTouchMove; + this.followTouchMove = pick(options.tooltip.followTouchMove, true); } this.setDOMEvents(); }, @@ -9069,106 +9468,104 @@ }); return coordinates; }, /** - * Return the index in the tooltipPoints array, corresponding to pixel position in - * the plot area. - */ - getIndex: function (e) { - var chart = this.chart; - return chart.inverted ? - chart.plotHeight + chart.plotTop - e.chartY : - e.chartX - chart.plotLeft; - }, - - /** * 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) { + var pointer = this, chart = pointer.chart, series = chart.series, tooltip = chart.tooltip, + shared = tooltip ? tooltip.shared : false, followPointer, - point, - points, + //point, + //points, hoverPoint = chart.hoverPoint, hoverSeries = chart.hoverSeries, i, - j, + trueXkd, + trueX, + //j, distance = chart.chartWidth, - index = pointer.getIndex(e), - anchor; + rdistance = chart.chartWidth, + anchor, - // shared tooltip - if (tooltip && pointer.options.tooltip.shared && !(hoverSeries && hoverSeries.noSharedTooltip)) { - points = []; + kdpoints = [], + kdpoint; - // loop over all series and find the ones with points closest to the mouse - i = series.length; - for (j = 0; j < i; j++) { - if (series[j].visible && - series[j].options.enableMouseTracking !== false && - !series[j].noSharedTooltip && series[j].singularTooltips !== true && series[j].tooltipPoints.length) { - point = series[j].tooltipPoints[index]; - if (point && point.series) { // not a dummy point, #1544 - point._dist = mathAbs(index - point.clientX); - distance = mathMin(distance, point._dist); - points.push(point); - } + // 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 = []; } } - // remove furthest points - i = points.length; - while (i--) { - if (points[i]._dist > distance) { - points.splice(i, 1); + } + + if (shared || !hoverSeries) { + // Find nearest points on all series + each(series, function (s) { + // Skip hidden series + if (s.visible && pick(s.options.enableMouseTracking, true)) { + kdpoints.push(s.searchPoint(e)); } - } - // refresh the tooltip if necessary - if (points.length && (points[0].clientX !== pointer.hoverX)) { - tooltip.refresh(points, e); - pointer.hoverX = points[0].clientX; - } + }); + // Find absolute nearest point + each(kdpoints, function (p) { + if (p && defined(p.plotX) && defined(p.plotY)) { + if ((p.dist.distX < distance) || ((p.dist.distX === distance || p.series.kdDimensions > 1) && p.dist.distR < rdistance)) { + distance = p.dist.distX; + rdistance = p.dist.distR; + kdpoint = p; + } + } + //point = kdpoints[0]; + }); + } else { + kdpoint = hoverSeries ? hoverSeries.searchPoint(e) : UNDEFINED; } - // Separate tooltip and general mouse events - followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer; - if (hoverSeries && hoverSeries.tracker && !followPointer) { // #2584, #2830 - - // get the point - point = hoverSeries.tooltipPoints[index]; - - // a new point is hovered, refresh the tooltip - if (point && point !== hoverPoint) { - - // trigger the events - point.onMouseOver(e); - + // Refresh tooltip for kdpoint + if (kdpoint && tooltip && kdpoint !== hoverPoint) { + // Draw tooltip if necessary + if (shared && !kdpoint.series.noSharedTooltip) { + i = kdpoints.length; + trueXkd = kdpoint.plotX + kdpoint.series.xAxis.left; + while (i--) { + trueX = kdpoints[i].plotX + kdpoints[i].series.xAxis.left; + if (kdpoints[i].x !== kdpoint.x || trueX !== trueXkd || !defined(kdpoints[i].y) || (kdpoints[i].series.noSharedTooltip || false)) { + kdpoints.splice(i, 1); + } + } + tooltip.refresh(kdpoints, e); + each(kdpoints, function (point) { + point.onMouseOver(e); + }); + } else { + tooltip.refresh(kdpoint, e); + kdpoint.onMouseOver(e); } - - } else if (tooltip && followPointer && !tooltip.isHidden) { - anchor = tooltip.getAnchor([{}], e); - tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] }); + + // 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] }); + } } - - // 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); - } - - // Draw independent crosshairs + + // Crosshair each(chart.axes, function (axis) { - axis.drawCrosshair(e, pick(point, hoverPoint)); - }); + axis.drawCrosshair(e, pick(kdpoint, hoverPoint)); + }); + }, /** @@ -9185,20 +9582,27 @@ tooltipPoints = tooltip && tooltip.shared ? chart.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) { + // 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, allowMove); + } else { + axis.hideCrosshair(); + } + }); + } // Full reset } else { @@ -9357,11 +9761,12 @@ /** * On mouse up or touch end across the entire document, drop the selection. */ drop: function (e) { - var chart = this.chart, + var pointer = this, + chart = this.chart, hasPinched = this.hasPinched; if (this.selectionMarker) { var selectionData = { xAxis: [], @@ -9378,24 +9783,22 @@ // a selection has been made if (this.hasDragged || hasPinched) { // record each axis' min and max each(chart.axes, function (axis) { - if (axis.zoomEnabled) { + 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 (!isNaN(selectionMin) && !isNaN(selectionMax)) { // #859 - selectionData[axis.coll].push({ - axis: axis, - min: mathMin(selectionMin, selectionMax), // for reversed axes, - max: mathMax(selectionMin, selectionMax) - }); - runZoom = true; - } + 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)); @@ -9717,11 +10120,10 @@ pinch: function (e) { var self = this, chart = self.chart, pinchDown = self.pinchDown, - followTouchMove = self.followTouchMove, touches = e.touches, touchesLength = touches.length, lastValidTouch = self.lastValidTouch, hasZoom = self.hasZoom, selectionMarker = self.selectionMarker, @@ -9729,11 +10131,11 @@ fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, PREFIX + 'tracker') && chart.runTrackerClick) || self.runChartClick), clip = {}; // On touch devices, only proceed to trigger click if a handler is defined - if ((hasZoom || followTouchMove) && !fireClickEvent) { + if (hasZoom && !fireClickEvent) { e.preventDefault(); } // Normalize each touch map(touches, function (e) { @@ -9782,11 +10184,11 @@ // Scale and translate the groups to provide visual feedback during pinching self.scaleGroups(transform, clip); // Optionally move the tooltip on touchmove - if (!hasZoom && followTouchMove && touchesLength === 1) { + if (!hasZoom && self.followTouchMove && touchesLength === 1) { this.runPointActions(self.normalize(e)); } else if (self.res) { self.res = false; this.reset(false, 0); } @@ -9800,11 +10202,11 @@ if (e.touches.length === 1) { e = this.normalize(e); - if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { + if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop) && !chart.openMenu) { // Run mouse events and display tooltip etc this.runPointActions(e); this.pinch(e); @@ -9937,11 +10339,11 @@ */ init: function (chart, options) { var legend = this, itemStyle = options.itemStyle, - padding = pick(options.padding, 8), + padding, itemMarginTop = options.itemMarginTop || 0; this.options = options; if (!options.enabled) { @@ -9949,17 +10351,16 @@ } legend.itemStyle = itemStyle; legend.itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle); legend.itemMarginTop = itemMarginTop; - legend.padding = padding; + 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.lastLineHeight = 0; legend.symbolWidth = pick(options.symbolWidth, 16); legend.pages = []; // Render it @@ -10216,11 +10617,10 @@ // 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 } // 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; @@ -10276,10 +10676,43 @@ }); return allItems; }, /** + * 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[0] + options.verticalAlign[0] + options.layout[0]; + + if (this.display && !options.floating) { + + 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] + ); + } + }); + } + }, + + /** * 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 () { @@ -10330,24 +10763,23 @@ legend.allItems = allItems; legend.display = display = !!allItems.length; // render the items + legend.lastLineHeight = 0; each(allItems, function (item) { legend.renderItem(item); }); - // Draw the border - legendWidth = options.width || legend.offsetWidth; + // 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) { - legendWidth += padding; - legendHeight += padding; if (!box) { legend.box = box = renderer.rect( 0, 0, @@ -10682,17 +11114,22 @@ /** * The chart class * @param {Object} options * @param {Function} callback Function to run when the chart has loaded */ -function Chart() { +var Chart = Highcharts.Chart = function () { this.init.apply(this, arguments); -} +}; Chart.prototype = { /** + * Hook for modules + */ + callbacks: [], + + /** * Initialize the chart */ init: function (userOptions, callback) { // Handle regular options @@ -10821,22 +11258,10 @@ y >= 0 && y <= this.plotHeight; }, /** - * Adjust all axes tick amounts - */ - adjustTickAmounts: function () { - if (this.options.chart.alignTicks !== false) { - each(this.axes, function (axis) { - axis.adjustTickAmount(); - }); - } - this.maxTicks = null; - }, - - /** * Redraw legend, axes or series based on updated data * * @param {Boolean|Object} animation Whether to apply animation, and optionally animation * configuration */ @@ -10922,12 +11347,10 @@ // set axes scales each(axes, function (axis) { axis.setScale(); }); } - - chart.adjustTickAmounts(); } chart.getMargins(); // #3098 if (hasCartesianSeries) { @@ -11055,12 +11478,10 @@ optionsArray = xAxisOptions.concat(yAxisOptions); each(optionsArray, function (axisOptions) { axis = new Axis(chart, axisOptions); }); - - chart.adjustTickAmounts(); }, /** * Get the currently selected points from all series @@ -11350,102 +11771,67 @@ 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; }, /** * 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 () { + getMargins: function (skipAxes) { var chart = this, spacing = chart.spacing, - axisOffset, - legend = chart.legend, margin = chart.margin, - legendOptions = chart.options.legend, - legendMargin = pick(legendOptions.margin, 20), - legendX = legendOptions.x, - legendY = legendOptions.y, - align = legendOptions.align, - verticalAlign = legendOptions.verticalAlign, titleOffset = chart.titleOffset; chart.resetMargins(); - axisOffset = chart.axisOffset; // 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 - if (legend.display && !legendOptions.floating) { - if (align === 'right') { // horizontal alignment handled first - if (!defined(margin[1])) { - chart.marginRight = mathMax( - chart.marginRight, - legend.legendWidth - legendX + legendMargin + spacing[1] - ); - } - } else if (align === 'left') { - if (!defined(margin[3])) { - chart.plotLeft = mathMax( - chart.plotLeft, - legend.legendWidth + legendX + legendMargin + spacing[3] - ); - } + chart.legend.adjustMargins(margin, spacing); - } else if (verticalAlign === 'top') { - if (!defined(margin[0])) { - chart.plotTop = mathMax( - chart.plotTop, - legend.legendHeight + legendY + legendMargin + spacing[0] - ); - } - - } else if (verticalAlign === 'bottom') { - if (!defined(margin[2])) { - chart.marginBottom = mathMax( - chart.marginBottom, - legend.legendHeight - legendY + legendMargin + spacing[2] - ); - } - } - } - // adjust for scroller if (chart.extraBottomMargin) { chart.marginBottom += chart.extraBottomMargin; } if (chart.extraTopMargin) { chart.plotTop += chart.extraTopMargin; } + if (!skipAxes) { + this.getAxisMargins(); + } + }, + getAxisMargins: function () { + + 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) { axis.getOffset(); }); } - - if (!defined(margin[3])) { - chart.plotLeft += axisOffset[3]; - } - if (!defined(margin[0])) { - chart.plotTop += axisOffset[0]; - } - if (!defined(margin[2])) { - chart.marginBottom += axisOffset[2]; - } - if (!defined(margin[1])) { - chart.marginRight += axisOffset[1]; - } + // Add the axis offsets + each(marginNames, function (m, side) { + if (!defined(margin[side])) { + chart[m] += axisOffset[side]; + } + }); + chart.setChartSize(); }, /** @@ -11639,18 +12025,15 @@ /** * Initial margins before auto size margins are applied */ resetMargins: function () { - var chart = this, - spacing = chart.spacing, - margin = chart.margin; + var chart = this; - chart.plotTop = pick(margin[0], spacing[0]); - chart.marginRight = pick(margin[1], spacing[1]); - chart.marginBottom = pick(margin[2], spacing[2]); - chart.plotLeft = pick(margin[3], spacing[3]); + 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]; }, /** @@ -11838,13 +12221,10 @@ * Render series for the chart */ renderSeries: function () { each(this.series, function (serie) { serie.translate(); - if (serie.setTooltipPoints) { - serie.setTooltipPoints(); - } serie.render(); }); }, /** @@ -11881,37 +12261,53 @@ */ render: function () { var chart = this, axes = chart.axes, renderer = chart.renderer, - options = chart.options; + options = chart.options, + tempWidth, + tempHeight, + redoHorizontal, + redoVertical; // Title chart.setTitle(); // Legend chart.legend = new Legend(chart, options.legend); chart.getStacks(); // render stacks + // Get chart margins + chart.getMargins(true); + chart.setChartSize(); + + // 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 + // Get margins by pre-rendering axes - // set axes scales each(axes, function (axis) { axis.setScale(); }); + chart.getAxisMargins(); - chart.getMargins(); + // If the plot area size has changed significantly, calculate tick positions again + redoHorizontal = tempWidth / chart.plotWidth > 1.2; + redoVertical = tempHeight / chart.plotHeight > 1.1; - chart.maxTicks = null; // reset for second pass - each(axes, function (axis) { - axis.setTickPositions(true); // update to reflect the new margins - axis.setMaxTicks(); - }); - chart.adjustTickAmounts(); - chart.getMargins(); // second pass to check for new labels + if (redoHorizontal || redoVertical) { + 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 + } // Draw the borders and backgrounds chart.drawChartBox(); @@ -12110,18 +12506,20 @@ // run callbacks if (callback) { callback.apply(chart, [chart]); } each(chart.callbacks, function (fn) { - fn.apply(chart, [chart]); + 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 + // If the chart was rendered outside the top container, put it back in (#3679) chart.cloneRenderTo(true); - - fireEvent(chart, 'load'); }, /** * Creates arrays for spacing and margin from given options. @@ -12135,13 +12533,10 @@ pick(options[target + 'Bottom'], tArray[2]), pick(options[target + 'Left'], tArray[3])]; } }; // end Chart -// Hook for exporting module -Chart.prototype.callbacks = []; - 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. */ @@ -12187,10 +12582,11 @@ init: function (series, options, x) { var point = this, colors; point.series = series; + point.color = series.color; // #3445 point.applyOptions(options, x); point.pointAttr = {}; if (series.options.colorByPoint) { colors = series.options.colors || series.chart.options.colors; @@ -12427,11 +12823,11 @@ * - series.yData and series.processedYData contain clean x values, equivalent to series.data and series.points. * * @param {Object} chart * @param {Object} options */ -var Series = function () {}; +var Series = Highcharts.Series = function () {}; Series.prototype = { isCartesian: true, type: 'line', @@ -12587,19 +12983,31 @@ /** * 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 () { - var series = this, - options = series.options, - xIncrement = series.xIncrement; + var options = this.options, + xIncrement = this.xIncrement, + date, + pointInterval, + pointIntervalUnit = options.pointIntervalUnit; + xIncrement = pick(xIncrement, options.pointStart, 0); - - series.pointInterval = pick(series.pointInterval, options.pointInterval, 1); - - series.xIncrement = xIncrement + series.pointInterval; + + 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; }, /** * Divide the series data into segments divided by null values. @@ -12654,11 +13062,12 @@ chartOptions = chart.options, plotOptions = chartOptions.plotOptions, userOptions = chart.userOptions || {}, userPlotOptions = userOptions.plotOptions || {}, typeOptions = plotOptions[this.type], - options; + options, + zones; this.userOptions = itemOptions; options = merge( typeOptions, @@ -12679,12 +13088,29 @@ // Delete marker object if not allowed (#1125) if (typeOptions.marker === null) { delete options.marker; } + // 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; - }, getCyclic: function (prop, value, defaults) { var i, userOptions = this.userOptions, @@ -12740,11 +13166,10 @@ options = series.options, chart = series.chart, firstPoint = null, xAxis = series.xAxis, hasCategories = xAxis && !!xAxis.categories, - tooltipPoints = series.tooltipPoints, i, turboThreshold = options.turboThreshold, pt, xData = this.xData, yData = this.yData, @@ -12755,11 +13180,11 @@ dataLength = data.length; redraw = pick(redraw, true); // 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) { + if (updatePoints !== false && dataLength && oldDataLength === dataLength && !series.cropped && !series.hasGroupedData && series.visible) { each(data, function (point, i) { oldData[i].update(point, false, null, false); }); } else { @@ -12843,13 +13268,10 @@ while (i--) { if (oldData[i] && oldData[i].destroy) { oldData[i].destroy(); } } - if (tooltipPoints) { // #2594 - tooltipPoints.length = 0; - } // reset minRange (#878) if (xAxis) { xAxis.minRange = xAxis.userMinRange; } @@ -12880,11 +13302,10 @@ closestPointRange, xAxis = series.xAxis, i, // loop variable options = series.options, cropThreshold = options.cropThreshold, - activePointCount = 0, isCartesian = series.isCartesian, xExtremes, min, max; @@ -12913,23 +13334,18 @@ croppedData = this.cropData(series.xData, series.yData, min, max); processedXData = croppedData.xData; processedYData = croppedData.yData; cropStart = croppedData.start; cropped = true; - activePointCount = processedXData.length; } } // Find the closest distance between processed points for (i = processedXData.length - 1; i >= 0; i--) { distance = processedXData[i] - processedXData[i - 1]; - if (!cropped && processedXData[i] > min && processedXData[i] < max) { - activePointCount++; - } - if (distance > 0 && (closestPointRange === UNDEFINED || distance < closestPointRange)) { closestPointRange = distance; // Unsorted data is not supported by the line tooltip, as well as data grouping and // navigation in Stock charts (#725) and width calculation of columns (#1900) @@ -12941,11 +13357,10 @@ // Record the properties series.cropped = cropped; // undefined or true series.cropStart = cropStart; series.processedXData = processedXData; series.processedYData = processedYData; - series.activePointCount = activePointCount; if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC series.pointRange = closestPointRange || 1; } series.closestPointRange = closestPointRange; @@ -13122,11 +13537,15 @@ dataLength = points.length, hasModifyValue = !!series.modifyValue, i, pointPlacement = options.pointPlacement, dynamicallyPlaced = pointPlacement === 'between' || isNumber(pointPlacement), - threshold = options.threshold; + threshold = options.threshold, + plotX, + plotY, + lastPlotX, + closestPointRangePx = Number.MAX_VALUE; // Translate each point for (i = 0; i < dataLength; i++) { var point = points[i], xValue = point.x, @@ -13134,18 +13553,18 @@ yBottom = point.low, stack = stacking && yAxis.stacks[(series.negStacks && yValue < threshold ? '-' : '') + series.stackKey], pointStack, stackValues; - // Discard disallowed y values for log axes - if (yAxis.isLog && yValue <= 0) { + // Discard disallowed y values for log axes (#3434) + if (yAxis.isLog && yValue !== null && yValue <= 0) { point.y = yValue = null; error(10); } // Get the plotX translation - point.plotX = xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement, this.type === 'flags'); // Math.round fixes #591 + point.plotX = plotX = xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement, this.type === 'flags'); // Math.round fixes #591 // Calculate the bottom y value for stacked series if (stacking && series.visible && stack && stack[xValue]) { @@ -13179,74 +13598,116 @@ if (hasModifyValue) { yValue = series.modifyValue(yValue, point); } // Set the the plotY value, reset it for redraws - point.plotY = (typeof yValue === 'number' && yValue !== Infinity) ? - //mathRound(yAxis.translate(yValue, 0, 1, 0, 1) * 10) / 10 : // Math.round fixes #591 - yAxis.translate(yValue, 0, 1, 0, 1) : + 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; + // Set client related positions for mouse tracking - point.clientX = dynamicallyPlaced ? xAxis.translate(xValue, 0, 0, 0, 1) : point.plotX; // #1514 + 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, + 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].join(','), + 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); + + } + clipRect.count += 1; + + if (this.options.clip !== false) { + this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect); + this.markerGroup.clip(markerClipRect); + this.sharedClipKey = sharedClipKey; + } + + // Remove the shared clipping rectancgle 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, - renderer = chart.renderer, clipRect, - markerClipRect, animation = series.options.animation, - clipBox = series.clipBox || chart.clipBox, - inverted = chart.inverted, sharedClipKey; // Animation option is set to true if (animation && !isObject(animation)) { animation = defaultPlotOptions[series.type].animation; } - sharedClipKey = ['_sharedClip', animation.duration, animation.easing, clipBox.height].join(','); // Initialize the animation. Set up the clipping rectangle. if (init) { - // If a clipping rectangle with the same properties is currently present in the chart, use that. - clipRect = chart[sharedClipKey]; - markerClipRect = chart[sharedClipKey + 'm']; - if (!clipRect) { - chart[sharedClipKey] = clipRect = renderer.clipRect( - extend(clipBox, { width: 0 }) - ); + series.setClip(animation); - 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 - ); - } - series.group.clip(clipRect); - series.markerGroup.clip(markerClipRect); - series.sharedClipKey = sharedClipKey; - // Run the animation } else { + sharedClipKey = this.sharedClipKey; clipRect = chart[sharedClipKey]; if (clipRect) { clipRect.animate({ width: chart.plotSizeX }, animation); @@ -13265,35 +13726,12 @@ /** * This runs after animation to land on the final plot clipping */ afterAnimate: function () { - var chart = this.chart, - sharedClipKey = this.sharedClipKey, - group = this.group, - clipBox = this.clipBox; - - if (group && this.options.clip !== false) { - if (!sharedClipKey || !clipBox) { - group.clip(clipBox ? chart.renderer.clipRect(clipBox) : chart.clipRect); - } - this.markerGroup.clip(); // no clip - } - + this.setClip(); fireEvent(this, 'afterAnimate'); - - // Remove the shared clipping rectancgle when all series are shown - setTimeout(function () { - if (sharedClipKey && chart[sharedClipKey]) { - if (!clipBox) { - chart[sharedClipKey] = chart[sharedClipKey].destroy(); - } - if (chart[sharedClipKey + 'm']) { - chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy(); - } - } - }, 100); }, /** * Draw the markers */ @@ -13316,13 +13754,15 @@ pointMarkerOptions, hasPointMarker, enabled, isInside, markerGroup = series.markerGroup, + xAxis = series.xAxis, globallyEnabled = pick( seriesMarkerOptions.enabled, - !series.requireSorting || series.activePointCount < (0.5 * series.xAxis.len / seriesMarkerOptions.radius) + xAxis.isRadial, + series.closestPointRangePx > 2 * seriesMarkerOptions.radius ); if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) { i = points.length; @@ -13332,11 +13772,11 @@ plotY = point.plotY; graphic = point.graphic; pointMarkerOptions = point.marker || {}; hasPointMarker = !!point.marker; enabled = (globallyEnabled && pointMarkerOptions.enabled === UNDEFINED) || pointMarkerOptions.enabled; - isInside = chart.isInsidePlot(mathRound(plotX), plotY, chart.inverted); // #1858 + isInside = point.isInside; // only draw the point if y is defined if (enabled && plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) { // shortcuts @@ -13413,10 +13853,11 @@ 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 @@ -13424,14 +13865,15 @@ point, seriesPointAttr = [], pointAttr, pointAttrToOptions = series.pointAttrToOptions, hasPointSpecificOptions = series.hasPointSpecificOptions, - negativeColor = seriesOptions.negativeColor, 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 @@ -13444,10 +13886,15 @@ // 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); @@ -13471,12 +13918,18 @@ normalOptions = (point.options && point.options.marker) || point.options; if (normalOptions && normalOptions.enabled === false) { normalOptions.radius = 0; } - if (point.negative && negativeColor) { - point.color = point.fillColor = negativeColor; + if (zones.length) { + var j = 0, + threshold = zones[j]; + while (point[zoneAxis] >= threshold.value) { + threshold = zones[++j]; + } + + point.color = point.fillColor = threshold.color; } hasPointSpecificOptions = seriesOptions.colorByPoint || point.color; // #868 // check if the point has specific visual options @@ -13497,11 +13950,11 @@ pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {}; // Handle colors for column and pies if (!seriesOptions.marker) { // column, bar, point // If no hover color is given, brighten the normal color. #1619, #2579 - pointStateOptionsHover.color = pointStateOptionsHover.color || (!point.options.color && stateOptionsHover.color) || + pointStateOptionsHover.color = pointStateOptionsHover.color || (!point.options.color && stateOptionsHover[(point.negative && seriesNegativeColor ? 'negativeColor' : 'color')]) || Color(point.color) .brighten(pointStateOptionsHover.brightness || stateOptionsHover.brightness) .get(); } @@ -13707,40 +14160,40 @@ * Draw the actual graph */ drawGraph: function () { var series = this, options = this.options, - props = [['graph', options.lineColor || this.color]], + props = [['graph', options.lineColor || this.color, options.dashStyle]], lineWidth = options.lineWidth, - dashStyle = options.dashStyle, roundCap = options.linecap !== 'square', graphPath = this.getGraphPath(), - negativeColor = options.negativeColor; + fillColor = (this.fillGraph && this.color) || NONE, // polygon series use filled graph + zones = this.zones; - if (negativeColor) { - props.push(['graphNeg', negativeColor]); - } - - // draw the graph + each(zones, function (threshold, i) { + props.push(['colorGraph' + 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) { stop(graph); // cancel running animations, #459 graph.animate({ d: graphPath }); - } else if (lineWidth && graphPath.length) { // #1487 + } else if ((lineWidth || fillColor) && graphPath.length) { // #1487 attribs = { stroke: prop[1], 'stroke-width': lineWidth, - fill: NONE, + fill: fillColor, zIndex: 1 // #1069 }; - if (dashStyle) { - attribs.dashstyle = dashStyle; + if (prop[2]) { + attribs.dashstyle = prop[2]; } else if (roundCap) { attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round'; } series[graphKey] = series.chart.renderer.path(graphPath) @@ -13752,92 +14205,93 @@ }, /** * Clip the graphs into the positive and negative coloured graphs */ - clipNeg: function () { - var options = this.options, + applyZones: function () { + var series = this, chart = this.chart, renderer = chart.renderer, - negativeColor = options.negativeColor || options.negativeFillColor, - translatedThreshold, - posAttr, - negAttr, + zones = this.zones, + translatedFrom, + translatedTo, + clips = this.clips || [], + clipAttr, graph = this.graph, area = this.area, - posClip = this.posClip, - negClip = this.negClip, - chartWidth = chart.chartWidth, - chartHeight = chart.chartHeight, - chartSizeMax = mathMax(chartWidth, chartHeight), - yAxis = this.yAxis, - above, - below; + chartSizeMax = mathMax(chart.chartWidth, chart.chartHeight), + zoneAxis = this.zoneAxis || 'y', + axis = this[zoneAxis + 'Axis'], + reversed = axis.reversed, + horiz = axis.horiz; - if (negativeColor && (graph || area)) { - translatedThreshold = mathRound(yAxis.toPixels(options.threshold || 0, true)); - if (translatedThreshold < 0) { - chartSizeMax -= translatedThreshold; // #2534 - } - above = { - x: 0, - y: 0, - width: chartSizeMax, - height: translatedThreshold - }; - below = { - x: 0, - y: translatedThreshold, - width: chartSizeMax, - height: chartSizeMax - }; + if (zones.length && (graph || area)) { + // The use of the Color Threshold assumes there are no gaps + // so it is safe to hide the original graph and area + graph.hide(); + if (area) { area.hide(); } - if (chart.inverted) { + // Create the clips + each(zones, function (threshold, i) { + translatedFrom = pick(translatedTo, (reversed ? (horiz ? chart.plotWidth : 0) : (horiz ? 0 : axis.toPixels(axis.min)))); + translatedTo = mathRound(axis.toPixels(pick(threshold.value, axis.max), true)); - above.height = below.y = chart.plotWidth - translatedThreshold; - if (renderer.isVML) { - above = { - x: chart.plotWidth - translatedThreshold - chart.plotLeft, + if (axis.isXAxis) { + clipAttr = { + x: reversed ? translatedTo : translatedFrom, y: 0, - width: chartWidth, - height: chartHeight + width: Math.abs(translatedFrom - translatedTo), + height: chartSizeMax }; - below = { - x: translatedThreshold + chart.plotLeft - chartWidth, - y: 0, - width: chart.plotLeft + translatedThreshold, - height: chartWidth + if (!horiz) { + clipAttr.x = chart.plotHeight - clipAttr.x; + } + } else { + clipAttr = { + x: 0, + y: reversed ? translatedFrom : translatedTo, + width: chartSizeMax, + height: Math.abs(translatedFrom - translatedTo) }; + if (horiz) { + clipAttr.y = chart.plotWidth - clipAttr.y; + } + } + + /// VML SUPPPORT + if (chart.inverted && renderer.isVML) { + if (axis.isXAxis) { + clipAttr = { + x: 0, + y: reversed ? translatedFrom : translatedTo, + 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 (yAxis.reversed) { - posAttr = below; - negAttr = above; - } else { - posAttr = above; - negAttr = below; - } + if (clips[i]) { + clips[i].animate(clipAttr); + } else { + clips[i] = renderer.clipRect(clipAttr); - if (posClip) { // update - posClip.animate(posAttr); - negClip.animate(negAttr); - } else { + series['colorGraph' + i].clip(clips[i]); - this.posClip = posClip = renderer.clipRect(posAttr); - this.negClip = negClip = renderer.clipRect(negAttr); - - if (graph && this.graphNeg) { - graph.clip(posClip); - this.graphNeg.clip(negClip); + if (area) { + series['colorArea' + i].clip(clips[i]); + } } - - if (area) { - area.clip(posClip); - this.areaNeg.clip(negClip); - } - } + }); + this.clips = clips; } }, /** * Initialize and perform group inversion on series.group and series.markerGroup @@ -13966,11 +14420,11 @@ group.inverted = series.isCartesian ? chart.inverted : false; // draw the graph if any if (series.drawGraph) { series.drawGraph(); - series.clipNeg(); + series.applyZones(); } each(series.points, function (point) { if (point.redraw) { point.redraw(); @@ -13996,15 +14450,10 @@ // Handle inverted series and tracker groups if (chart.inverted) { series.invertGroups(); } - // Initial clipping, must be defined after inverting groups for VML - if (options.clip !== false && !series.sharedClipKey && !hasRendered) { - group.clip(chart.clipRect); - } - // Run the animation if (animDuration) { series.animate(); } @@ -14050,19 +14499,144 @@ translateY: pick(yAxis && yAxis.top, chart.plotTop) }); } series.translate(); - if (series.setTooltipPoints) { - series.setTooltipPoints(true); - } series.render(); if (wasDirtyData) { fireEvent(series, 'updatedData'); } + }, + + /** + * KD Tree && PointSearching Implementation + */ + + kdDimensions: 1, + kdTree: null, + kdAxisArray: ['plotX', 'plotY'], + kdComparer: 'distX', + + searchPoint: function (e) { + var series = this, + xAxis = series.xAxis, + yAxis = series.yAxis, + inverted = series.chart.inverted; + + e.plotX = inverted ? xAxis.len - e.chartY + xAxis.pos : e.chartX - xAxis.pos; + e.plotY = inverted ? yAxis.len - e.chartX + yAxis.pos : e.chartY - yAxis.pos; + + return this.searchKDTree(e); + }, + + 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 node + return { + point: points[median], + left: _kdtree(points.slice(0, median), depth + 1, dimensions), + right: _kdtree(points.slice(median + 1), depth + 1, dimensions) + }; + + } + } + + function startRecursive() { + series.kdTree = _kdtree(series.points, dimensions, dimensions); + } + + delete series.kdTree; + + if (series.options.kdSync) { // For testing tooltips, don't build async + startRecursive(); + } else { + setTimeout(startRecursive); + } + }, + + searchKDTree: function (point) { + var series = this, + kdComparer = this.kdComparer, + kdX = this.kdAxisArray[0], + kdY = this.kdAxisArray[1]; + + // Internal function + function _distance(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); + + return { + distX: defined(x) ? Math.sqrt(x) : Number.MAX_VALUE, + distY: defined(y) ? Math.sqrt(y) : Number.MAX_VALUE, + distR: defined(r) ? Math.sqrt(r) : 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; + point.dist = _distance(search, point); + + // Pick side based on distance to splitting point + tdist = search[axis] - point[axis]; + sideA = tdist < 0 ? 'left' : 'right'; + + // End of tree + if (tree[sideA]) { + nPoint1 =_search(search, tree[sideA], depth + 1, dimensions); + + ret = (nPoint1.dist[kdComparer] < ret.dist[kdComparer] ? nPoint1 : point); + + sideB = tdist < 0 ? 'right' : 'left'; + 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.dist[kdComparer]) { + nPoint2 = _search(search, tree[sideB], depth + 1, dimensions); + ret = (nPoint2.dist[kdComparer] < ret.dist[kdComparer] ? nPoint2 : ret); + } + } + } + return ret; + } + + if (!this.kdTree) { + this.buildKDTree(); + } + + if (this.kdTree) { + return _search(point, + this.kdTree, this.kdDimensions, this.kdDimensions); + } else { + return UNDEFINED; + } } + }; // end Series prototype /** * The class for stack items */ @@ -14508,11 +15082,12 @@ var point = this, series = point.series, graphic = point.graphic, i, chart = series.chart, - seriesOptions = series.options; + seriesOptions = series.options, + names = series.xAxis.names; redraw = pick(redraw, true); function update() { @@ -14537,10 +15112,13 @@ } // record changes in the parallel arrays i = point.index; series.updateParallelArrays(point, i); + if (names && point.name) { + names[point.x] = point.name; + } seriesOptions.data[i] = point.options; // redraw series.isDirty = series.isDirtyData = true; @@ -14569,41 +15147,11 @@ * @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 point = this, - series = point.series, - points = series.points, - chart = series.chart, - i, - data = series.data; - - setAnimation(animation, chart); - redraw = pick(redraw, true); - - // fire the event with a default handler of removing the point - point.firePointEvent('remove', null, function () { - - // splice all the parallel arrays - i = inArray(point, data); - if (data.length === points.length) { - points.splice(i, 1); - } - data.splice(i, 1); - series.options.data.splice(i, 1); - series.updateParallelArrays(point, 'splice', i, 1); - - point.destroy(); - - // redraw - series.isDirty = true; - series.isDirtyData = true; - if (redraw) { - chart.redraw(); - } - }); + this.series.removePoint(inArray(this, this.series.data), redraw, animation); } }); // Extend the series prototype for dynamic methods extend(Series.prototype, { @@ -14703,10 +15251,52 @@ 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 @@ -14754,10 +15344,15 @@ 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]; }); @@ -14767,25 +15362,23 @@ animation: false, index: this.index, pointStart: this.xData[0] // when updating after addPoint }, { data: this.options.data }, newOptions); - // Destroy the series and reinsert methods from the type prototype + // 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) { // Overwrite series-type specific methods (#2270) - if (proto.hasOwnProperty(n)) { - this[n] = UNDEFINED; - } + 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); } @@ -15045,18 +15638,16 @@ // Define local variables var series = this, areaPath = this.areaPath, options = this.options, - negativeColor = options.negativeColor, - negativeFillColor = options.negativeFillColor, + zones = this.zones, props = [['area', this.color, options.fillColor]]; // area name, main color, fill color - if (negativeColor || negativeFillColor) { - props.push(['areaNeg', negativeColor, negativeFillColor]); - } - + each(zones, function (threshold, i) { + props.push(['colorArea' + 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 @@ -15276,10 +15867,11 @@ 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) /** @@ -15376,11 +15968,11 @@ var series = this, chart = series.chart, options = series.options, borderWidth = series.borderWidth = pick( options.borderWidth, - series.activePointCount > 0.5 * series.xAxis.len ? 0 : 1 + 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), @@ -15429,22 +16021,22 @@ // Cache for access in polar point.barX = barX; point.pointWidth = pointWidth; - // Fix the tooltip on center of grouped columns (#1216, #424) + // Fix the tooltip on center of grouped columns (#1216, #424, #3648) point.tooltipPos = chart.inverted ? - [yAxis.len - plotY, series.xAxis.len - barX - barW / 2] : + [yAxis.len + yAxis.pos - chart.plotLeft - plotY, series.xAxis.len - barX - barW / 2] : [barX + barW / 2, plotY + yAxis.pos - chart.plotTop]; // Round off to obtain crisp edges and avoid overlapping with neighbours (#2694) right = mathRound(barX + barW) + xCrisp; barX = mathRound(barX) + xCrisp; barW = right - barX; fromTop = mathAbs(barY) < 0.5; - bottom = mathRound(barY + barH) + yCrisp; + bottom = mathMin(mathRound(barY + barH) + yCrisp, 9e4); // #3575 barY = mathRound(barY) + yCrisp; barH = bottom - barY; // Top edges are exceptions if (fromTop) { @@ -15511,12 +16103,12 @@ stop(graphic); graphic.attr(borderAttr)[chart.pointCount < animationLimit ? 'animate' : 'attr'](merge(shapeArgs)); } else { point.graphic = graphic = renderer[point.shapeType](shapeArgs) - .attr(pointAttr) .attr(borderAttr) + .attr(pointAttr) .add(series.group) .shadow(options.shadow, null, options.stacking && !options.borderRadius); } } else if (graphic) { @@ -15597,28 +16189,31 @@ /** * 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:{series.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/>' - }, - stickyTracking: false + } }); /** * The scatter series class */ var ScatterSeries = extendClass(Series, { type: 'scatter', sorted: false, requireSorting: false, noSharedTooltip: true, - trackerGroups: ['markerGroup', 'dataLabelsGroup'], + trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'], takeOrdinalPosition: false, // #2342 - singularTooltips: true, + kdDimensions: 2, + kdComparer: 'distR', drawGraph: function () { if (this.options.lineWidth) { Series.prototype.drawGraph.call(this); } } @@ -15642,13 +16237,14 @@ // connectorPadding: 5, distance: 30, enabled: true, formatter: function () { // #2945 return this.point.name; - } + }, // softConnector: true, - //y: 0 + x: 0 + // y: 0 }, ignoreHiddenPoint: true, //innerSize: 0, legendType: 'point', marker: null, // point options are specified in the base options @@ -15679,16 +16275,10 @@ Point.prototype.init.apply(this, arguments); var point = this, toggleSlice; - // Disallow negative values (#1530) - if (point.y < 0) { - point.y = null; - } - - //visible: options.visible !== false, extend(point, { visible: point.visible !== false, name: pick(point.name, 'Slice') }); @@ -15793,11 +16383,10 @@ pointAttrToOptions: { // mapping between SVG attributes and the corresponding options stroke: 'borderColor', 'stroke-width': 'borderWidth', fill: 'color' }, - singularTooltips: true, /** * Pies have one color each point */ getColor: noop, @@ -15868,10 +16457,16 @@ len = points.length; // Get the total sum for (i = 0; i < len; i++) { point = points[i]; + + // Disallow negative values (#1530, #3623) + if (point.y < 0) { + point.y = null; + } + total += (ignoreHiddenPoint && !point.visible) ? 0 : point.y; } this.total = total; // Set each point's properties @@ -16064,10 +16659,13 @@ }); }, + + searchPoint: noop, + /** * Utility for sorting data labels */ sortByAngle: function (points, sign) { points.sort(function (a, b) { @@ -16106,11 +16704,12 @@ points = series.points, pointOptions, generalOptions, hasRendered = series.hasRendered || 0, str, - dataLabelsGroup; + dataLabelsGroup, + renderer = series.chart.renderer; if (options.enabled || series._hasPointLabels) { // Process default alignment of data labels for columns if (series.dlProcessOptions) { @@ -16146,14 +16745,16 @@ labelConfig, attr, name, rotation, connector = point.connector, - isNew = true; + isNew = true, + style, + moreStyle = {}; // Determine if each data label is enabled - pointOptions = point.options && point.options.dataLabels; + pointOptions = point.dlOptions || (point.options && point.options.dataLabels); // dlOptions is used in treemaps enabled = pick(pointOptions && pointOptions.enabled, generalOptions.enabled); // #2282 // If the point is outside the plot area, destroy it. #678, #820 if (dataLabel && !enabled) { @@ -16164,21 +16765,22 @@ } else if (enabled) { // Create individual options structure that can be extended without // affecting others options = merge(generalOptions, pointOptions); + style = options.style; rotation = options.rotation; // Get the string labelConfig = point.getLabelConfig(); str = options.format ? format(options.format, labelConfig) : options.formatter.call(labelConfig, options); // Determine the color - options.style.color = pick(options.color, options.style.color, series.color, 'black'); + style.color = pick(options.color, style.color, series.color, 'black'); // update existing label if (dataLabel) { @@ -16206,28 +16808,40 @@ 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]; } } - dataLabel = point.dataLabel = series.chart.renderer[rotation ? 'text' : 'label']( // labels don't support rotation + dataLabel = point.dataLabel = renderer[rotation ? 'text' : 'label']( // labels don't support rotation str, 0, -999, null, null, null, options.useHTML ) .attr(attr) - .css(extend(options.style, cursor && { cursor: cursor })) + .css(extend(style, moreStyle)) .add(dataLabelsGroup) .shadow(options.shadow); } @@ -16247,10 +16861,12 @@ 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; @@ -16270,12 +16886,13 @@ height: bBox.height }); // 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, + x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x, y: alignTo.y + options.y + alignTo.height / 2 }) .attr({ // #3003 align: options.align }); @@ -16310,47 +16927,48 @@ Series.prototype.justifyDataLabel = function (dataLabel, options, alignAttr, bBox, alignTo, isNew) { var chart = this.chart, align = options.align, verticalAlign = options.verticalAlign, off, - justified; + justified, + padding = dataLabel.box ? 0 : (dataLabel.padding || 0); // Off left - off = alignAttr.x; + off = alignAttr.x + padding; if (off < 0) { if (align === 'right') { options.align = 'left'; } else { options.x = -off; } justified = true; } // Off right - off = alignAttr.x + bBox.width; + off = alignAttr.x + bBox.width - padding; if (off > chart.plotWidth) { if (align === 'left') { options.align = 'right'; } else { options.x = chart.plotWidth - off; } justified = true; } // Off top - off = alignAttr.y; + 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; + off = alignAttr.y + bBox.height - padding; if (off > chart.plotHeight) { if (verticalAlign === 'top') { options.verticalAlign = 'bottom'; } else { options.y = chart.plotHeight - off; @@ -16771,24 +17389,24 @@ /** * 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 chart = this.chart, - inverted = chart.inverted, + var inverted = this.chart.inverted, + series = point.series, dlBox = point.dlBox || point.shapeArgs, // data label box for alignment - below = point.below || (point.plotY > pick(this.translatedThreshold, chart.plotSizeY)), + below = point.below || (point.plotY > pick(this.translatedThreshold, series.yAxis.len)), inside = pick(options.inside, !!this.options.stacking); // draw it inside the box? // Align to the column itself, or the top of it if (dlBox) { // Area range uses this method but not alignTo alignTo = merge(dlBox); if (inverted) { alignTo = { - x: chart.plotWidth - alignTo.y - alignTo.height, - y: chart.plotHeight - alignTo.x - alignTo.width, + x: series.yAxis.len - alignTo.y - alignTo.height, + y: series.xAxis.len - alignTo.x - alignTo.width, width: alignTo.height, height: alignTo.width }; } @@ -16822,10 +17440,109 @@ } /** + * @license Highcharts JS v4.1.0 (2015-02-16) + * 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, + addEvent = HighchartsAdapter.addEvent; + + // 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 = []; + + each(chart.series, function (series) { + var dlOptions = series.options.dataLabels; + if ((dlOptions.enabled || series._hasPointLabels) && !dlOptions.allowOverlap) { + each(series.points, function (point) { + if (point.dataLabel) { + point.dataLabel.labelrank = point.labelrank; + labels.push(point.dataLabel); + } + }); + } + }); + chart.hideOverlappingLabels(labels); + } + + // Do it now ... + collectAndHide(); + + // ... and after each chart redraw + addEvent(chart, 'redraw', collectAndHide); + + }); + + /** + * Hide overlapping labels. Labels are moved and faded in and out on zoom to provide a smooth + * visual imression. + */ + Chart.prototype.hideOverlappingLabels = function (labels) { + + var len = labels.length, + label, + i, + j, + label1, + label2, + intersectRect = function (pos1, pos2, size1, size2) { + return !( + pos2.x > pos1.x + size1.width || + pos2.x + size2.width < pos1.x || + pos2.y > pos1.y + size1.height || + pos2.y + size2.height < pos1.y + ); + }; + + // Mark with initial opacity + for (i = 0; i < len; i++) { + label = labels[i]; + if (label) { + label.oldOpacity = label.opacity; + label.newOpacity = 1; + } + } + + // Detect overlapping labels + for (i = 0; i < len; i++) { + label1 = labels[i]; + + for (j = i + 1; j < len; ++j) { + label2 = labels[j]; + if (label1 && label2 && label1.placed && label2.placed && label1.newOpacity !== 0 && label2.newOpacity !== 0 && + intersectRect(label1.alignAttr, label2.alignAttr, label1, label2)) { + (label1.labelrank < label2.labelrank ? label1 : label2).newOpacity = 0; + } + } + } + + // Hide or show + for (i = 0; i < len; i++) { + label = labels[i]; + if (label) { + if (label.oldOpacity !== label.newOpacity && label.placed) { + label.alignAttr.opacity = label.newOpacity; + label[label.isOld && label.newOpacity ? 'animate' : 'attr'](label.alignAttr); + } + label.isOld = true; + } + } + }; + +}(Highcharts));/** * TrackerMixin for points and graphs */ var TrackerMixin = Highcharts.TrackerMixin = { @@ -17039,12 +17756,13 @@ defaultChecked: item.selected // required by IE7 }, legend.options.itemCheckboxStyle, legend.chart.container); addEvent(item.checkbox, 'click', function (event) { var target = event.target; - fireEvent(item, 'checkboxClick', { - checked: target.checked + fireEvent(item.series || item, 'checkboxClick', { // #3712 + checked: target.checked, + item: item }, function () { item.select(); } ); @@ -17165,13 +17883,16 @@ 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; + newMax = axis.toValue(startPos + chart[isX ? 'plotWidth' : 'plotHeight'] - mousePos, true) - halfPointRange, + goingLeft = startPos > mousePos; // #3613 - if (axis.series.length && newMin > mathMin(extremes.dataMin, extremes.min) && newMax < mathMax(extremes.dataMax, extremes.max)) { + 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; } chart[isX ? 'mouseDownX' : 'mouseDownY'] = mousePos; // set new reference for next run @@ -17390,11 +18111,11 @@ // Show me your halo haloOptions = stateOptions[state] && stateOptions[state].halo; if (haloOptions && haloOptions.size) { if (!halo) { series.halo = halo = chart.renderer.path() - .add(series.seriesGroup); + .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) @@ -17502,11 +18223,11 @@ if (stateOptions[state] && stateOptions[state].enabled === false) { return; } if (state) { - lineWidth = stateOptions[state].lineWidth || lineWidth + (stateOptions[state].lineWidthPlus || 0); + lineWidth = (stateOptions[state].lineWidth || lineWidth) + (stateOptions[state].lineWidthPlus || 0); } if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML attribs = { 'stroke-width': lineWidth @@ -17582,78 +18303,10 @@ fireEvent(series, showOrHide); }, /** - * Memorize tooltip texts and positions - */ - setTooltipPoints: function (renew) { - var series = this, - points = [], - pointsLength, - low, - high, - xAxis = series.xAxis, - xExtremes = xAxis && xAxis.getExtremes(), - axisLength = xAxis ? (xAxis.tooltipLen || xAxis.len) : series.chart.plotSizeX, // tooltipLen and tooltipPosName used in polar - point, - pointX, - nextPoint, - i, - tooltipPoints = []; // a lookup array for each pixel in the x dimension - - // don't waste resources if tracker is disabled - if (series.options.enableMouseTracking === false || series.singularTooltips) { - return; - } - - // renew - if (renew) { - series.tooltipPoints = null; - } - - // concat segments to overcome null values - each(series.segments || series.points, function (segment) { - points = points.concat(segment); - }); - - // Reverse the points in case the X axis is reversed - if (xAxis && xAxis.reversed) { - points = points.reverse(); - } - - // Polar needs additional shaping - if (series.orderTooltipPoints) { - series.orderTooltipPoints(points); - } - - // Assign each pixel position to the nearest point - pointsLength = points.length; - for (i = 0; i < pointsLength; i++) { - point = points[i]; - pointX = point.x; - if (pointX >= xExtremes.min && pointX <= xExtremes.max) { // #1149 - nextPoint = points[i + 1]; - - // Set this range's low to the last range's high plus one - low = high === UNDEFINED ? 0 : high + 1; - // Now find the new high - high = points[i + 1] ? - mathMin(mathMax(0, mathFloor( // #2070 - (point.clientX + (nextPoint ? (nextPoint.wrappedClientX || nextPoint.clientX) : axisLength)) / 2 - )), axisLength) : - axisLength; - - while (low >= 0 && low <= high) { - tooltipPoints[low++] = point; - } - } - } - series.tooltipPoints = tooltipPoints; - }, - - /** * Show the graph */ show: function () { this.setVisible(true); }, @@ -17688,46 +18341,39 @@ }); // global variables extend(Highcharts, { // Constructors - Axis: Axis, - Chart: Chart, Color: Color, Point: Point, Tick: Tick, Renderer: Renderer, - Series: Series, 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, - numberFormat: numberFormat, - seriesTypes: seriesTypes, setOptions: setOptions, addEvent: addEvent, removeEvent: removeEvent, createElement: createElement, discardElement: discardElement, css: css, each: each, - extend: extend, map: map, merge: merge, - pick: pick, splat: splat, extendClass: extendClass, pInt: pInt, - wrap: wrap, svg: hasSVG, canvas: useCanVG, vml: !hasSVG && !useCanVG, product: PRODUCT, version: VERSION