app/assets/javascripts/highcharts.js in highcharts-rails-4.0.1 vs app/assets/javascripts/highcharts.js in highcharts-rails-4.0.3

- old
+ new

@@ -1,19 +1,19 @@ // ==ClosureCompiler== // @compilation_level SIMPLE_OPTIMIZATIONS /** - * @license Highcharts JS v4.0.1 (2014-04-24) + * @license Highcharts JS v4.0.3 (2014-07-03) * * (c) 2009-2014 Torstein Honsi * * License: www.highcharts.com/license */ // JSLint options: /*global Highcharts, document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $, console, each, grep */ - +/*jslint ass: true, sloppy: true, forin: true, plusplus: true, nomen: true, vars: true, regexp: true, newcap: true, browser: true, continue: true, white: true */ (function () { // encapsulated variables var UNDEFINED, doc = document, win = window, @@ -50,15 +50,16 @@ defaultOptions, dateFormat, // function globalAnimation, pathAnim, timeUnits, - noop = function () {}, + error, + noop = function () { return UNDEFINED; }, charts = [], chartCount = 0, PRODUCT = 'Highcharts', - VERSION = '4.0.1', + VERSION = '4.0.3', // some constants for frequently used strings DIV = 'div', ABSOLUTE = 'absolute', RELATIVE = 'relative', @@ -71,18 +72,10 @@ L = 'L', numRegex = /^[0-9]+$/, NORMAL_STATE = '', HOVER_STATE = 'hover', SELECT_STATE = 'select', - MILLISECOND = 'millisecond', - SECOND = 'second', - MINUTE = 'minute', - HOUR = 'hour', - DAY = 'day', - WEEK = 'week', - MONTH = 'month', - YEAR = 'year', // Object for extending Axis AxisPlotLineOrBandExtension, // constants for attributes @@ -103,15 +96,19 @@ setMonth, setFullYear, // lookup over the types and the associated classes - seriesTypes = {}; + seriesTypes = {}, + Highcharts; // The Highcharts namespace -var Highcharts = win.Highcharts = win.Highcharts ? error(16, true) : {}; - +if (win.Highcharts) { + error(16, true); +} else { + Highcharts = win.Highcharts = {}; +} /** * 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 */ @@ -178,26 +175,10 @@ return ret; } /** - * Take an array and turn into a hash with even number arguments as keys and odd numbers as - * values. Allows creating constants for commonly used style properties, attributes etc. - * Avoid it in performance critical situations like looping - */ -function hash() { - var i = 0, - args = arguments, - length = args.length, - obj = {}; - for (; i < length; i++) { - obj[args[i++]] = args[i]; - } - return obj; -} - -/** * Shortcut for parseInt * @param {Object} s * @param {Number} mag Magnitude */ function pInt(s, mag) { @@ -215,11 +196,11 @@ /** * Check for object * @param {Object} obj */ function isObject(obj) { - return typeof obj === 'object'; + return obj && typeof obj === 'object'; } /** * Check for array * @param {Object} obj @@ -315,11 +296,11 @@ i, arg, length = args.length; for (i = 0; i < length; i++) { arg = args[i]; - if (typeof arg !== 'undefined' && arg !== null) { + if (arg !== UNDEFINED && arg !== null) { return arg; } } } @@ -366,11 +347,11 @@ * Extend a prototyped class by new members * @param {Object} parent * @param {Object} members */ function extendClass(parent, members) { - var object = function () {}; + var object = function () { return UNDEFINED; }; object.prototype = new parent(); extend(object.prototype, members); return object; } @@ -617,39 +598,10 @@ return interval; } /** - * Helper class that contains variuos counters that are local to the chart. - */ -function ChartCounters() { - this.color = 0; - this.symbol = 0; -} - -ChartCounters.prototype = { - /** - * Wraps the color counter if it reaches the specified length. - */ - wrapColor: function (length) { - if (this.color >= length) { - this.color = 0; - } - }, - - /** - * Wraps the symbol counter if it reaches the specified length. - */ - wrapSymbol: function (length) { - if (this.symbol >= length) { - this.symbol = 0; - } - } -}; - - -/** * Utility method that sorts an object array and keeping the order of equal items. * ECMA script standard does not specify the behaviour when items are equal. */ function stableSort(arr, sortFunction) { var length = arr.length, @@ -746,18 +698,20 @@ } /** * Provide error messages for debugging, with links to online explanation */ -function error(code, stop) { +error = function (code, stop) { var msg = 'Highcharts error #' + code + ': www.highcharts.com/errors/' + code; if (stop) { throw msg; - } else if (win.console) { + } + // else ... + if (win.console) { console.log(msg); } -} +}; /** * Fix JS round off float errors * @param {Number} num */ @@ -778,22 +732,20 @@ } /** * The time unit lookup */ -/*jslint white: true*/ -timeUnits = hash( - MILLISECOND, 1, - SECOND, 1000, - MINUTE, 60000, - HOUR, 3600000, - DAY, 24 * 3600000, - WEEK, 7 * 24 * 3600000, - MONTH, 31 * 24 * 3600000, - YEAR, 31556952000 -); -/*jslint white: false*/ +timeUnits = { + millisecond: 1, + second: 1000, + minute: 60000, + hour: 3600000, + day: 24 * 3600000, + week: 7 * 24 * 3600000, + month: 31 * 24 * 3600000, + year: 31556952000 +}; /** * Path interpolation algorithm used across adapters */ pathAnim = { /** @@ -1004,13 +956,13 @@ function (arr, fn) { // modern browsers return Array.prototype.forEach.call(arr, fn); } : function (arr, fn) { // legacy - var i = 0, + var i, len = arr.length; - for (; i < len; i++) { + for (i = 0; i < len; i++) { if (fn.call(arr[i], arr[i], i, arr) === false) { return i; } } }; @@ -1296,11 +1248,11 @@ } }; defaultOptions = { colors: ['#7cb5ec', '#434348', '#90ed7d', '#f7a35c', - '#8085e9', '#f15c80', '#e4d354', '#8085e8', '#8d4653', '#91e8e1'], // docs + '#8085e9', '#f15c80', '#e4d354', '#8085e8', '#8d4653', '#91e8e1'], symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'], lang: { loading: 'Loading...', months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], @@ -1313,12 +1265,12 @@ thousandsSep: ',' }, global: { useUTC: true, //timezoneOffset: 0, - canvasToolsURL: 'http://code.highcharts.com/4.0.1/modules/canvas-tools.js', - VMLRadialGradientURL: 'http://code.highcharts.com/4.0.1/gfx/vml-radial-gradient.png' + canvasToolsURL: 'http://code.highcharts.com/4.0.3/modules/canvas-tools.js', + VMLRadialGradientURL: 'http://code.highcharts.com/4.0.3/gfx/vml-radial-gradient.png' }, chart: { //animation: true, //alignTicks: false, //reflow: true, @@ -1371,11 +1323,11 @@ margin: 15, // x: 0, // verticalAlign: 'top', // y: null, style: { - color: '#333333', // docs + color: '#333333', fontSize: '18px' } }, subtitle: { @@ -1384,11 +1336,11 @@ // floating: false // x: 0, // verticalAlign: 'top', // y: null, style: { - color: '#555555' // docs + color: '#555555' } }, plotOptions: { line: { // base series options @@ -1415,12 +1367,13 @@ radius: 4, lineColor: '#FFFFFF', //fillColor: null, states: { // states for a single point hover: { - enabled: true - //radius: base + 2 + enabled: true, + lineWidthPlus: 1, + radiusPlus: 2 }, select: { fillColor: '#FFFFFF', lineColor: '#000000', lineWidth: 2 @@ -1452,11 +1405,11 @@ //pointInterval: 1, //showInLegend: null, // auto: true for standalone series, false for linked series states: { // states for the entire series hover: { //enabled: false, - //lineWidth: base + 1, + lineWidthPlus: 1, marker: { // lineWidth: base + 1, // radius: base + 1 }, halo: { @@ -1496,11 +1449,11 @@ labelFormatter: function () { return this.name; }, //borderWidth: 0, borderColor: '#909090', - borderRadius: 0, // docs + borderRadius: 0, navigation: { // animation: true, activeColor: '#274b6d', // arrowSize: 12 inactiveColor: '#CCC' @@ -1512,13 +1465,13 @@ // backgroundColor: null, /*style: { padding: '5px' },*/ itemStyle: { - color: '#333333', // docs + color: '#333333', fontSize: '12px', - fontWeight: 'bold' // docs + fontWeight: 'bold' }, itemHoverStyle: { //cursor: 'pointer', removed as of #601 color: '#000' }, @@ -1549,11 +1502,11 @@ loading: { // hideDuration: 100, labelStyle: { fontWeight: 'bold', position: RELATIVE, - top: '1em' + top: '45%' }, // showDuration: 0, style: { position: ABSOLUTE, backgroundColor: 'white', @@ -1579,13 +1532,13 @@ month: '%B %Y', year: '%Y' }, //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/>', // docs + pointFormat: '<span style="color:{series.color}">\u25CF</span> {series.name}: <b>{point.y}</b><br/>', shadow: true, - //shape: 'calout', + //shape: 'callout', //shared: false, snap: isTouchDevice ? 25 : 10, style: { color: '#333333', cursor: 'default', @@ -1814,10 +1767,17 @@ * A wrapper object for SVG elements */ function SVGElement() {} SVGElement.prototype = { + + // 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'], + /** * Initialize the SVG renderer * @param {Object} renderer * @param {String} nodeName */ @@ -1826,15 +1786,12 @@ wrapper.element = nodeName === 'span' ? createElement(nodeName) : doc.createElementNS(SVG_NS, nodeName); wrapper.renderer = renderer; }, + /** - * Default base for animation - */ - opacity: 1, - /** * Animate a given attribute * @param {Object} params * @param {Number} options The same options as in jQuery animation * @param {Function} complete Function to perform at the end of animation */ @@ -1851,10 +1808,11 @@ this.attr(params); if (complete) { complete(); } } + return this; }, /** * Build an SVG gradient out of a common JavaScript configuration object */ @@ -2106,11 +2064,11 @@ var wrapper = this, key, attribs = {}, normalizer, - strokeWidth = rect.strokeWidth || wrapper.strokeWidth || (wrapper.attr && wrapper.attr('stroke-width')) || 0; + strokeWidth = rect.strokeWidth || wrapper.strokeWidth || 0; normalizer = mathRound(strokeWidth) % 2 / 2; // mathRound because strokeWidth can sometimes have roundoff errors // normalize for crisp edges rect.x = mathFloor(rect.x || wrapper.x || 0) + normalizer; @@ -2622,12 +2580,12 @@ each(shadows, function (shadow) { wrapper.safeRemoveChild(shadow); }); } - // In case of useHTML, clean up empty containers emulating SVG groups (#1960, #2393). - while (parentToClean && parentToClean.div.childNodes.length === 0) { + // In case of useHTML, clean up empty containers emulating SVG groups (#1960, #2393, #2697). + while (parentToClean && parentToClean.div && parentToClean.div.childNodes.length === 0) { grandParent = parentToClean.parentGroup; wrapper.safeRemoveChild(parentToClean.div); delete parentToClean.div; parentToClean = grandParent; } @@ -2710,11 +2668,11 @@ * for animation. */ _defaultGetter: function (key) { var ret = pick(this[key], this.element ? this.element.getAttribute(key) : null, 0); - if (/^[0-9\.]+$/.test(ret)) { // is numerical + if (/^[\-0-9\.]+$/.test(ret)) { // is numerical ret = parseFloat(ret); } return ret; }, @@ -2741,15 +2699,16 @@ .replace('shortdash', '3,1,') .replace('longdash', '8,3,') .replace(/dot/g, '1,3,') .replace('dash', '4,3,') .replace(/,$/, '') + .replace('solid', 1) .split(','); // ending comma i = value.length; while (i--) { - value[i] = pInt(value[i]) * this.element.getAttribute('stroke-width'); + value[i] = pInt(value[i]) * this['stroke-width']; } value = value.join(','); this.element.setAttribute('stroke-dasharray', value); } }, @@ -2758,19 +2717,10 @@ }, opacitySetter: function (value, key, element) { this[key] = value; element.setAttribute(key, value); }, - // In Chrome/Win < 6 as well as Batik and PhantomJS as of 1.9.7, the stroke attribute can't be set when the stroke- - // width is 0. #1369 - 'stroke-widthSetter': function (value, key, element) { - if (value === 0) { - value = 0.00001; - } - this.strokeWidth = value; // read in symbol paths like 'callout' - element.setAttribute(key, value); - }, titleSetter: function (value) { var titleNode = this.element.getElementsByTagName('title')[0]; if (!titleNode) { titleNode = doc.createElementNS(SVG_NS, 'title'); this.element.appendChild(titleNode); @@ -2787,11 +2737,10 @@ this.renderer.buildText(this); } } }, fillSetter: function (value, key, element) { - if (typeof value === 'string') { element.setAttribute(key, value); } else if (value) { this.colorGradient(value, key, element); } @@ -2811,28 +2760,26 @@ SVGElement.prototype.rotationSetter = SVGElement.prototype.verticalAlignSetter = SVGElement.prototype.scaleXSetter = SVGElement.prototype.scaleYSetter = function (value, key) { this[key] = value; this.doTransform = true; }; -SVGElement.prototype.strokeSetter = SVGElement.prototype.fillSetter; - - -// In Chrome/Win < 6 as well as Batik, the stroke attribute can't be set when the stroke- -// width is 0. #1369 -/*SVGElement.prototype['stroke-widthSetter'] = SVGElement.prototype.strokeSetter = function (value, key) { +// WebKit and Batik have problems with a stroke-width of zero, so in this case we remove the +// stroke attribute altogether. #1270, #1369, #3065, #3072. +SVGElement.prototype['stroke-widthSetter'] = SVGElement.prototype.strokeSetter = function (value, key, element) { this[key] = value; // Only apply the stroke attribute if the stroke width is defined and larger than 0 if (this.stroke && this['stroke-width']) { - this.element.setAttribute('stroke', this.stroke); - this.element.setAttribute('stroke-width', this['stroke-width']); + this.strokeWidth = this['stroke-width']; + SVGElement.prototype.fillSetter.call(this, this.stroke, 'stroke', element); // use prototype as instance may be overridden + element.setAttribute('stroke-width', this['stroke-width']); this.hasStroke = true; } else if (key === 'stroke-width' && value === 0 && this.hasStroke) { - this.element.removeAttribute('stroke'); + element.removeAttribute('stroke'); this.hasStroke = false; } -};*/ +}; /** * The default SVG renderer */ @@ -3000,28 +2947,31 @@ hrefRegex, parentX = attr(textNode, 'x'), textStyles = wrapper.styles, width = wrapper.textWidth, textLineHeight = textStyles && textStyles.lineHeight, + textStroke = textStyles && textStyles.HcTextStroke, i = childNodes.length, 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) + ((textStyles && textStyles.fontSize) || renderer.style.fontSize || 12), + tspan ).h; }; /// remove old text while (i--) { textNode.removeChild(childNodes[i]); } - // Skip tspans, add text directly to text node - if (!hasMarkup && textStr.indexOf(' ') === -1) { + // 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)); return; // Complex strings, add more logic } else { @@ -3092,10 +3042,13 @@ } // add attributes attr(tspan, attributes); + // Append it + textNode.appendChild(tspan); + // first span on subsequent line, add the line height if (!spanNo && lineNo) { // allow getting the right offset height in exporting in IE if (!hasSVG && forExport) { @@ -3105,31 +3058,23 @@ // Set the line height based on the font size of either // the text element or the tspan element attr( tspan, 'dy', - getLineHeight(tspan), - // Safari 6.0.2 - too optimized for its own good (#1539) - // TODO: revisit this with future versions of Safari - isWebKit && tspan.offsetHeight + getLineHeight(tspan) ); } - // Append it - textNode.appendChild(tspan); - - spanNo++; - // check width and apply soft breaks if (width) { var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273 - hasWhiteSpace = words.length > 1 && textStyles.whiteSpace !== 'nowrap', + hasWhiteSpace = spans.length > 1 || (words.length > 1 && textStyles.whiteSpace !== 'nowrap'), tooLong, actualWidth, - clipHeight = wrapper._clipHeight, + hcHeight = textStyles.HcHeight, rest = [], - dy = getLineHeight(), + dy = getLineHeight(tspan), softLineNo = 1, bBox; while (hasWhiteSpace && (words.length || rest.length)) { delete wrapper.bBox; // delete cache @@ -3145,12 +3090,11 @@ if (!tooLong || words.length === 1) { // new line needed words = rest; rest = []; if (words.length) { softLineNo++; - - if (clipHeight && softLineNo * dy > clipHeight) { + if (hcHeight && softLineNo * dy > hcHeight) { words = ['...']; wrapper.attr('title', wrapper.textStr); } else { tspan = doc.createElementNS(SVG_NS, 'tspan'); @@ -3160,25 +3104,26 @@ }); if (spanStyle) { // #390 attr(tspan, 'style', spanStyle); } textNode.appendChild(tspan); - - if (actualWidth > width) { // a single word is pressing it out - width = actualWidth; - } } } + if (actualWidth > width) { // a single word is pressing it out + width = actualWidth; + } } else { // append to existing line tspan tspan.removeChild(tspan.firstChild); rest.unshift(words.pop()); } if (words.length) { tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-'))); } } } + + spanNo++; } } }); }); } @@ -3501,12 +3446,11 @@ 'href', src); } else { // could be exporting in IE // using href throws "not supported" in ie7 and under, requries regex shim to fix later elemWrapper.element.setAttribute('hc-svg-href', src); - } - + } return elemWrapper; }, /** * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object. @@ -3821,18 +3765,19 @@ }); } if (!useHTML) { wrapper.xSetter = function (value, key, element) { - var childNodes = element.childNodes, - child, + var tspans = element.getElementsByTagName('tspan'), + tspan, + parentVal = element.getAttribute(key), i; - for (i = 1; i < childNodes.length; i++) { - child = childNodes[i]; - // if the x values are equal, the tspan represents a linebreak - if (child.getAttribute('x') === element.getAttribute('x')) { - child.setAttribute('x', value); + for (i = 0; i < tspans.length; i++) { + tspan = tspans[i]; + // If the x values are equal, the tspan represents a linebreak + if (tspan.getAttribute(key) === parentVal) { + tspan.setAttribute(key, value); } } element.setAttribute(key, value); }; } @@ -3841,22 +3786,27 @@ }, /** * Utility to return the baseline offset and total line height from the font size */ - fontMetrics: function (fontSize) { + fontMetrics: function (fontSize, elem) { fontSize = fontSize || this.style.fontSize; + if (elem && win.getComputedStyle) { + elem = elem.element || elem; // SVGElement + fontSize = win.getComputedStyle(elem, "").fontSize; + } fontSize = /px/.test(fontSize) ? pInt(fontSize) : /em/.test(fontSize) ? parseFloat(fontSize) * 12 : 12; // Empirical values found by comparing font size and bounding box height. // Applies to the default font family. http://jsfiddle.net/highcharts/7xvn7/ var lineHeight = fontSize < 24 ? fontSize + 4 : mathRound(fontSize * 1.2), baseline = mathRound(lineHeight * 0.8); return { h: lineHeight, - b: baseline + b: baseline, + f: fontSize }; }, /** * Add a label, a text item that can hold a colored or gradient background @@ -3909,11 +3859,11 @@ text.getBBox(); 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).b; + baselineOffset = padding + renderer.fontMetrics(style && style.fontSize, text).b; if (needsBox) { // create the border box if it is not already present @@ -4085,11 +4035,11 @@ */ css: function (styles) { if (styles) { var textStyles = {}; styles = merge(styles); // create a copy to avoid altering the original object (#537) - each(['fontSize', 'fontWeight', 'fontFamily', 'color', 'lineHeight', 'width', 'textDecoration', 'textShadow'], function (prop) { + each(wrapper.textProps, function (prop) { if (styles[prop] !== UNDEFINED) { textStyles[prop] = styles[prop]; delete styles[prop]; } }); @@ -4458,11 +4408,11 @@ if (!hasSVG && !useCanVG) { /** * The VML element wrapper. */ -Highcharts.VMLElement = VMLElement = { +VMLElement = { /** * Initialize a new VML element wrapper. It builds the markup as a string * to minimize DOM traffic. * @param {Object} renderer @@ -4823,11 +4773,11 @@ }, dSetter: function (value, key, element) { var i, shadows = this.shadows; value = value || []; - this.d = value.join(' '); // used in getter for animation + this.d = value.join && value.join(' '); // used in getter for animation element.path = value = this.pathToVML(value); // update shadows if (shadows) { @@ -4921,11 +4871,11 @@ }, zIndexSetter: function (value, key, element) { element.style[key] = value; } }; -VMLElement = extendClass(SVGElement, VMLElement); +Highcharts.VMLElement = VMLElement = extendClass(SVGElement, VMLElement); // Some shared setters VMLElement.prototype.ySetter = VMLElement.prototype.widthSetter = VMLElement.prototype.heightSetter = @@ -5603,10 +5553,11 @@ 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 && @@ -5649,18 +5600,18 @@ // first call if (!defined(label)) { attr = { align: axis.labelAlign }; - if (isNumber(labelOptions.rotation)) { - attr.rotation = labelOptions.rotation; + if (isNumber(rotation)) { + attr.rotation = rotation; } if (width && labelOptions.ellipsis) { - attr._clipHeight = axis.len / tickPositions.length; + css.HcHeight = axis.len / tickPositions.length; } - tick.label = + tick.label = label = defined(str) && labelOptions.enabled ? chart.renderer.text( str, 0, 0, @@ -5670,17 +5621,25 @@ // without position absolute, IE export sometimes is wrong .css(css) .add(axis.labelGroup) : null; + // 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); } + 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 */ @@ -5753,11 +5712,11 @@ // Find the firsth neighbour on the same line do { index += (isFirst ? 1 : -1); neighbour = axis.ticks[tickPositions[index]]; - } while (tickPositions[index] && (!neighbour || neighbour.label.line !== line)); + } 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? @@ -5818,29 +5777,17 @@ */ getLabelPosition: function (x, y, label, horiz, labelOptions, tickmarkOffset, index, step) { var axis = this.axis, transA = axis.transA, reversed = axis.reversed, - staggerLines = axis.staggerLines, - baseline = axis.chart.renderer.fontMetrics(labelOptions.style.fontSize).b, - rotation = labelOptions.rotation; + staggerLines = axis.staggerLines; x = x + labelOptions.x - (tickmarkOffset && horiz ? tickmarkOffset * transA * (reversed ? -1 : 1) : 0); - y = y + labelOptions.y - (tickmarkOffset && !horiz ? + y = y + this.yOffset - (tickmarkOffset && !horiz ? tickmarkOffset * transA * (reversed ? 1 : -1) : 0); - // Correct for rotation (#1764) - if (rotation && axis.side === 2) { - y -= baseline - baseline * mathCos(rotation * deg2rad); - } - - // Vertically centered - if (!defined(labelOptions.y) && !rotation) { // #1951 - y += baseline - label.getBBox().height / 2; - } - // Correct for staggered labels if (staggerLines) { label.line = (index / (step || 1) % staggerLines); y += label.line * (axis.labelOffset / staggerLines); } @@ -5902,10 +5849,11 @@ xy = tick.getPosition(horiz, pos, tickmarkOffset, old), x = xy.x, y = xy.y, reverseCrisp = ((horiz && x === axis.pos + axis.len) || (!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687 + opacity = pick(opacity, 1); this.isActive = true; // create the grid line if (gridLineWidth) { gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLineWidth * reverseCrisp, old, true); @@ -6157,12 +6105,13 @@ .css(optionsLabel.style) .add(); } // get the bounding box and align the label - xs = [path[1], path[4], pick(path[6], path[1])]; - ys = [path[2], path[5], pick(path[7], path[2])]; + // #3000 changed to better handle choice between plotband or plotline + xs = [path[1], path[4], (isBand ? path[6] : path[1])]; + ys = [path[2], path[5], (isBand ? path[7] : path[2])]; x = arrayMin(xs); y = arrayMin(ys); label.align(optionsLabel, false, { x: x, @@ -6218,15 +6167,15 @@ return path; }, addPlotBand: function (options) { - this.addPlotBandOrLine(options, 'plotBands'); + return this.addPlotBandOrLine(options, 'plotBands'); }, addPlotLine: function (options) { - this.addPlotBandOrLine(options, 'plotLines'); + return this.addPlotBandOrLine(options, 'plotLines'); }, /** * Add a plot band or plot line after render time * @@ -6431,11 +6380,11 @@ * These options extend the defaultOptions for bottom axes */ defaultBottomAxisOptions: { labels: { x: 0, - y: 20 + y: null // based on font size // overflow: undefined, // staggerLines: null }, title: { rotation: 0 @@ -7119,10 +7068,12 @@ */ setTickPositions: 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, @@ -7211,11 +7162,11 @@ // 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 && options.startOnTick && options.endOnTick) { + !this.isLog && !categories && startOnTick && endOnTick) { keepTwoTicksOnly = true; axis.tickInterval /= 4; // tick extremes closer to the real values } } @@ -7301,17 +7252,22 @@ var roundedMin = tickPositions[0], roundedMax = tickPositions[tickPositions.length - 1], minPointOffset = axis.minPointOffset || 0, singlePad; - if (options.startOnTick) { + // Prevent all ticks from being removed (#3195) + if (!startOnTick && !endOnTick && !categories && tickPositions.length === 2) { + tickPositions.splice(1, 0, (roundedMax + roundedMin) / 2); + } + + if (startOnTick) { axis.min = roundedMin; } else if (axis.min - minPointOffset > roundedMin) { tickPositions.shift(); } - if (options.endOnTick) { + if (endOnTick) { axis.max = roundedMax; } else if (axis.max + minPointOffset < roundedMax) { tickPositions.pop(); } @@ -7537,11 +7493,11 @@ horiz = this.horiz, width = pick(options.width, chart.plotWidth - offsetLeft + offsetRight), height = pick(options.height, chart.plotHeight), top = pick(options.top, chart.plotTop), left = pick(options.left, chart.plotLeft + offsetLeft), - percentRegex = /%$/; // docs + percentRegex = /%$/; // Check for percentage based input values if (percentRegex.test(height)) { height = parseInt(height, 10) / 100 * chart.plotHeight; } @@ -7636,10 +7592,11 @@ titleOffsetOption, titleMargin = 0, axisTitleOptions = options.title, labelOptions = options.labels, labelOffset = 0, // reset + labelOffsetPadded, axisOffset = chart.axisOffset, clipOffset = chart.clipOffset, directionFactor = [-1, 1, 1, -1][side], n, i, @@ -7651,11 +7608,11 @@ pos, bBox, x, w, lineNo, - lineHeightCorrection = side === 2 ? renderer.fontMetrics(labelOptions.style.fontSize).b : 0; + 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); @@ -7732,12 +7689,12 @@ labelOffset = mathMax( ticks[pos].getLabelSize(), labelOffset ); } - }); + if (axis.staggerLines) { labelOffset *= axis.staggerLines; axis.labelOffset = labelOffset; } @@ -7770,30 +7727,30 @@ axis.axisTitle.isNew = true; } if (showAxis) { titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width']; - titleMargin = pick(axisTitleOptions.margin, horiz ? 5 : 10); titleOffsetOption = axisTitleOptions.offset; + titleMargin = defined(titleOffsetOption) ? 0 : pick(axisTitleOptions.margin, horiz ? 5 : 10); } // hide or show the title depending on whether showEmpty is set axis.axisTitle[showAxis ? 'show' : 'hide'](); } // handle automatic or user set offset axis.offset = directionFactor * pick(options.offset, axisOffset[side]); - axis.axisTitleMargin = - pick(titleOffsetOption, - labelOffset + titleMargin + - (labelOffset && (directionFactor * options.labels[horiz ? 'y' : 'x'] - lineHeightCorrection)) - ); + lineHeightCorrection = side === 2 ? axis.tickBaseline : 0; + labelOffsetPadded = labelOffset + titleMargin + + (labelOffset && (directionFactor * (horiz ? pick(labelOptions.y, axis.tickBaseline + 8) : labelOptions.x) - lineHeightCorrection)); + axis.axisTitleMargin = pick(titleOffsetOption, labelOffsetPadded); axisOffset[side] = mathMax( axisOffset[side], - axis.axisTitleMargin + titleOffset + directionFactor * axis.offset + axis.axisTitleMargin + titleOffset + directionFactor * axis.offset, + labelOffsetPadded // #3027 ); clipOffset[invertedSide] = mathMax(clipOffset[invertedSide], mathFloor(options.lineWidth / 2) * 2); }, /** @@ -7959,11 +7916,11 @@ // render new ticks in old position if (slideInTicks && ticks[pos].isNew) { ticks[pos].render(i, true, 0.1); } - ticks[pos].render(i, false, 1); + ticks[pos].render(i); } }); // In a categorized axis, the tick marks are displayed between labels. So // we need to add a tick mark and grid line at the left edge of the X axis. @@ -8083,29 +8040,21 @@ /** * Redraw the axis to reflect changes in the data or axis extremes */ redraw: function () { - var axis = this, - chart = axis.chart, - pointer = chart.pointer; - - // hide tooltip and hover states - if (pointer) { - pointer.reset(true); - } - + // render the axis - axis.render(); + this.render(); // move plot lines and bands - each(axis.plotLinesAndBands, function (plotLine) { + each(this.plotLinesAndBands, function (plotLine) { plotLine.render(); }); // mark associated series as dirty and ready for redraw - each(axis.series, function (series) { + each(this.series, function (series) { series.isDirty = true; }); }, @@ -8238,44 +8187,44 @@ minDate = new Date(min - timezoneOffset), interval = normalizedInterval.unitRange, count = normalizedInterval.count; if (defined(min)) { // #1300 - if (interval >= timeUnits[SECOND]) { // second + if (interval >= timeUnits.second) { // second minDate.setMilliseconds(0); - minDate.setSeconds(interval >= timeUnits[MINUTE] ? 0 : + minDate.setSeconds(interval >= timeUnits.minute ? 0 : count * mathFloor(minDate.getSeconds() / count)); } - if (interval >= timeUnits[MINUTE]) { // minute - minDate[setMinutes](interval >= timeUnits[HOUR] ? 0 : + if (interval >= timeUnits.minute) { // minute + minDate[setMinutes](interval >= timeUnits.hour ? 0 : count * mathFloor(minDate[getMinutes]() / count)); } - if (interval >= timeUnits[HOUR]) { // hour - minDate[setHours](interval >= timeUnits[DAY] ? 0 : + if (interval >= timeUnits.hour) { // hour + minDate[setHours](interval >= timeUnits.day ? 0 : count * mathFloor(minDate[getHours]() / count)); } - if (interval >= timeUnits[DAY]) { // day - minDate[setDate](interval >= timeUnits[MONTH] ? 1 : + if (interval >= timeUnits.day) { // day + minDate[setDate](interval >= timeUnits.month ? 1 : count * mathFloor(minDate[getDate]() / count)); } - if (interval >= timeUnits[MONTH]) { // month - minDate[setMonth](interval >= timeUnits[YEAR] ? 0 : + if (interval >= timeUnits.month) { // month + minDate[setMonth](interval >= timeUnits.year ? 0 : count * mathFloor(minDate[getMonth]() / count)); minYear = minDate[getFullYear](); } - if (interval >= timeUnits[YEAR]) { // year + if (interval >= timeUnits.year) { // year minYear -= minYear % count; minDate[setFullYear](minYear); } // week is a special case that runs outside the hierarchy - if (interval === timeUnits[WEEK]) { + if (interval === timeUnits.week) { // get start of current week, independent of count minDate[setDate](minDate[getDate]() - minDate[getDay]() + pick(startOfWeek, 1)); } @@ -8296,22 +8245,22 @@ // iterate and add tick positions at appropriate values while (time < max) { tickPositions.push(time); // if the interval is years, use Date.UTC to increase years - if (interval === timeUnits[YEAR]) { + if (interval === timeUnits.year) { time = makeTime(minYear + i * count, 0); // if the interval is months, use Date.UTC to increase months - } else if (interval === timeUnits[MONTH]) { + } else if (interval === timeUnits.month) { time = makeTime(minYear, minMonth + i * count); // if we're using global time, the interval is not fixed as it jumps // one hour at the DST crossover - } else if (!useUTC && (interval === timeUnits[DAY] || interval === timeUnits[WEEK])) { + } else if (!useUTC && (interval === timeUnits.day || interval === timeUnits.week)) { time = makeTime(minYear, minMonth, minDateDate + - i * count * (interval === timeUnits[DAY] ? 1 : 7)); + i * count * (interval === timeUnits.day ? 1 : 7)); // else, the interval is fixed and we use simple addition } else { time += interval * count; } @@ -8323,13 +8272,13 @@ tickPositions.push(time); // mark new days if the time is dividible by day (#1649, #1760) each(grep(tickPositions, function (time) { - return interval <= timeUnits[HOUR] && time % timeUnits[DAY] === localTimezoneOffset; + return interval <= timeUnits.hour && time % timeUnits.day === localTimezoneOffset; }), function (time) { - higherRanks[time] = DAY; + higherRanks[time] = 'day'; }); } // record information on the chosen unit - for dynamic label formatter @@ -8349,32 +8298,32 @@ * prevent it for running over again for each segment having the same interval. * #662, #697. */ Axis.prototype.normalizeTimeTickInterval = function (tickInterval, unitsOption) { var units = unitsOption || [[ - MILLISECOND, // unit name + 'millisecond', // unit name [1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples ], [ - SECOND, + 'second', [1, 2, 5, 10, 15, 30] ], [ - MINUTE, + 'minute', [1, 2, 5, 10, 15, 30] ], [ - HOUR, + 'hour', [1, 2, 3, 4, 6, 8, 12] ], [ - DAY, + 'day', [1, 2] ], [ - WEEK, + 'week', [1, 2] ], [ - MONTH, + 'month', [1, 2, 3, 4, 6] ], [ - YEAR, + 'year', null ]], unit = units[units.length - 1], // default unit is years interval = timeUnits[unit[0]], multiples = unit[1], @@ -8399,19 +8348,19 @@ } } } // prevent 2.5 years intervals, though 25, 250 etc. are allowed - if (interval === timeUnits[YEAR] && tickInterval < 5 * interval) { + if (interval === timeUnits.year && tickInterval < 5 * interval) { multiples = [1, 2, 5]; } // get the count count = normalizeTickInterval( tickInterval / interval, multiples, - unit[0] === YEAR ? mathMax(getMagnitude(tickInterval / interval), 1) : 1 // #1913, #2360 + unit[0] === 'year' ? mathMax(getMagnitude(tickInterval / interval), 1) : 1 // #1913, #2360 ); return { unitRange: interval, count: count, @@ -8464,12 +8413,11 @@ for (i = roundedMin; i < max + 1 && !break2; i++) { len = intermediate.length; for (j = 0; j < len && !break2; j++) { pos = log2lin(lin2log(i) * intermediate[j]); - - if (pos > min && (!minor || lastPos <= max)) { // #1670 + if (pos > min && (!minor || lastPos <= max) && lastPos !== UNDEFINED) { // #1670, lastPos is #3113 positions.push(lastPos); } if (lastPos > max) { break2 = true; @@ -8595,32 +8543,34 @@ * @private */ move: function (x, y, anchorX, anchorY) { var tooltip = this, now = tooltip.now, - animate = tooltip.options.animation !== false && !tooltip.isHidden, + animate = tooltip.options.animation !== false && !tooltip.isHidden && + // When we get close to the target position, abort animation and land on the right place (#3056) + (mathAbs(x - now.x) > 1 || mathAbs(y - now.y) > 1), skipAnchor = tooltip.followPointer || tooltip.len > 1; - // get intermediate values for animation + // Get intermediate values for animation extend(now, { x: animate ? (2 * now.x + x) / 3 : x, y: animate ? (now.y + y) / 2 : y, anchorX: skipAnchor ? UNDEFINED : animate ? (2 * now.anchorX + anchorX) / 3 : anchorX, anchorY: skipAnchor ? UNDEFINED : animate ? (now.anchorY + anchorY) / 2 : anchorY }); - // move to the intermediate value + // Move to the intermediate value tooltip.label.attr(now); - // run on next tick of the mouse tracker - if (animate && (mathAbs(x - now.x) > 1 || mathAbs(y - now.y) > 1)) { + // Run on next tick of the mouse tracker + if (animate) { - // never allow two timeouts + // Never allow two timeouts clearTimeout(this.tooltipTimeout); - // set the fixed interval ticking for the smooth tooltip + // Set the fixed interval ticking for the smooth tooltip this.tooltipTimeout = setTimeout(function () { // The interval function may still be running during destroy, so check that the chart is really there before calling. if (tooltip) { tooltip.move(x, y, anchorX, anchorY); } @@ -8965,11 +8915,11 @@ 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)) { + (timeUnits[n] <= timeUnits.day && point.key % timeUnits[n] > 0)) { xDateFormat = dateTimeLabelFormats[n]; break; } } } else { @@ -9324,11 +9274,12 @@ plotWidth = chart.plotWidth, plotHeight = chart.plotHeight, clickedInside, size, mouseDownX = this.mouseDownX, - mouseDownY = this.mouseDownY; + mouseDownY = this.mouseDownY, + panKey = chartOptions.panKey && e[chartOptions.panKey + 'Key']; // If the mouse is outside the plot area, adjust to cooordinates // inside to prevent the selection marker from going outside if (chartX < plotLeft) { chartX = plotLeft; @@ -9350,11 +9301,11 @@ if (this.hasDragged > 10) { clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop); // make a selection - if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside) { + if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside && !panKey) { if (!this.selectionMarker) { this.selectionMarker = chart.renderer.rect( plotLeft, plotTop, zoomHor ? 1 : plotWidth, @@ -9418,12 +9369,13 @@ // record each axis' min and max each(chart.axes, function (axis) { if (axis.zoomEnabled) { var horiz = axis.horiz, - selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop)), - selectionMax = axis.toValue((horiz ? selectionLeft + selectionWidth : selectionTop + selectionHeight)); + 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, @@ -9511,12 +9463,12 @@ var chart = this.chart; hoverChartIndex = chart.index; - // normalize e = this.normalize(e); + e.returnValue = false; // #2251, #3224 if (chart.mouseIsDown === 'mousedown') { this.drag(e); } @@ -9788,12 +9740,12 @@ // Identify the data bounds in pixels each(chart.axes, function (axis) { if (axis.zoomEnabled) { var bounds = chart.bounds[axis.horiz ? 'h' : 'v'], minPixelPadding = axis.minPixelPadding, - min = axis.toPixels(axis.dataMin), - max = axis.toPixels(axis.dataMax), + min = axis.toPixels(pick(axis.options.min, axis.dataMin)), + max = axis.toPixels(pick(axis.options.max, axis.dataMax)), absMin = mathMin(min, max), absMax = mathMax(min, max); // Store the bounds for use in the touchmove handler bounds.min = mathMin(axis.pos, absMin - minPixelPadding); @@ -9979,11 +9931,10 @@ if (!options.enabled) { return; } - legend.baseline = pInt(itemStyle.fontSize) + 3 + itemMarginTop; // used in Series prototype legend.itemStyle = itemStyle; legend.itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle); legend.itemMarginTop = itemMarginTop; legend.padding = padding; legend.initialItemX = padding; @@ -10177,11 +10128,11 @@ symbolWidth = legend.symbolWidth, symbolPadding = options.symbolPadding, itemStyle = legend.itemStyle, itemHiddenStyle = legend.itemHiddenStyle, padding = legend.padding, - itemDistance = horizontal ? pick(options.itemDistance, 20) : 0, // docs + itemDistance = horizontal ? pick(options.itemDistance, 20) : 0, ltr = !options.rtl, itemHeight, widthOption = options.width, itemMarginBottom = options.itemMarginBottom || 0, itemMarginTop = legend.itemMarginTop, @@ -10200,27 +10151,33 @@ // A group to hold the symbol and text. Text is to be appended in Legend class. item.legendGroup = renderer.g('legend-item') .attr({ zIndex: 1 }) .add(legend.scrollGroup); - // Draw the legend symbol inside the group box - series.drawLegendSymbol(legend, item); - // Generate the list item text and add it to the group item.legendItem = li = renderer.text( options.labelFormat ? format(options.labelFormat, item) : options.labelFormatter.call(item), ltr ? symbolWidth + symbolPadding : -symbolPadding, - legend.baseline, + legend.baseline || 0, useHTML ) .css(merge(item.visible ? itemStyle : itemHiddenStyle)) // merge to prevent modifying original (#1021) .attr({ align: ltr ? 'left' : 'right', zIndex: 2 }) .add(item.legendGroup); + // Get the baseline for the first item - the font size is equal for all + if (!legend.baseline) { + legend.baseline = renderer.fontMetrics(itemStyle.fontSize, li).f + 3 + itemMarginTop; + li.attr('y', legend.baseline); + } + + // Draw the legend symbol inside the group box + series.drawLegendSymbol(legend, item); + if (legend.setItemEvents) { legend.setItemEvents(item, li, useHTML, itemStyle, itemHiddenStyle); } // Colorize the items @@ -10469,11 +10426,11 @@ // Reset the legend height and adjust the clipping rectangle pages.length = 0; if (legendHeight > spaceHeight && !options.useHTML) { - this.clipHeight = clipHeight = spaceHeight - 20 - this.titleHeight - this.padding; + this.clipHeight = clipHeight = mathMax(spaceHeight - 20 - this.titleHeight - this.padding, 0); this.currentPage = pick(this.currentPage, 1); this.fullHeight = legendHeight; // Fill pages with Y positions so that the top of each a legend item defines // the scroll top for each page (#2098) @@ -10646,13 +10603,13 @@ legendOptions = legend.options, legendSymbol, symbolWidth = legend.symbolWidth, renderer = this.chart.renderer, legendItemGroup = this.legendGroup, - verticalCenter = legend.baseline - mathRound(renderer.fontMetrics(legendOptions.itemStyle.fontSize).b * 0.3), + verticalCenter = legend.baseline - mathRound(renderer.fontMetrics(legendOptions.itemStyle.fontSize, this.legendItem).b * 0.3), attr; - + // Draw the line if (options.lineWidth) { attr = { 'stroke-width': options.lineWidth }; @@ -10806,12 +10763,11 @@ chart.xAxis = []; chart.yAxis = []; // Expose methods and variables chart.animation = useCanVG ? false : pick(optionsChart.animation, true); - chart.pointCount = 0; - chart.counters = new ChartCounters(); + chart.pointCount = chart.colorCounter = chart.symbolCounter = 0; chart.firstRender(); }, /** @@ -10876,10 +10832,11 @@ pointer = chart.pointer, legend = chart.legend, redrawLegend = chart.isDirtyLegend, hasStackedSeries, hasDirtyStacks, + hasCartesianSeries = chart.hasCartesianSeries, isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed? seriesLength = series.length, i = seriesLength, serie, renderer = chart.renderer, @@ -10939,11 +10896,11 @@ if (hasStackedSeries) { chart.getStacks(); } - if (chart.hasCartesianSeries) { + if (hasCartesianSeries) { if (!chart.isResizing) { // reset maxTicks chart.maxTicks = null; @@ -10952,12 +10909,15 @@ axis.setScale(); }); } chart.adjustTickAmounts(); - chart.getMargins(); + } + chart.getMargins(); // #3098 + + if (hasCartesianSeries) { // If one axis is dirty, all axes must be redrawn (#792, #2169) each(axes, function (axis) { if (axis.isDirty) { isDirtyBox = true; } @@ -10977,13 +10937,12 @@ if (isDirtyBox || hasStackedSeries) { axis.redraw(); } }); - - } + // the plot areas size has changed if (isDirtyBox) { chart.drawChartBox(); } @@ -11188,25 +11147,30 @@ subtitle = this.subtitle, options = this.options, titleOptions = options.title, subtitleOptions = options.subtitle, requiresDirtyBox, + renderer = this.renderer, autoWidth = this.spacingBox.width - 44; // 44 makes room for default context button if (title) { title .css({ width: (titleOptions.width || autoWidth) + PX }) - .align(extend({ y: 15 }, titleOptions), false, 'spacingBox'); + .align(extend({ + y: renderer.fontMetrics(titleOptions.style.fontSize, title).b - 3 + }, titleOptions), false, 'spacingBox'); if (!titleOptions.floating && !titleOptions.verticalAlign) { titleOffset = title.getBBox().height; } } if (subtitle) { subtitle .css({ width: (subtitleOptions.width || autoWidth) + PX }) - .align(extend({ y: titleOffset + titleOptions.margin }, subtitleOptions), false, 'spacingBox'); + .align(extend({ + y: titleOffset + (titleOptions.margin - 13) + renderer.fontMetrics(titleOptions.style.fontSize, subtitle).b + }, subtitleOptions), false, 'spacingBox'); if (!subtitleOptions.floating && !subtitleOptions.verticalAlign) { titleOffset = mathCeil(titleOffset + subtitle.getBBox().height); } } @@ -11645,11 +11609,11 @@ clipY = mathCeil(mathMax(plotBorderWidth, clipOffset[0]) / 2); chart.clipBox = { x: clipX, y: clipY, width: mathFloor(chart.plotSizeX - mathMax(plotBorderWidth, clipOffset[1]) / 2 - clipX), - height: mathFloor(chart.plotSizeY - mathMax(plotBorderWidth, clipOffset[2]) / 2 - clipY) + height: mathMax(0, mathFloor(chart.plotSizeY - mathMax(plotBorderWidth, clipOffset[2]) / 2 - clipY)) }; if (!skipAxes) { each(chart.axes, function (axis) { axis.setAxisSize(); @@ -11865,24 +11829,49 @@ serie.setTooltipPoints(); } serie.render(); }); }, + + /** + * Render labels for the chart + */ + renderLabels: function () { + var chart = this, + labels = chart.options.labels; + if (labels.items) { + each(labels.items, function (label) { + var style = extend(labels.style, label.style), + x = pInt(style.left) + chart.plotLeft, + y = pInt(style.top) + chart.plotTop + 12; + // delete to prevent rewriting in IE + delete style.left; + delete style.top; + + chart.renderer.text( + label.html, + x, + y + ) + .attr({ zIndex: 2 }) + .css(style) + .add(); + + }); + } + }, + /** * Render all graphics for the chart */ render: function () { var chart = this, axes = chart.axes, renderer = chart.renderer, options = chart.options; - var labels = options.labels, - credits = options.credits, - creditsHref; - // Title chart.setTitle(); // Legend @@ -11925,57 +11914,43 @@ .add(); } chart.renderSeries(); // Labels - if (labels.items) { - each(labels.items, function (label) { - var style = extend(labels.style, label.style), - x = pInt(style.left) + chart.plotLeft, - y = pInt(style.top) + chart.plotTop + 12; + chart.renderLabels(); - // delete to prevent rewriting in IE - delete style.left; - delete style.top; + // Credits + chart.showCredits(options.credits); - renderer.text( - label.html, - x, - y - ) - .attr({ zIndex: 2 }) - .css(style) - .add(); + // Set flag + chart.hasRendered = true; - }); - } + }, - // Credits - if (credits.enabled && !chart.credits) { - creditsHref = credits.href; - chart.credits = renderer.text( + /** + * Show chart credits based on config options + */ + showCredits: function (credits) { + if (credits.enabled && !this.credits) { + this.credits = this.renderer.text( credits.text, 0, 0 ) .on('click', function () { - if (creditsHref) { - location.href = creditsHref; + if (credits.href) { + location.href = credits.href; } }) .attr({ align: credits.position.align, zIndex: 8 }) .css(credits.style) .add() .align(credits.position); } - - // Set flag - chart.hasRendered = true; - }, /** * Clean up memory usage */ @@ -12221,11 +12196,11 @@ * @param {Object} options */ applyOptions: function (options, x) { var point = this, series = point.series, - pointValKey = series.pointValKey; + pointValKey = series.options.pointValKey || series.pointValKey; options = Point.prototype.optionsToObject.call(this, options); // copy options directly to point extend(point, options); @@ -12692,64 +12667,49 @@ } return options; }, - /** - * Get the series' color - */ - getColor: function () { - var options = this.options, + + getCyclic: function (prop, value, defaults) { + var i, userOptions = this.userOptions, - defaultColors = this.chart.options.colors, - counters = this.chart.counters, - color, - colorIndex; + indexName = '_' + prop + 'Index', + counterName = prop + 'Counter'; - color = options.color || defaultPlotOptions[this.type].color; - - if (!color && !options.colorByPoint) { - if (defined(userOptions._colorIndex)) { // after Series.update() - colorIndex = userOptions._colorIndex; + if (!value) { + if (defined(userOptions[indexName])) { // after Series.update() + i = userOptions[indexName]; } else { - userOptions._colorIndex = counters.color; - colorIndex = counters.color++; + userOptions[indexName] = i = this.chart[counterName] % defaults.length; + this.chart[counterName] += 1; } - color = defaultColors[colorIndex]; + value = defaults[i]; } + this[prop] = value; + }, - this.color = color; - counters.wrapColor(defaultColors.length); + /** + * Get the series' color + */ + getColor: function () { + if (!this.options.colorByPoint) { + this.getCyclic('color', this.options.color || defaultPlotOptions[this.type].color, this.chart.options.colors); + } }, /** * Get the series' symbol */ getSymbol: function () { - var series = this, - userOptions = series.userOptions, - seriesMarkerOption = series.options.marker, - chart = series.chart, - defaultSymbols = chart.options.symbols, - counters = chart.counters, - symbolIndex; + var seriesMarkerOption = this.options.marker; - series.symbol = seriesMarkerOption.symbol; - if (!series.symbol) { - if (defined(userOptions._symbolIndex)) { // after Series.update() - symbolIndex = userOptions._symbolIndex; - } else { - userOptions._symbolIndex = counters.symbol; - symbolIndex = counters.symbol++; - } - series.symbol = defaultSymbols[symbolIndex]; - } + this.getCyclic('symbol', seriesMarkerOption.symbol, this.chart.options.symbols); // don't substract radius in image symbols (#604) - if (/^url/.test(series.symbol)) { + if (/^url/.test(this.symbol)) { seriesMarkerOption.radius = 0; } - counters.wrapSymbol(defaultSymbols.length); }, drawLegendSymbol: LegendSymbolMixin.drawLineMarker, /** @@ -12907,10 +12867,11 @@ i, // loop variable options = series.options, cropThreshold = options.cropThreshold, activePointCount = 0, isCartesian = series.isCartesian, + xExtremes, min, max; // If the series data or axes haven't changed, don't go through this. Return false to pass // the message on to override methods like in data grouping. @@ -12920,12 +12881,13 @@ // optionally filter out points outside the plot area if (isCartesian && series.sorted && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) { - min = xAxis.min; - max = xAxis.max; + xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053) + min = xExtremes.min; + max = xExtremes.max; // it's outside current extremes if (processedXData[dataLength - 1] < min || processedXData[0] > max) { processedXData = []; processedYData = []; @@ -13451,12 +13413,12 @@ // series type specific modifications if (seriesOptions.marker) { // line, spline, area, areaspline, scatter // if no hover radius is given, default to normal radius + 2 - stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + 2; - stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + 1; + stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + stateOptionsHover.radiusPlus; + stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + stateOptionsHover.lineWidthPlus; } else { // column, bar, pie // if no hover color is given, brighten the normal color stateOptionsHover.color = stateOptionsHover.color || @@ -14430,14 +14392,23 @@ * @param {String} str An optional text to show in the loading label instead of the default one */ showLoading: function (str) { var chart = this, options = chart.options, - loadingDiv = chart.loadingDiv; + loadingDiv = chart.loadingDiv, + loadingOptions = options.loading, + setLoadingSize = function () { + if (loadingDiv) { + css(loadingDiv, { + left: chart.plotLeft + PX, + top: chart.plotTop + PX, + width: chart.plotWidth + PX, + height: chart.plotHeight + PX + }); + } + }; - var loadingOptions = options.loading; - // create the layer at the first call if (!loadingDiv) { chart.loadingDiv = loadingDiv = createElement(DIV, { className: PREFIX + 'loading' }, extend(loadingOptions.style, { @@ -14449,33 +14420,30 @@ 'span', null, loadingOptions.labelStyle, loadingDiv ); - + addEvent(chart, 'redraw', setLoadingSize); // #1080 } // update text chart.loadingSpan.innerHTML = str || options.lang.loading; // show it if (!chart.loadingShown) { css(loadingDiv, { opacity: 0, - display: '', - left: chart.plotLeft + PX, - top: chart.plotTop + PX, - width: chart.plotWidth + PX, - height: chart.plotHeight + PX + display: '' }); animate(loadingDiv, { opacity: loadingOptions.style.opacity }, { duration: loadingOptions.showDuration || 0 }); chart.loadingShown = true; } + setLoadingSize(); }, /** * Hide the loading layer */ @@ -14740,18 +14708,26 @@ /** * Update the series with a new set of options */ update: function (newOptions, redraw) { - var chart = this.chart, + var series = this, + chart = this.chart, // must use user options when changing type because this.options is merged // in with type specific plotOptions oldOptions = this.userOptions, oldType = this.type, proto = seriesTypes[oldType].prototype, + preserve = ['group', 'markerGroup', 'dataLabelsGroup'], n; + // Make sure groups are not destroyed (#3094) + each(preserve, function (prop) { + preserve[prop] = series[prop]; + delete series[prop]; + }); + // Do the merge, with some forced options newOptions = merge(oldOptions, { animation: false, index: this.index, pointStart: this.xData[0] // when updating after addPoint @@ -14764,12 +14740,18 @@ 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); } } }); @@ -14872,22 +14854,22 @@ * For stacks, don't split segments on null values. Instead, draw null values with * no marker. Also insert dummy points for any X position that exists in other series * in the stack. */ getSegments: function () { - var segments = [], + var series = this, + segments = [], segment = [], keys = [], xAxis = this.xAxis, yAxis = this.yAxis, stack = yAxis.stacks[this.stackKey], pointMap = {}, plotX, plotY, points = this.points, connectNulls = this.options.connectNulls, - val, i, x; if (this.options.stacking && !this.cropped) { // cropped causes artefacts in Stock, and perf issue // Create a map where we can quickly look up the points by their X value. @@ -14904,10 +14886,13 @@ keys.sort(function (a, b) { return a - b; }); each(keys, function (x) { + var y = 0, + stackPoint; + if (connectNulls && (!pointMap[x] || pointMap[x].y === null)) { // #1836 return; // The point exists, push it to the segment } else if (pointMap[x]) { @@ -14915,13 +14900,23 @@ // There is no point for this X value in this series, so we // insert a dummy point in order for the areas to be drawn // correctly. } else { + + // Loop down the stack to find the series below this one that has + // a value (#1991) + for (i = series.index; i <= yAxis.series.length; i++) { + stackPoint = stack[x].points[i + ',' + x]; + if (stackPoint) { + y = stackPoint[1]; + break; + } + } + plotX = xAxis.translate(x); - val = stack[x].percent ? (stack[x].total ? stack[x].cum * 100 / stack[x].total : 0) : stack[x].cum; // #1991 - plotY = yAxis.toPixels(val, true); + plotY = yAxis.toPixels(y, true); segment.push({ y: null, plotX: plotX, clientX: plotX, plotY: plotY, @@ -15353,32 +15348,38 @@ threshold = options.threshold, translatedThreshold = series.translatedThreshold = yAxis.getThreshold(threshold), minPointLength = pick(options.minPointLength, 5), metrics = series.getColumnMetrics(), pointWidth = metrics.width, - seriesBarW = series.barW = mathCeil(mathMax(pointWidth, 1 + 2 * borderWidth)), // rounded and postprocessed for border width + seriesBarW = series.barW = mathMax(pointWidth, 1 + 2 * borderWidth), // postprocessed for border width pointXOffset = series.pointXOffset = metrics.offset, xCrisp = -(borderWidth % 2 ? 0.5 : 0), yCrisp = borderWidth % 2 ? 0.5 : 1; if (chart.renderer.isVML && chart.inverted) { yCrisp += 1; } + // When the pointPadding is 0, we want the columns to be packed tightly, so we allow individual + // columns to have individual sizes. When pointPadding is greater, we strive for equal-width + // columns (#2694). + if (options.pointPadding) { + seriesBarW = mathCeil(seriesBarW); + } + Series.prototype.translate.apply(series); - // record the new values + // Record the new values each(series.points, function (point) { var yBottom = pick(point.yBottom, translatedThreshold), plotY = mathMin(mathMax(-999 - yBottom, point.plotY), yAxis.len + 999 + yBottom), // Don't draw too far outside plot area (#1303, #2241) barX = point.plotX + pointXOffset, barW = seriesBarW, barY = mathMin(plotY, yBottom), right, bottom, fromTop, - fromLeft, barH = mathMax(plotY, yBottom) - barY; // Handle options.minPointLength if (mathAbs(barH) < minPointLength) { if (minPointLength) { @@ -15395,26 +15396,21 @@ point.pointWidth = pointWidth; // Fix the tooltip on center of grouped columns (#1216) point.tooltipPos = chart.inverted ? [yAxis.len - plotY, series.xAxis.len - barX - barW / 2] : [barX + barW / 2, plotY]; - // Round off to obtain crisp edges - fromLeft = mathAbs(barX) < 0.5; + // 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; barY = mathRound(barY) + yCrisp; barH = bottom - barY; - // Top and left edges are exceptions - if (fromLeft) { - barX += 1; - barW -= 1; - } + // Top edges are exceptions if (fromTop) { barY -= 1; barH += 1; } @@ -15424,10 +15420,11 @@ x: barX, y: barY, width: barW, height: barH }; + }); }, getSymbol: noop, @@ -15453,24 +15450,27 @@ chart = this.chart, options = series.options, renderer = chart.renderer, animationLimit = options.animationLimit || 250, shapeArgs, - pointAttr, - borderAttr; + pointAttr; // draw the columns each(series.points, function (point) { var plotY = point.plotY, - graphic = point.graphic; + graphic = point.graphic, + borderAttr; if (plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) { shapeArgs = point.shapeArgs; + borderAttr = defined(series.borderWidth) ? { 'stroke-width': series.borderWidth } : {}; + pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] || series.pointAttr[NORMAL_STATE]; + if (graphic) { // update stop(graphic); graphic.attr(borderAttr)[chart.pointCount < animationLimit ? 'animate' : 'attr'](merge(shapeArgs)); } else { @@ -15560,11 +15560,11 @@ * Set the default options for scatter */ defaultPlotOptions.scatter = merge(defaultSeriesOptions, { lineWidth: 0, tooltip: { - headerFormat: '<span style="color:{series.color}">\u25CF</span> <span style="font-size: 10px;"> {series.name}</span><br/>', // docs + 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 }); @@ -15574,11 +15574,11 @@ var ScatterSeries = extendClass(Series, { type: 'scatter', sorted: false, requireSorting: false, noSharedTooltip: true, - trackerGroups: ['markerGroup'], + trackerGroups: ['markerGroup', 'dataLabelsGroup'], takeOrdinalPosition: false, // #2342 singularTooltips: true, drawGraph: function () { if (this.options.lineWidth) { Series.prototype.drawGraph.call(this); @@ -15602,11 +15602,11 @@ // connectorWidth: 1, // connectorColor: point.color, // connectorPadding: 5, distance: 30, enabled: true, - formatter: function () { + formatter: function () { // #2945 return this.point.name; } // softConnector: true, //y: 0 }, @@ -15731,11 +15731,11 @@ haloPath: function (size) { var shapeArgs = this.shapeArgs, chart = this.series.chart; - return this.series.chart.renderer.symbols.arc(chart.plotLeft + shapeArgs.x, chart.plotTop + shapeArgs.y, shapeArgs.r + size, shapeArgs.r + size, { + return this.sliced || !this.visible ? [] : this.series.chart.renderer.symbols.arc(chart.plotLeft + shapeArgs.x, chart.plotTop + shapeArgs.y, shapeArgs.r + size, shapeArgs.r + size, { innerR: this.shapeArgs.r, start: shapeArgs.start, end: shapeArgs.end }); } @@ -16080,18 +16080,21 @@ // Create a separate group for the data labels to avoid rotation dataLabelsGroup = series.plotGroup( 'dataLabelsGroup', 'data-labels', - HIDDEN, + options.defer ? HIDDEN : VISIBLE, options.zIndex || 6 ); if (!series.hasRendered && pick(options.defer, true)) { dataLabelsGroup.attr({ opacity: 0 }); addEvent(series, 'afterAnimate', function () { - series.dataLabelsGroup.show()[seriesOptions.animation ? 'animate' : 'attr']({ opacity: 1 }, { duration: 200 }); + if (series.visible) { // #3023, #3024 + dataLabelsGroup.show(); + } + dataLabelsGroup[seriesOptions.animation ? 'animate' : 'attr']({ opacity: 1 }, { duration: 200 }); }); } // Make the labels for each point generalOptions = options; @@ -16226,16 +16229,17 @@ 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 - alignAttr = { - align: options.align, - x: alignTo.x + options.x + alignTo.width / 2, - y: alignTo.y + options.y + alignTo.height / 2 - }; - dataLabel[isNew ? 'attr' : 'animate'](alignAttr); + dataLabel[isNew ? 'attr' : 'animate']({ + x: alignTo.x + options.x + alignTo.width / 2, + y: alignTo.y + options.y + alignTo.height / 2 + }) + .attr({ // #3003 + align: options.align + }); } else { dataLabel.align(options, null, alignTo); alignAttr = dataLabel.alignAttr; // Handle justify or crop @@ -16373,17 +16377,10 @@ if (point.dataLabel && point.visible) { // #407, #2510 halves[point.half].push(point); } }); - // assume equal label heights - i = 0; - while (!labelHeight && data[i]) { // #1569 - labelHeight = data[i] && data[i].dataLabel && (data[i].dataLabel.getBBox().height || 21); // 21 is for #968 - i++; - } - /* Loop over the points in each half, starting from the top and bottom * of the pie to detect overlapping labels. */ i = 2; while (i--) { @@ -16391,42 +16388,69 @@ var slots = [], slotsLength, usedSlots = [], points = halves[i], pos, + bottom, length = points.length, slotIndex; + if (!length) { + continue; + } + // Sort by angle series.sortByAngle(points, i - 0.5); + // Assume equal label heights on either hemisphere (#2630) + j = labelHeight = 0; + while (!labelHeight && points[j]) { // #1569 + labelHeight = points[j] && points[j].dataLabel && (points[j].dataLabel.getBBox().height || 21); // 21 is for #968 + j++; + } + // Only do anti-collision when we are outside the pie and have connectors (#856) if (distanceOption > 0) { - // build the slots - for (pos = centerY - radius - distanceOption; pos <= centerY + radius + distanceOption; pos += labelHeight) { + // Build the slots + bottom = mathMin(centerY + radius + distanceOption, chart.plotHeight); + for (pos = mathMax(0, centerY - radius - distanceOption); pos <= bottom; pos += labelHeight) { slots.push(pos); + } + slotsLength = slots.length; - // visualize the slot - /* + + /* Visualize the slots + if (!series.slotElements) { + series.slotElements = []; + } + if (i === 1) { + series.slotElements.forEach(function (elem) { + elem.destroy(); + }); + series.slotElements.length = 0; + } + + slots.forEach(function (pos, no) { var slotX = series.getX(pos, i) + chart.plotLeft - (i ? 100 : 0), slotY = pos + chart.plotTop; + if (!isNaN(slotX)) { - chart.renderer.rect(slotX, slotY - 7, 100, labelHeight, 1) + series.slotElements.push(chart.renderer.rect(slotX, slotY - 7, 100, labelHeight, 1) .attr({ 'stroke-width': 1, - stroke: 'silver' + stroke: 'silver', + fill: 'rgba(0,0,255,0.1)' }) - .add(); - chart.renderer.text('Slot '+ (slots.length - 1), slotX, slotY + 4) + .add()); + series.slotElements.push(chart.renderer.text('Slot '+ no, slotX, slotY + 4) .attr({ fill: 'silver' - }).add(); + }).add()); } - */ - } - slotsLength = slots.length; + }); + // */ // if there are more values than available slots, remove lowest values if (length > slotsLength) { // create an array for sorting and ranking the points within each quarter rankArr = [].concat(points); @@ -16506,22 +16530,22 @@ // if the slot next to currrent slot is free, the y value is allowed // to fall back to the natural position y = slot.y; if ((naturalY > y && slots[slotIndex + 1] !== null) || (naturalY < y && slots[slotIndex - 1] !== null)) { - y = naturalY; + y = mathMin(mathMax(0, naturalY), chart.plotHeight); } } else { y = naturalY; } // get the x - use the natural x position for first and last slot, to prevent the top // and botton slice connectors from touching each other on either side x = options.justify ? seriesCenter[0] + (i ? -1 : 1) * (radius + distanceOption) : - series.getX(slotIndex === 0 || slotIndex === slots.length - 1 ? naturalY : y, i); + series.getX(y === centerY - radius - distanceOption || y === centerY + radius + distanceOption ? naturalY : y, i); // Record the placement and visibility dataLabel._attr = { visibility: visibility, @@ -17190,13 +17214,13 @@ */ onMouseOut: function () { var chart = this.series.chart, hoverPoints = chart.hoverPoints; - if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887 - this.firePointEvent('mouseOut'); + this.firePointEvent('mouseOut'); + if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887, #2240 this.setState(); chart.hoverPoint = null; } }, @@ -17437,10 +17461,10 @@ if (stateOptions[state] && stateOptions[state].enabled === false) { return; } if (state) { - lineWidth = stateOptions[state].lineWidth || lineWidth + 1; + 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