app/assets/javascripts/highcharts.js in highcharts-rails-3.0.2 vs app/assets/javascripts/highcharts.js in highcharts-rails-3.0.3

- old
+ new

@@ -1,10 +1,10 @@ // ==ClosureCompiler== // @compilation_level SIMPLE_OPTIMIZATIONS /** - * @license Highcharts JS v3.0.2 (2013-06-05) + * @license Highcharts JS v3.0.3 (2013-07-31) * * (c) 2009-2013 Torstein Hønsi * * License: www.highcharts.com/license */ @@ -53,11 +53,11 @@ pathAnim, timeUnits, noop = function () {}, charts = [], PRODUCT = 'Highcharts', - VERSION = '3.0.2', + VERSION = '3.0.3', // some constants for frequently used strings DIV = 'div', ABSOLUTE = 'absolute', RELATIVE = 'relative', @@ -147,19 +147,19 @@ len = arguments.length, ret = {}, doCopy = function (copy, original) { var value, key; + // An object is replacing a primitive + if (typeof copy !== 'object') { + copy = {}; + } + for (key in original) { if (original.hasOwnProperty(key)) { value = original[key]; - // An object is replacing a primitive - if (typeof copy !== 'object') { - copy = {}; - } - // Copy the contents of objects, but not arrays or DOM nodes if (value && typeof value === 'object' && Object.prototype.toString.call(value) !== '[object Array]' && typeof value.nodeType !== 'number') { copy[key] = doCopy(copy[key] || {}, value); @@ -385,18 +385,18 @@ * @param {String} thousandsSep The thousands separator, defaults to the one given in the lang options */ function numberFormat(number, decimals, decPoint, thousandsSep) { var lang = defaultOptions.lang, // http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_number_format/ - n = number, + n = +number || 0, c = decimals === -1 ? - ((n || 0).toString().split('.')[1] || '').length : // preserve decimals + (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 || 0).toFixed(c))), + 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) : ""); } @@ -568,10 +568,17 @@ ret.push(str); return ret.join(''); } /** + * Get the magnitude of a number + */ +function getMagnitude(num) { + return math.pow(10, mathFloor(math.log(num) / math.LN10)); +} + +/** * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5 * @param {Number} interval * @param {Array} multiples * @param {Number} magnitude * @param {Object} options @@ -679,11 +686,15 @@ if (interval === timeUnits[YEAR] && tickInterval < 5 * interval) { multiples = [1, 2, 5]; } // get the count - count = normalizeTickInterval(tickInterval / interval, multiples); + count = normalizeTickInterval( + tickInterval / interval, + multiples, + unit[0] === YEAR ? getMagnitude(tickInterval / interval) : 1 // #1913 + ); return { unitRange: interval, count: count, unitName: unit[0] @@ -1144,11 +1155,11 @@ } }); // Extend the opacity getter, needed for fading opacity with IE9 and jQuery 1.10+ wrap(opacityHook, 'get', function (proceed, elem, computed) { - return elem.attr ? (elem.opacity || 0) : proceed.call(this, elem, computed); + return elem.attr ? (elem.opacity || 0) : proceed.call(this, elem, computed); }); // Define the setter function for d (path definitions) dSetter = function (fx) { @@ -1460,11 +1471,11 @@ var defaultLabelOptions = { enabled: true, // rotation: 0, - align: 'center', + // align: 'center', x: 0, y: 15, /*formatter: function () { return this.value; },*/ @@ -1492,12 +1503,12 @@ resetZoomTitle: 'Reset zoom level 1:1', thousandsSep: ',' }, global: { useUTC: true, - canvasToolsURL: 'http://code.highcharts.com/3.0.2/modules/canvas-tools.js', - VMLRadialGradientURL: 'http://code.highcharts.com/3.0.2/gfx/vml-radial-gradient.png' + canvasToolsURL: 'http://code.highcharts.com/3.0.3/modules/canvas-tools.js', + VMLRadialGradientURL: 'http://code.highcharts.com/3.0.3/gfx/vml-radial-gradient.png' }, chart: { //animation: true, //alignTicks: false, //reflow: true, @@ -1544,14 +1555,14 @@ }, title: { text: 'Chart title', align: 'center', // floating: false, - // margin: 15, + margin: 15, // x: 0, // verticalAlign: 'top', - y: 15, + // y: null, style: { color: '#274b6d',//#3E576F', fontSize: '16px' } @@ -1560,11 +1571,11 @@ text: '', align: 'center', // floating: false // x: 0, // verticalAlign: 'top', - y: 30, + // y: null, style: { color: '#4d759e' } }, @@ -1606,13 +1617,14 @@ }, point: { events: {} }, dataLabels: merge(defaultLabelOptions, { + align: 'center', enabled: false, formatter: function () { - return numberFormat(this.y, -1); + return this.y === null ? '' : numberFormat(this.y, -1); }, verticalAlign: 'bottom', // above singular point y: 0 // backgroundColor: undefined, // borderColor: undefined, @@ -2156,11 +2168,11 @@ .replace(/,$/, '') .split(','); // ending comma i = value.length; while (i--) { - value[i] = pInt(value[i]) * hash['stroke-width']; + value[i] = pInt(value[i]) * pick(hash['stroke-width'], wrapper['stroke-width']); } value = value.join(','); } // IE9/MooTools combo: MooTools returns objects instead of numbers and IE9 Beta 2 @@ -2214,11 +2226,11 @@ } skipAttr = true; } // let the shadow follow the main element - if (shadows && /^(width|height|visibility|x|y|d|transform)$/.test(key)) { + if (shadows && /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(key)) { i = shadows.length; while (i--) { attr( shadows[i], key, @@ -2269,11 +2281,16 @@ /** * Add a class name to an element */ addClass: function (className) { - attr(this.element, 'class', attr(this.element, 'class') + ' ' + className); + var element = this.element, + currentClassName = attr(element, 'class') || ''; + + if (currentClassName.indexOf(className) === -1) { + attr(element, 'class', currentClassName + ' ' + className); + } return this; }, /* hasClass and removeClass are not (yet) needed hasClass: function (className) { return attr(this.element, 'class').indexOf(className) !== -1; @@ -2412,19 +2429,20 @@ * Add an event listener * @param {String} eventType * @param {Function} handler */ on: function (eventType, handler) { + var element = this.element; // touch if (hasTouch && eventType === 'click') { - this.element.ontouchstart = function (e) { + element.ontouchstart = function (e) { e.preventDefault(); - handler(); + handler.call(element, e); }; } // simplest possible event model for internal use - this.element['on' + eventType] = handler; + element['on' + eventType] = handler; return this; }, /** * Set the coordinates needed to draw a consistent radial gradient across @@ -2566,37 +2584,22 @@ sintheta = 0, quad, textWidth = pInt(wrapper.textWidth), xCorr = wrapper.xCorr || 0, yCorr = wrapper.yCorr || 0, - currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth].join(','), - rotationStyle = {}, - cssTransformKey; + currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth].join(','); if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed if (defined(rotation)) { - if (renderer.isSVG) { // #916 - cssTransformKey = isIE ? '-ms-transform' : isWebKit ? '-webkit-transform' : isFirefox ? 'MozTransform' : isOpera ? '-o-transform' : ''; - rotationStyle[cssTransformKey] = rotationStyle.transform = 'rotate(' + rotation + 'deg)'; - - } else { - radians = rotation * deg2rad; // deg to rad - costheta = mathCos(radians); - sintheta = mathSin(radians); + radians = rotation * deg2rad; // deg to rad + costheta = mathCos(radians); + sintheta = mathSin(radians); - // Adjust for alignment and rotation. Rotation of useHTML content is not yet implemented - // but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+ - // has support for CSS3 transform. The getBBox method also needs to be updated - // to compensate for the rotation, like it currently does for SVG. - // Test case: http://highcharts.com/tests/?file=text-rotation - rotationStyle.filter = rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta, - ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta, - ', sizingMethod=\'auto expand\')'].join('') : NONE; - } - css(elem, rotationStyle); + wrapper.setSpanRotation(rotation, sintheta, costheta); + } width = pick(wrapper.elemWidth, elem.offsetWidth); height = pick(wrapper.elemHeight, elem.offsetHeight); @@ -2651,10 +2654,21 @@ wrapper.cTT = currentTextTransform; } }, /** + * Set the rotation of an individual HTML span + */ + setSpanRotation: function (rotation) { + var rotationStyle = {}, + cssTransformKey = isIE ? '-ms-transform' : isWebKit ? '-webkit-transform' : isFirefox ? 'MozTransform' : isOpera ? '-o-transform' : ''; + + rotationStyle[cssTransformKey] = rotationStyle.transform = 'rotate(' + rotation + 'deg)'; + css(this.element, rotationStyle); + }, + + /** * Private method to update the transform attribute based on internal * properties */ updateTransform: function () { var wrapper = this, @@ -2952,10 +2966,12 @@ */ destroy: function () { var wrapper = this, element = wrapper.element || {}, shadows = wrapper.shadows, + parentToClean = wrapper.renderer.isSVG && element.nodeName === 'SPAN' && element.parentNode, + grandParent, key, i; // remove events element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = element.point = null; @@ -2981,10 +2997,17 @@ each(shadows, function (shadow) { wrapper.safeRemoveChild(shadow); }); } + // In case of useHTML, clean up empty containers emulating SVG groups (#1960). + while (parentToClean && parentToClean.childNodes.length === 0) { + grandParent = parentToClean.parentNode; + wrapper.safeRemoveChild(parentToClean); + parentToClean = grandParent; + } + // remove from alignObjects if (wrapper.alignTo) { erase(wrapper.renderer.alignedObjects, wrapper); } @@ -3069,22 +3092,28 @@ */ init: function (container, width, height, forExport) { var renderer = this, loc = location, boxWrapper, + element, desc; boxWrapper = renderer.createElement('svg') .attr({ - xmlns: SVG_NS, version: '1.1' }); - container.appendChild(boxWrapper.element); + element = boxWrapper.element; + container.appendChild(element); + // For browsers other than IE, add the namespace attribute (#1978) + if (container.innerHTML.indexOf('xmlns') === -1) { + attr(element, 'xmlns', SVG_NS); + } + // object properties renderer.isSVG = true; - renderer.box = boxWrapper.element; + renderer.box = element; renderer.boxWrapper = boxWrapper; renderer.alignedObjects = []; // Page url used for internal references. #24, #672, #1070 renderer.url = (isFirefox || isWebKit) && doc.getElementsByTagName('base').length ? @@ -3201,11 +3230,11 @@ .replace(/<a/g, '<span') .replace(/<\/(b|strong|i|em|a)>/g, '</span>') .split(/<br.*?>/g), childNodes = textNode.childNodes, styleRegex = /style="([^"]+)"/, - hrefRegex = /href="([^"]+)"/, + hrefRegex = /href="(http[^"]+)"/, parentX = attr(textNode, 'x'), textStyles = wrapper.styles, width = textStyles && textStyles.width && pInt(textStyles.width), textLineHeight = textStyles && textStyles.lineHeight, i = childNodes.length; @@ -3247,87 +3276,91 @@ span = (span.replace(/<(.|\n)*?>/g, '') || ' ') .replace(/&lt;/g, '<') .replace(/&gt;/g, '>'); - // add the text node - tspan.appendChild(doc.createTextNode(span)); + // Nested tags aren't supported, and cause crash in Safari (#1596) + if (span !== ' ') { + + // add the text node + tspan.appendChild(doc.createTextNode(span)); - if (!spanNo) { // first span in a line, align it to the left - attributes.x = parentX; - } else { - attributes.dx = 0; // #16 - } + if (!spanNo) { // first span in a line, align it to the left + attributes.x = parentX; + } else { + attributes.dx = 0; // #16 + } - // add attributes - attr(tspan, attributes); + // add attributes + attr(tspan, attributes); - // first span on subsequent line, add the line height - if (!spanNo && lineNo) { + // 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) { - css(tspan, { display: 'block' }); + // allow getting the right offset height in exporting in IE + if (!hasSVG && forExport) { + css(tspan, { display: 'block' }); + } + + // Set the line height based on the font size of either + // the text element or the tspan element + attr( + tspan, + 'dy', + textLineHeight || renderer.fontMetrics( + /px$/.test(tspan.style.fontSize) ? + tspan.style.fontSize : + textStyles.fontSize + ).h, + // Safari 6.0.2 - too optimized for its own good (#1539) + // TODO: revisit this with future versions of Safari + isWebKit && tspan.offsetHeight + ); } - // Set the line height based on the font size of either - // the text element or the tspan element - attr( - tspan, - 'dy', - textLineHeight || renderer.fontMetrics( - /px$/.test(tspan.style.fontSize) ? - tspan.style.fontSize : - textStyles.fontSize - ).h, - // Safari 6.0.2 - too optimized for its own good (#1539) - // TODO: revisit this with future versions of Safari - isWebKit && tspan.offsetHeight - ); - } + // Append it + textNode.appendChild(tspan); - // Append it - textNode.appendChild(tspan); + spanNo++; - spanNo++; + // check width and apply soft breaks + if (width) { + var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273 + tooLong, + actualWidth, + rest = []; - // check width and apply soft breaks - if (width) { - var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273 - tooLong, - actualWidth, - rest = []; + while (words.length || rest.length) { + delete wrapper.bBox; // delete cache + actualWidth = wrapper.getBBox().width; + tooLong = actualWidth > width; + if (!tooLong || words.length === 1) { // new line needed + words = rest; + rest = []; + if (words.length) { + tspan = doc.createElementNS(SVG_NS, 'tspan'); + attr(tspan, { + dy: textLineHeight || 16, + x: parentX + }); + if (spanStyle) { // #390 + attr(tspan, 'style', spanStyle); + } + textNode.appendChild(tspan); - while (words.length || rest.length) { - delete wrapper.bBox; // delete cache - actualWidth = wrapper.getBBox().width; - tooLong = actualWidth > width; - if (!tooLong || words.length === 1) { // new line needed - words = rest; - rest = []; - if (words.length) { - tspan = doc.createElementNS(SVG_NS, 'tspan'); - attr(tspan, { - dy: textLineHeight || 16, - x: parentX - }); - if (spanStyle) { // #390 - attr(tspan, 'style', spanStyle); + if (actualWidth > width) { // a single word is pressing it out + width = actualWidth; + } } - textNode.appendChild(tspan); - - if (actualWidth > width) { // a single word is pressing it out - width = actualWidth; - } + } else { // append to existing line tspan + tspan.removeChild(tspan.firstChild); + rest.unshift(words.pop()); } - } else { // append to existing line tspan - tspan.removeChild(tspan.firstChild); - rest.unshift(words.pop()); + if (words.length) { + tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-'))); + } } - if (words.length) { - tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-'))); - } } } } }); }); @@ -3400,16 +3433,16 @@ } }, pressedState); pressedStyle = pressedState[STYLE]; delete pressedState[STYLE]; - // add the events - addEvent(label.element, 'mouseenter', function () { + // Add the events. IE9 and IE10 need mouseover and mouseout to funciton (#667). + addEvent(label.element, isIE ? 'mouseover' : 'mouseenter', function () { label.attr(hoverState) .css(hoverStyle); }); - addEvent(label.element, 'mouseleave', function () { + addEvent(label.element, isIE ? 'mouseout' : 'mouseleave', function () { stateOptions = [normalState, hoverState, pressedState][curState]; stateStyle = [normalStyle, hoverStyle, pressedStyle][curState]; label.attr(stateOptions) .css(stateStyle); }); @@ -3494,26 +3527,30 @@ * @param {Number} innerR Inner radius like used in donut charts * @param {Number} start Starting angle * @param {Number} end Ending angle */ arc: function (x, y, r, innerR, start, end) { - // arcs are defined as symbols for the ability to set - // attributes in attr and animate + var arc; if (isObject(x)) { y = x.y; r = x.r; innerR = x.innerR; start = x.start; end = x.end; x = x.x; } - return this.symbol('arc', x || 0, y || 0, r || 0, r || 0, { + + // Arcs are defined as symbols for the ability to set + // attributes in attr and animate + arc = this.symbol('arc', x || 0, y || 0, r || 0, r || 0, { innerR: innerR || 0, start: start || 0, end: end || 0 }); + arc.r = r; // #959 + return arc; }, /** * Draw and return a rectangle * @param {Number} x Left position @@ -4536,10 +4573,26 @@ * VML always uses htmlUpdateTransform */ updateTransform: SVGElement.prototype.htmlUpdateTransform, /** + * Set the rotation of a span with oldIE's filter + */ + setSpanRotation: function (rotation, sintheta, costheta) { + // Adjust for alignment and rotation. Rotation of useHTML content is not yet implemented + // but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+ + // has support for CSS3 transform. The getBBox method also needs to be updated + // to compensate for the rotation, like it currently does for SVG. + // Test case: http://highcharts.com/tests/?file=text-rotation + css(this.element, { + filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta, + ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta, + ', sizingMethod=\'auto expand\')'].join('') : NONE + }); + }, + + /** * Get or set attributes */ attr: function (hash, val) { var wrapper = this, key, @@ -4757,11 +4810,13 @@ });*/ skipAttr = true; // rotation on VML elements } else if (nodeName === 'shape' && key === 'rotation') { - wrapper[key] = value; + + wrapper[key] = element.style[key] = value; // style is for #1873 + // Correction for the 1x1 size of the shape container. Used in gauge needles. element.style.left = -mathRound(mathSin(value * deg2rad) + 1) + PX; element.style.top = mathRound(mathCos(value * deg2rad)) + PX; // translation for animation @@ -5667,11 +5722,11 @@ tickPositions = axis.tickPositions, width = (horiz && categories && !labelOptions.step && !labelOptions.staggerLines && !labelOptions.rotation && chart.plotWidth / tickPositions.length) || - (!horiz && (chart.optionsMarginLeft || chart.plotWidth / 2)), // #1580 + (!horiz && (chart.optionsMarginLeft || chart.chartWidth * 0.33)), // #1580, #1931 isFirst = pos === tickPositions[0], isLast = pos === tickPositions[tickPositions.length - 1], css, attr, value = categories ? @@ -5706,11 +5761,11 @@ css = extend(css, labelOptions.style); // first call if (!defined(label)) { attr = { - align: labelOptions.align + align: axis.labelAlign }; if (isNumber(labelOptions.rotation)) { attr.rotation = labelOptions.rotation; } tick.label = @@ -5755,11 +5810,11 @@ var bBox = this.labelBBox, // assume getLabelSize has run at this point axis = this.axis, options = axis.options, labelOptions = options.labels, width = bBox.width, - leftSide = width * { left: 0, center: 0.5, right: 1 }[labelOptions.align] - labelOptions.x; + leftSide = width * { left: 0, center: 0.5, right: 1 }[axis.labelAlign] - labelOptions.x; return [-leftSide, width - leftSide]; }, /** @@ -5845,25 +5900,32 @@ */ 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, + baseline = axis.chart.renderer.fontMetrics(labelOptions.style.fontSize).b, + rotation = labelOptions.rotation; x = x + labelOptions.x - (tickmarkOffset && horiz ? tickmarkOffset * transA * (reversed ? -1 : 1) : 0); y = y + labelOptions.y - (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)) { - y += pInt(label.styles.lineHeight) * 0.9 - label.getBBox().height / 2; + if (!defined(labelOptions.y) && !rotation) { // #1951 + y += baseline - label.getBBox().height / 2; } // Correct for staggered labels if (staggerLines) { - y += (index / (step || 1) % staggerLines) * 16; + y += (index / (step || 1) % staggerLines) * (axis.labelOffset / staggerLines); } return { x: x, y: y @@ -6158,11 +6220,12 @@ // add the SVG element if (!label) { plotLine.label = label = renderer.text( optionsLabel.text, 0, - 0 + 0, + optionsLabel.useHTML // docs: useHTML for plotLines and plotBands ) .attr({ align: optionsLabel.textAlign || optionsLabel.align, rotation: optionsLabel.rotation, zIndex: zIndex @@ -6222,10 +6285,16 @@ this.options = options; // Save the x value to be able to position the label later this.x = x; + // Initialize total value + this.total = 0; + + // This will keep each points' extremes stored by series.index + this.points = {}; + // Save the stack option on the series configuration object, and whether to treat it as percent this.stack = stackOption; this.percent = stacking === 'percent'; // The align options and text align varies on whether the stack is negative and @@ -6254,15 +6323,22 @@ this.total = total; this.cum = total; }, /** + * Adds value to stack total, this method takes care of correcting floats + */ + addValue: function (y) { + this.setTotal(correctFloat(this.total + y)); + }, + + /** * Renders the stack total label and adds it to the stack label group. */ render: function (group) { var options = this.options, - formatOption = options.format, // docs: added stackLabel.format option + formatOption = options.format, str = formatOption ? format(formatOption, this) : options.formatter.call(this); // format the text in the label // Change the text to reflect the new total and set visibility to hidden in case the serie is hidden @@ -6280,10 +6356,14 @@ }) .add(group); // add to the labels-group } }, + cacheExtremes: function (series, extremes) { + this.points[series.index] = extremes; + }, + /** * Sets the offset that the stack has from the x value and repositions the label. */ setOffset: function (xOffset, xWidth) { var stackItem = this, @@ -6419,11 +6499,10 @@ endOnTick: true, gridLineWidth: 1, tickPixelInterval: 72, showLastLabel: true, labels: { - align: 'right', x: -8, y: 3 }, lineWidth: 0, maxPadding: 0.05, @@ -6452,11 +6531,10 @@ /** * These options extend the defaultOptions for left axes */ defaultLeftAxisOptions: { labels: { - align: 'right', x: -8, y: null }, title: { rotation: 270 @@ -6466,11 +6544,10 @@ /** * These options extend the defaultOptions for right axes */ defaultRightAxisOptions: { labels: { - align: 'left', x: 8, y: null }, title: { rotation: 90 @@ -6480,11 +6557,10 @@ /** * These options extend the defaultOptions for bottom axes */ defaultBottomAxisOptions: { labels: { - align: 'center', x: 0, y: 14 // overflow: undefined, // staggerLines: null }, @@ -6495,11 +6571,10 @@ /** * These options extend the defaultOptions for left axes */ defaultTopAxisOptions: { labels: { - align: 'center', x: 0, y: -5 // overflow: undefined // staggerLines: null }, @@ -6539,11 +6614,10 @@ axis.labelFormatter = options.labels.formatter || axis.defaultLabelFormatter; // can be overwritten by dynamic format // Flag, stagger lines or not - axis.staggerLines = axis.horiz && options.labels.staggerLines; axis.userOptions = userOptions; //axis.axisTitleMargin = UNDEFINED,// = options.title.margin, axis.minPixelPadding = 0; //axis.ignoreMinPadding = UNDEFINED; // can be set to true by a column or bar series @@ -6617,12 +6691,17 @@ axis.offset = options.offset || 0; // Dictionary for stacks axis.stacks = {}; + axis.oldStacks = {}; + + // Dictionary for stacks max values + axis.stacksMax = {}; + axis._stacksTouched = 0; - + // Min and max in the data //axis.dataMin = UNDEFINED, //axis.dataMax = UNDEFINED, // The axis range @@ -6689,14 +6768,14 @@ update: function (newOptions, redraw) { var chart = this.chart; newOptions = chart.options[this.xOrY + 'Axis'][this.options.index] = merge(this.userOptions, newOptions); - this.destroy(); + this.destroy(true); this._addedPlotLB = false; // #1611 - this.init(chart, newOptions); + this.init(chart, extend(newOptions, { events: UNDEFINED })); chart.isDirtyBox = true; if (pick(redraw, true)) { chart.redraw(); } @@ -6776,57 +6855,45 @@ } } return ret; }, - + /** * Get the minimum and maximum for the series of each axis */ getSeriesExtremes: function () { var axis = this, - chart = axis.chart, - stacks = axis.stacks, - posStack = [], - negStack = [], - stacksTouched = axis._stacksTouched = axis._stacksTouched + 1, - type, - i; - + chart = axis.chart; + axis.hasVisibleSeries = false; // reset dataMin and dataMax in case we're redrawing axis.dataMin = axis.dataMax = null; + // reset cached stacking extremes + axis.stacksMax = {}; + + axis.buildStacks(); + // loop through this axis' series each(axis.series, function (series) { if (series.visible || !chart.options.chart.ignoreHiddenSeries) { var seriesOptions = series.options, stacking, - posPointStack, - negPointStack, - stackKey, - stackOption, - negKey, xData, - yData, - x, - y, threshold = seriesOptions.threshold, - yDataLength, - activeYData = [], seriesDataMin, - seriesDataMax, - activeCounter = 0; - - axis.hasVisibleSeries = true; - + seriesDataMax; + + axis.hasVisibleSeries = true; + // Validate threshold in logarithmic axes if (axis.isLog && threshold <= 0) { - threshold = seriesOptions.threshold = null; + threshold = null; } // Get dataMin and dataMax for X axes if (axis.isXAxis) { xData = series.xData; @@ -6835,116 +6902,31 @@ axis.dataMax = mathMax(pick(axis.dataMax, xData[0]), arrayMax(xData)); } // Get dataMin and dataMax for Y axes, as well as handle stacking and processed data } else { - var isNegative, - pointStack, - key, - cropped = series.cropped, - xExtremes = series.xAxis.getExtremes(), - //findPointRange, - //pointRange, - j, - hasModifyValue = !!series.modifyValue; // Handle stacking stacking = seriesOptions.stacking; axis.usePercentage = stacking === 'percent'; // create a stack for this particular series type - if (stacking) { - stackOption = seriesOptions.stack; - stackKey = series.type + pick(stackOption, ''); - negKey = '-' + stackKey; - series.stackKey = stackKey; // used in translate - - posPointStack = posStack[stackKey] || []; // contains the total values for each x - posStack[stackKey] = posPointStack; - - negPointStack = negStack[negKey] || []; - negStack[negKey] = negPointStack; - } if (axis.usePercentage) { axis.dataMin = 0; axis.dataMax = 99; } - // processData can alter series.pointRange, so this goes after - //findPointRange = series.pointRange === null; + + // get this particular series extremes + series.getExtremes(); + seriesDataMax = series.dataMax; + seriesDataMin = series.dataMin; - xData = series.processedXData; - yData = series.processedYData; - yDataLength = yData.length; - - // loop over the non-null y values and read them into a local array - for (i = 0; i < yDataLength; i++) { - x = xData[i]; - y = yData[i]; - - // Read stacked values into a stack based on the x value, - // the sign of y and the stack key. Stacking is also handled for null values (#739) - if (stacking) { - isNegative = y < threshold; - pointStack = isNegative ? negPointStack : posPointStack; - key = isNegative ? negKey : stackKey; - - // Set the stack value and y for extremes - if (defined(pointStack[x])) { // we're adding to the stack - pointStack[x] = correctFloat(pointStack[x] + y); - y = [y, pointStack[x]]; // consider both the actual value and the stack (#1376) - - } else { // it's the first point in the stack - pointStack[x] = y; - } - - // add the series - if (!stacks[key]) { - stacks[key] = {}; - } - - // If the StackItem is there, just update the values, - // if not, create one first - if (!stacks[key][x]) { - stacks[key][x] = new StackItem(axis, axis.options.stackLabels, isNegative, x, stackOption, stacking); - } - stacks[key][x].setTotal(pointStack[x]); - stacks[key][x].touched = stacksTouched; - } - - // Handle non null values - if (y !== null && y !== UNDEFINED && (!axis.isLog || (y.length || y > 0))) { - - // general hook, used for Highstock compare values feature - if (hasModifyValue) { - y = series.modifyValue(y); - } - - // For points within the visible range, including the first point outside the - // visible range, consider y extremes - if (series.getExtremesFromAll || cropped || ((xData[i + 1] || x) >= xExtremes.min && - (xData[i - 1] || x) <= xExtremes.max)) { - - j = y.length; - if (j) { // array, like ohlc or range data - while (j--) { - if (y[j] !== null) { - activeYData[activeCounter++] = y[j]; - } - } - } else { - activeYData[activeCounter++] = y; - } - } - } - } - // Get the dataMin and dataMax so far. If percentage is used, the min and max are - // always 0 and 100. If the length of activeYData is 0, continue with null values. - if (!axis.usePercentage && activeYData.length) { - series.dataMin = seriesDataMin = arrayMin(activeYData); - series.dataMax = seriesDataMax = arrayMax(activeYData); + // always 0 and 100. If seriesDataMin and seriesDataMax is null, then series + // doesn't have active y data, we continue with nulls + if (!axis.usePercentage && defined(seriesDataMin) && defined(seriesDataMax)) { axis.dataMin = mathMin(pick(axis.dataMin, seriesDataMin), seriesDataMin); axis.dataMax = mathMax(pick(axis.dataMax, seriesDataMax), seriesDataMax); } // Adjust to threshold @@ -6958,28 +6940,17 @@ } } } } }); - - // Destroy unused stacks (#1044) - for (type in stacks) { - for (i in stacks[type]) { - if (stacks[type][i].touched < stacksTouched) { - stacks[type][i].destroy(); - delete stacks[type][i]; - } - } - } - }, /** * Translate from axis value to pixel position on the chart, or back * */ - translate: function (val, backwards, cvsCoord, old, handleLog, pointPlacementBetween) { + translate: function (val, backwards, cvsCoord, old, handleLog, pointPlacement) { var axis = this, axisLength = axis.len, sign = 1, cvsOffset = 0, localA = old ? axis.oldTransA : axis.transA, @@ -7018,13 +6989,15 @@ // From value to pixels } else { if (postTranslate) { // log and ordinal axes val = axis.val2lin(val); } - + if (pointPlacement === 'between') { + pointPlacement = 0.5; + } returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * minPixelPadding) + - (pointPlacementBetween ? localA * axis.pointRange / 2 : 0); + (isNumber(pointPlacement) ? localA * pointPlacement * axis.pointRange : 0); } return returnValue; }, @@ -7224,11 +7197,11 @@ ); interval = normalizeTickInterval( interval, null, - math.pow(10, mathFloor(math.log(interval) / math.LN10)) + getMagnitude(interval) ); positions = map(axis.getLinearTickPositions( interval, realMin, @@ -7388,22 +7361,22 @@ } else { each(axis.series, function (series) { var seriesPointRange = series.pointRange, pointPlacement = series.options.pointPlacement, seriesClosestPointRange = series.closestPointRange; - + 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, - pointPlacement ? 0 : seriesPointRange / 2 + 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( @@ -7454,11 +7427,10 @@ isLog = axis.isLog, isDatetimeAxis = axis.isDatetimeAxis, isXAxis = axis.isXAxis, isLinked = axis.isLinked, tickPositioner = axis.options.tickPositioner, - magnitude, maxPadding = options.maxPadding, minPadding = options.minPadding, length, linkedParentExtremes, tickIntervalOption = options.tickInterval, @@ -7553,21 +7525,25 @@ // hook for extensions, used in Highstock ordinal axes if (axis.postProcessTickInterval) { axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval); } + + // In column-like charts, don't cramp in more ticks than there are points (#1943) + if (axis.pointRange) { + axis.tickInterval = mathMax(axis.pointRange, axis.tickInterval); + } // 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; } // for linear axes, get magnitude and normalize the interval if (!isDatetimeAxis && !isLog) { // linear - magnitude = math.pow(10, mathFloor(math.log(axis.tickInterval) / math.LN10)); if (!tickIntervalOption) { - axis.tickInterval = normalizeTickInterval(axis.tickInterval, null, magnitude, options); + axis.tickInterval = normalizeTickInterval(axis.tickInterval, null, getMagnitude(axis.tickInterval), options); } } // get minorTickInterval axis.minorTickInterval = options.minorTickInterval === 'auto' && axis.tickInterval ? @@ -7704,14 +7680,24 @@ if (series.isDirtyData || series.isDirty || series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well isDirtyData = true; } }); - + + // do we really need to go through all this? if (isDirtyAxisLength || isDirtyData || axis.isLinked || axis.forceRedraw || axis.userMin !== axis.oldUserMin || axis.userMax !== axis.oldUserMax) { + + // reset stacks + if (!axis.isXAxis) { + for (type in stacks) { + for (i in stacks[type]) { + stacks[type][i].total = null; + } + } + } axis.forceRedraw = false; // get data extremes if needed axis.getSeriesExtremes(); @@ -7725,15 +7711,16 @@ // Mark as dirty if it is not already set to dirty and extremes have changed. #595. if (!axis.isDirty) { axis.isDirty = isDirtyAxisLength || axis.min !== axis.oldMin || axis.max !== axis.oldMax; } - } - - - // reset stacks - if (!axis.isXAxis) { + } else if (!axis.isXAxis) { + if (axis.oldStacks) { + stacks = axis.stacks = axis.oldStacks; + } + + // reset stacks for (type in stacks) { for (i in stacks[type]) { stacks[type][i].cum = stacks[type][i].total; } } @@ -7785,16 +7772,16 @@ * Overridable method for zooming chart. Pulled out in a separate method to allow overriding * in stock charts. */ zoom: function (newMin, newMax) { - // Prevent pinch zooming out of range + // Prevent pinch zooming out of range. Check for defined is for #1946. if (!this.allowZoomOutside) { - if (newMin <= this.dataMin) { + if (defined(this.dataMin) && newMin <= this.dataMin) { newMin = UNDEFINED; } - if (newMax >= this.dataMax) { + if (defined(this.dataMax) && newMax >= this.dataMax) { newMax = UNDEFINED; } } // In full view, displaying the reset zoom button is not required @@ -7902,10 +7889,28 @@ return obj; }, /** + * Compute auto alignment for the axis label based on which side the axis is on + * and the given rotation for the label + */ + autoLabelAlign: function (rotation) { + var ret, + angle = (pick(rotation, 0) - (this.side * 90) + 720) % 360; + + if (angle > 15 && angle < 165) { + ret = 'right'; + } else if (angle > 195 && angle < 345) { + ret = 'left'; + } else { + ret = 'center'; + } + return ret; + }, + + /** * Render the tick labels to a preliminary position to get their sizes */ getOffset: function () { var axis = this, chart = axis.chart, @@ -7925,15 +7930,28 @@ labelOptions = options.labels, labelOffset = 0, // reset axisOffset = chart.axisOffset, clipOffset = chart.clipOffset, directionFactor = [-1, 1, 1, -1][side], - n; + n, + i, + autoStaggerLines = 1, + maxStaggerLines = pick(labelOptions.maxStaggerLines, 5), // docs + lastRight, + overlap, + pos, + bBox, + x, + w, + lineNo; // 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); + + // Set/reset staggerLines + axis.staggerLines = axis.horiz && labelOptions.staggerLines; // Create the axisGroup and gridGroup elements on first iteration if (!axis.axisGroup) { axis.gridGroup = renderer.g('grid') .attr({ zIndex: options.gridZIndex || 1 }) @@ -7945,35 +7963,72 @@ .attr({ zIndex: labelOptions.zIndex || 7 }) .add(); } if (hasData || axis.isLinked) { + + // Set the explicit or automatic label alignment + axis.labelAlign = pick(labelOptions.align || axis.autoLabelAlign(labelOptions.rotation)); + 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) { + while (autoStaggerLines < maxStaggerLines) { + lastRight = []; + overlap = false; + + for (i = 0; i < tickPositions.length; i++) { + pos = tickPositions[i]; + bBox = ticks[pos].label && ticks[pos].label.bBox; + 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] === labelOptions.align) { + if (side === 0 || side === 2 || { 1: 'left', 3: 'right' }[side] === axis.labelAlign) { // get the highest offset labelOffset = mathMax( ticks[pos].getLabelSize(), labelOffset ); } }); - if (axis.staggerLines) { - labelOffset += (axis.staggerLines - 1) * 16; + labelOffset *= axis.staggerLines; + axis.labelOffset = labelOffset; } + } else { // doesn't have data for (n in ticks) { ticks[n].destroy(); delete ticks[n]; @@ -8324,16 +8379,27 @@ * Remove a plot band or plot line from the chart by id * @param {Object} id */ removePlotBandOrLine: function (id) { var plotLinesAndBands = this.plotLinesAndBands, + options = this.options, + userOptions = this.userOptions, i = plotLinesAndBands.length; while (i--) { if (plotLinesAndBands[i].id === id) { plotLinesAndBands[i].destroy(); } } + each([options.plotLines || [], userOptions.plotLines || [], options.plotBands || [], userOptions.plotBands || []], function (arr) { + i = arr.length; + while (i--) { + if (arr[i].id === id) { + erase(arr, arr[i]); + } + } + }); + }, /** * Update the axis title by options */ @@ -8368,10 +8434,26 @@ }); }, /** + * + */ + buildStacks: function () { + if (this.isXAxis) { + return; + } + + var series = this.series, + last = series.length - 1; + + each(series, function (serie, i) { + serie.setStackedPoints(i === last); + }); + }, + + /** * Set new axis categories and optionally redraw * @param {Array} categories * @param {Boolean} redraw */ setCategories: function (categories, redraw) { @@ -8379,17 +8461,19 @@ }, /** * Destroys an Axis instance. */ - destroy: function () { + destroy: function (keepEvents) { var axis = this, stacks = axis.stacks, stackKey; // Remove the events - removeEvent(axis); + if (!keepEvents) { + removeEvent(axis); + } // Destroy each stack total for (stackKey in stacks) { destroyObjectProperties(stacks[stackKey]); @@ -8796,19 +8880,24 @@ var path, i = crosshairsOptions.length, attribs, axis, - val; + val, + series; while (i--) { - axis = point.series[i ? 'yAxis' : 'xAxis']; + series = point.series; + axis = series[i ? 'yAxis' : 'xAxis']; if (crosshairsOptions[i] && axis) { val = i ? pick(point.stackY, point.y) : point.x; // #814 if (axis.isLog) { // #1671 val = log2lin(val); } + if (series.modifyValue) { // #1205 + val = series.modifyValue(val); + } path = axis.getPlotLinePath( val, 1 ); @@ -8906,12 +8995,10 @@ * Add crossbrowser support for chartX and chartY * @param {Object} e The event object in standard browsers */ normalize: function (e) { var chartPosition, - chartX, - chartY, ePos; // common IE normalizing e = e || win.event; if (!e.target) { @@ -8925,22 +9012,14 @@ ePos = e.touches ? e.touches.item(0) : e; // get mouse position this.chartPosition = chartPosition = offset(this.chart.container); - // chartX and chartY - if (ePos.pageX === UNDEFINED) { // IE < 9. #886. - chartX = e.x; - chartY = e.y; - } else { - chartX = ePos.pageX - chartPosition.left; - chartY = ePos.pageY - chartPosition.top; - } - + // Old IE and compatibility mode use clientX. #886, #2005. return extend(e, { - chartX: mathRound(chartX), - chartY: mathRound(chartY) + chartX: mathRound(pick(ePos.pageX, ePos.clientX) - chartPosition.left), + chartY: mathRound(pick(ePos.pageY, ePos.clientY) - chartPosition.top) }); }, /** * Get the click position in terms of axis values. @@ -9095,22 +9174,24 @@ /** * Scale series groups to a certain scale and translation */ scaleGroups: function (attribs, clip) { - var chart = this.chart; + var chart = this.chart, + seriesAttribs; // Scale each series each(chart.series, function (series) { + seriesAttribs = attribs || series.getPlotBox(); // #1701 if (series.xAxis && series.xAxis.zoomEnabled) { - series.group.attr(attribs); + series.group.attr(seriesAttribs); if (series.markerGroup) { - series.markerGroup.attr(attribs); + series.markerGroup.attr(seriesAttribs); series.markerGroup.clip(clip ? chart.clipRect : null); } if (series.dataLabelsGroup) { - series.dataLabelsGroup.attr(attribs); + series.dataLabelsGroup.attr(seriesAttribs); } } }); // Clip @@ -9426,16 +9507,11 @@ } this.selectionMarker = this.selectionMarker.destroy(); // Reset scaling preview if (hasPinched) { - this.scaleGroups({ - translateX: chart.plotLeft, - translateY: chart.plotTop, - scaleX: 1, - scaleY: 1 - }); + this.scaleGroups(); } } // Reset all if (chart) { // it may be destroyed on mouse up - #877 @@ -9610,10 +9686,14 @@ // Run mouse events and display tooltip etc this.runPointActions(e); this.pinch(e); + + } else { + // Hide the tooltip on touching outside the plot area (#1203) + this.reset(); } } else if (e.touches.length === 2) { this.pinch(e); } @@ -9761,11 +9841,10 @@ stroke: symbolColor, fill: symbolColor }, key, val; - if (legendItem) { legendItem.css({ fill: textColor, color: textColor }); // color for #1553, oldIE } if (legendLine) { @@ -9773,11 +9852,11 @@ } if (legendSymbol) { // Apply marker options - if (markerOptions) { + if (markerOptions && legendSymbol.isMarker) { // #585 markerOptions = item.convertAttribs(markerOptions); for (key in markerOptions) { val = markerOptions[key]; if (val !== UNDEFINED) { symbolAttr[key] = val; @@ -9824,11 +9903,11 @@ var checkbox = item.checkbox; // destroy SVG elements each(['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], function (key) { if (item[key]) { - item[key].destroy(); + item[key] = item[key].destroy(); } }); if (checkbox) { discardElement(item.checkbox); @@ -9883,20 +9962,23 @@ */ renderTitle: function () { var options = this.options, padding = this.padding, titleOptions = options.title, - titleHeight = 0; + titleHeight = 0, + bBox; if (titleOptions.text) { if (!this.title) { this.title = this.chart.renderer.label(titleOptions.text, padding - 3, padding - 4, null, null, null, null, null, 'legend-title') .attr({ zIndex: 1 }) .css(titleOptions.style) .add(this.group); } - titleHeight = this.title.getBBox().height; + bBox = this.title.getBBox(); + titleHeight = bBox.height; + this.offsetWidth = bBox.width; // #1717 this.contentGroup.attr({ translateY: titleHeight }); } this.titleHeight = titleHeight; }, @@ -9913,10 +9995,11 @@ symbolWidth = options.symbolWidth, symbolPadding = options.symbolPadding, itemStyle = legend.itemStyle, itemHiddenStyle = legend.itemHiddenStyle, padding = legend.padding, + itemDistance = horizontal ? pick(options.itemDistance, 8) : 0, // docs ltr = !options.rtl, itemHeight, widthOption = options.width, itemMarginBottom = options.itemMarginBottom || 0, itemMarginTop = legend.itemMarginTop, @@ -10008,11 +10091,11 @@ // calculate the positions for the next line bBox = li.getBBox(); itemWidth = item.legendItemWidth = - options.itemWidth || symbolWidth + symbolPadding + bBox.width + padding + + options.itemWidth || symbolWidth + symbolPadding + bBox.width + itemDistance + (showCheckbox ? 20 : 0); legend.itemHeight = itemHeight = bBox.height; // if the item exceeds the width, start a new line if (horizontal && legend.itemX - initialItemX + itemWidth > @@ -10046,11 +10129,11 @@ legend.lastLineHeight = itemHeight; } // the width of the widest item legend.offsetWidth = widthOption || mathMax( - horizontal ? legend.itemX - initialItemX : itemWidth, + (horizontal ? legend.itemX - initialItemX - itemDistance : itemWidth) + padding, legend.offsetWidth ); }, /** @@ -10383,11 +10466,11 @@ this.optionsMarginBottom = pick(optionsChart.marginBottom, margin[2]); this.optionsMarginLeft = pick(optionsChart.marginLeft, margin[3]); var chartEvents = optionsChart.events; - this.runChartClick = chartEvents && !!chartEvents.click; + //this.runChartClick = chartEvents && !!chartEvents.click; this.bounds = { h: {}, v: {} }; // Pixel data bounds for touch zoom this.callback = callback; this.isResizing = 0; this.options = options; @@ -10518,11 +10601,12 @@ chartOptions = this.options, axis; /*jslint unused: false*/ axis = new Axis(this, merge(options, { - index: this[key].length + index: this[key].length, + isX: isX })); /*jslint unused: true*/ // Push the new axis options to the chart options chartOptions[key] = splat(chartOptions[key] || {}); @@ -10574,10 +10658,11 @@ series = chart.series, pointer = chart.pointer, legend = chart.legend, redrawLegend = chart.isDirtyLegend, hasStackedSeries, + hasDirtyStacks, isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed? seriesLength = series.length, i = seriesLength, serie, renderer = chart.renderer, @@ -10588,19 +10673,27 @@ if (isHiddenChart) { chart.cloneRenderTo(); } + // Adjust title layout (reflow multiline text) + chart.layOutTitles(); + // link stacked series while (i--) { serie = series[i]; - if (serie.isDirty && serie.options.stacking) { + + if (serie.options.stacking) { hasStackedSeries = true; - break; + + if (serie.isDirty) { + hasDirtyStacks = true; + break; + } } } - if (hasStackedSeries) { // mark others as dirty + if (hasDirtyStacks) { // mark others as dirty i = seriesLength; while (i--) { serie = series[i]; if (serie.options.stacking) { serie.isDirty = true; @@ -10623,21 +10716,31 @@ legend.render(); chart.isDirtyLegend = false; } + // reset stacks + if (hasStackedSeries) { + chart.getStacks(); + } + if (chart.hasCartesianSeries) { if (!chart.isResizing) { // reset maxTicks chart.maxTicks = null; // set axes scales each(axes, function (axis) { axis.setScale(); }); + } else { + // build stacks + each(axes, function (axis) { + axis.buildStacks(); + }); } chart.adjustTickAmounts(); chart.getMargins(); // redraw axes @@ -10663,11 +10766,10 @@ if (isDirtyBox) { chart.drawChartBox(); } - // redraw affected series each(series, function (serie) { if (serie.isDirty && serie.visible && (!serie.isCartesian || serie.xAxis)) { // issue #153 serie.redraw(); @@ -10860,10 +10962,30 @@ return serie.selected; }); }, /** + * Generate stacks for each series and calculate stacks total values + */ + getStacks: function () { + var chart = this; + + // reset stacks for each yAxis + each(chart.yAxis, function (axis) { + if (axis.stacks && axis.hasVisibleSeries) { + axis.oldStacks = axis.stacks; + } + }); + + each(chart.series, function (series) { + if (series.options.stacking && (series.visible === true || chart.options.chart.ignoreHiddenSeries === false)) { + series.stackKey = series.type + pick(series.options.stack, ''); + } + }); + }, + + /** * Display the zoom button */ showResetZoom: function () { var chart = this, lang = defaultOptions.lang, @@ -11011,15 +11133,53 @@ align: chartTitleOptions.align, 'class': PREFIX + name, zIndex: chartTitleOptions.zIndex || 4 }) .css(chartTitleOptions.style) - .add() - .align(chartTitleOptions, false, 'spacingBox'); - } + .add(); + } }); + chart.layOutTitles(); + }, + /** + * Lay out the chart titles and cache the full offset height for use in getMargins + */ + layOutTitles: function () { + var titleOffset = 0, + title = this.title, + subtitle = this.subtitle, + options = this.options, + titleOptions = options.title, + subtitleOptions = options.subtitle, + 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'); + + if (!titleOptions.floating && !titleOptions.verticalAlign) { + titleOffset = title.getBBox().height; + + // Adjust for browser consistency + backwards compat after #776 fix + if (titleOffset >= 18 && titleOffset <= 25) { + titleOffset = 15; + } + } + } + if (subtitle) { + subtitle + .css({ width: (subtitleOptions.width || autoWidth) + PX }) + .align(extend({ y: titleOffset + titleOptions.margin }, subtitleOptions), false, 'spacingBox'); + + if (!subtitleOptions.floating && !subtitleOptions.verticalAlign) { + titleOffset = mathCeil(titleOffset + subtitle.getBBox().height); + } + } + + this.titleOffset = titleOffset; // used in getMargins }, /** * Get chart width and height according to options and container size */ @@ -11054,11 +11214,11 @@ delete this.renderToClone; } // Set up the clone } else { - if (container) { + if (container && container.parentNode === this.renderTo) { this.renderTo.removeChild(container); // do not clone this } this.renderToClone = clone = this.renderTo.cloneNode(0); css(clone, { position: ABSOLUTE, @@ -11174,34 +11334,27 @@ legend = chart.legend, optionsMarginTop = chart.optionsMarginTop, optionsMarginLeft = chart.optionsMarginLeft, optionsMarginRight = chart.optionsMarginRight, optionsMarginBottom = chart.optionsMarginBottom, - chartTitleOptions = chart.options.title, - chartSubtitleOptions = chart.options.subtitle, legendOptions = chart.options.legend, legendMargin = pick(legendOptions.margin, 10), legendX = legendOptions.x, legendY = legendOptions.y, align = legendOptions.align, verticalAlign = legendOptions.verticalAlign, - titleOffset; + titleOffset = chart.titleOffset; chart.resetMargins(); axisOffset = chart.axisOffset; - // adjust for title and subtitle - if ((chart.title || chart.subtitle) && !defined(chart.optionsMarginTop)) { - titleOffset = mathMax( - (chart.title && !chartTitleOptions.floating && !chartTitleOptions.verticalAlign && chartTitleOptions.y) || 0, - (chart.subtitle && !chartSubtitleOptions.floating && !chartSubtitleOptions.verticalAlign && chartSubtitleOptions.y) || 0 - ); - if (titleOffset) { - chart.plotTop = mathMax(chart.plotTop, titleOffset + pick(chartTitleOptions.margin, 15) + spacingTop); - } + // Adjust for title and subtitle + if (titleOffset && !defined(optionsMarginTop)) { + chart.plotTop = mathMax(chart.plotTop, titleOffset + chart.options.title.margin + spacingTop); } - // adjust for legend + + // Adjust for legend if (legend.display && !legendOptions.floating) { if (align === 'right') { // horizontal alignment handled first if (!defined(optionsMarginRight)) { chart.marginRight = mathMax( chart.marginRight, @@ -11628,15 +11781,18 @@ // Legend chart.legend = new Legend(chart, options.legend); + chart.getStacks(); // render stacks + // Get margins by pre-rendering axes // set axes scales each(axes, function (axis) { axis.setScale(); }); + chart.getMargins(); chart.maxTicks = null; // reset for second pass each(axes, function (axis) { axis.setTickPositions(true); // update to reflect the new margins @@ -12175,11 +12331,12 @@ var point = this, series = point.series, graphic = point.graphic, i, data = series.data, - chart = series.chart; + chart = series.chart, + seriesOptions = series.options; redraw = pick(redraw, true); // fire the event with a default handler of doing the update point.firePointEvent('update', { options: options }, function () { @@ -12197,15 +12354,17 @@ // record changes in the parallel arrays i = inArray(point, data); series.xData[i] = point.x; series.yData[i] = series.toYData ? series.toYData(point) : point.y; series.zData[i] = point.z; - series.options.data[i] = point.options; + seriesOptions.data[i] = point.options; // redraw - series.isDirty = true; - series.isDirtyData = true; + series.isDirty = series.isDirtyData = chart.isDirtyBox = true; + if (seriesOptions.legendType === 'point') { // #1831, #1885 + chart.legend.destroyItem(point); + } if (redraw) { chart.redraw(animation); } }); }, @@ -12610,10 +12769,11 @@ } // register it series.segments = segments; }, + /** * Set the series options by merging from the options tree * @param {Object} itemOptions */ setOptions: function (itemOptions) { @@ -12712,11 +12872,11 @@ legendOptions = legend.options, legendSymbol, symbolWidth = legendOptions.symbolWidth, renderer = this.chart.renderer, legendItemGroup = this.legendGroup, - baseline = legend.baseline, + verticalCenter = legend.baseline - mathRound(renderer.fontMetrics(legendOptions.itemStyle.fontSize).b * 0.3), attr; // Draw the line if (options.lineWidth) { attr = { @@ -12726,14 +12886,14 @@ attr.dashstyle = options.dashStyle; } this.legendLine = renderer.path([ M, 0, - baseline - 4, + verticalCenter, L, symbolWidth, - baseline - 4 + verticalCenter ]) .attr(attr) .add(legendItemGroup); } @@ -12741,15 +12901,16 @@ if (markerOptions && markerOptions.enabled) { radius = markerOptions.radius; this.legendSymbol = legendSymbol = renderer.symbol( this.symbol, (symbolWidth / 2) - radius, - baseline - 4 - radius, + verticalCenter - radius, 2 * radius, 2 * radius ) .add(legendItemGroup); + legendSymbol.isMarker = true; } }, /** * Add a point dynamically after chart load time @@ -12776,17 +12937,18 @@ point; setAnimation(animation, chart); // Make graph animate sideways - if (graph && shift) { - graph.shift = currentShift + 1; + if (shift) { + each([graph, area, series.graphNeg, series.areaNeg], function (shape) { + if (shape) { + shape.shift = currentShift + 1; + } + }); } if (area) { - if (shift) { // #780 - area.shift = currentShift + 1; - } area.isArea = true; // needed in animation, both with and without shift } // Optional redraw, defaults to true redraw = pick(redraw, true); @@ -12819,16 +12981,16 @@ yData.shift(); zData.shift(); dataOptions.shift(); } } - series.getAttribs(); // redraw series.isDirty = true; series.isDirtyData = true; if (redraw) { + series.getAttribs(); // #1937 chart.redraw(); } }, /** @@ -12855,21 +13017,21 @@ // parallel arrays var xData = [], yData = [], zData = [], dataLength = data ? data.length : [], - turboThreshold = options.turboThreshold || 1000, + turboThreshold = pick(options.turboThreshold, 1000), // docs: 0 to disable pt, pointArrayMap = series.pointArrayMap, valueCount = pointArrayMap && pointArrayMap.length, hasToYData = !!series.toYData; // In turbo mode, only one- or twodimensional arrays of numbers are allowed. The // first value is tested, and we assume that all the rest are defined the same // way. Although the 'for' loops are similar, they are repeated inside each // if-else conditional for max performance. - if (dataLength > turboThreshold) { + if (turboThreshold && dataLength > turboThreshold) { // find the first non-null point i = 0; while (firstPoint === null && i < dataLength) { firstPoint = data[i]; @@ -12911,22 +13073,16 @@ series.pointClass.prototype.applyOptions.apply(pt, [data[i]]); xData[i] = pt.x; yData[i] = hasToYData ? series.toYData(pt) : pt.y; zData[i] = pt.z; if (names && pt.name) { - names[i] = pt.name; + names[pt.x] = pt.name; // #2046 } } } } - // Unsorted data is not supported by the line tooltip as well as data grouping and - // navigation in Stock charts (#725) - if (series.requireSorting && xData.length > 1 && xData[1] < xData[0]) { - error(15); - } - // Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON if (isString(yData[0])) { error(14, true); } @@ -13000,12 +13156,12 @@ processData: function (force) { var series = this, processedXData = series.xData, // copied during slice operation below processedYData = series.yData, dataLength = processedXData.length, + croppedData, cropStart = 0, - cropEnd = dataLength, cropped, distance, closestPointRange, xAxis = series.xAxis, i, // loop variable @@ -13016,69 +13172,95 @@ // If the series data or axes haven't changed, don't go through this. Return false to pass // the message on to override methods like in data grouping. if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) { return false; } + // optionally filter out points outside the plot area if (isCartesian && series.sorted && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) { - var extremes = xAxis.getExtremes(), - min = extremes.min, - max = extremes.max; + var min = xAxis.min, + max = xAxis.max; // it's outside current extremes if (processedXData[dataLength - 1] < min || processedXData[0] > max) { processedXData = []; processedYData = []; // only crop if it's actually spilling out } else if (processedXData[0] < min || processedXData[dataLength - 1] > max) { - - // iterate up to find slice start - for (i = 0; i < dataLength; i++) { - if (processedXData[i] >= min) { - cropStart = mathMax(0, i - 1); - break; - } - } - // proceed to find slice end - for (; i < dataLength; i++) { - if (processedXData[i] > max) { - cropEnd = i + 1; - break; - } - - } - processedXData = processedXData.slice(cropStart, cropEnd); - processedYData = processedYData.slice(cropStart, cropEnd); + croppedData = this.cropData(series.xData, series.yData, min, max); + processedXData = croppedData.xData; + processedYData = croppedData.yData; + cropStart = croppedData.start; cropped = true; } } // Find the closest distance between processed points - for (i = processedXData.length - 1; i > 0; i--) { + for (i = processedXData.length - 1; i >= 0; i--) { distance = processedXData[i] - processedXData[i - 1]; if (distance > 0 && (closestPointRange === UNDEFINED || distance < closestPointRange)) { closestPointRange = distance; + + // Unsorted data is not supported by the line tooltip, as well as data grouping and + // navigation in Stock charts (#725) and width calculation of columns (#1900) + } else if (distance < 0 && series.requireSorting) { + error(15); } } - + // Record the properties series.cropped = cropped; // undefined or true series.cropStart = cropStart; series.processedXData = processedXData; series.processedYData = processedYData; - + if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC series.pointRange = closestPointRange || 1; } series.closestPointRange = closestPointRange; }, /** + * Iterate over xData and crop values between min and max. Returns object containing crop start/end + * cropped xData with corresponding part of yData, dataMin and dataMax within the cropped range + */ + cropData: function (xData, yData, min, max) { + var dataLength = xData.length, + cropStart = 0, + cropEnd = dataLength, + i; + + // iterate up to find slice start + for (i = 0; i < dataLength; i++) { + if (xData[i] >= min) { + cropStart = mathMax(0, i - 1); + break; + } + } + + // proceed to find slice end + for (; i < dataLength; i++) { + if (xData[i] > max) { + cropEnd = i + 1; + break; + } + } + + return { + xData: xData.slice(cropStart, cropEnd), + yData: yData.slice(cropStart, cropEnd), + start: cropStart, + end: cropEnd + }; + }, + + + /** * Generate the data point after the data has been processed by cropping away * unused points and optionally grouped in Highcharts Stock. */ generatePoints: function () { var series = this, @@ -13135,10 +13317,158 @@ series.data = data; series.points = points; }, /** + * Adds series' points value to corresponding stack + */ + setStackedPoints: function () { + if (!this.options.stacking || (this.visible !== true && this.chart.options.chart.ignoreHiddenSeries !== false)) { + return; + } + + var series = this, + xData = series.processedXData, + yData = series.processedYData, + yDataLength = yData.length, + seriesOptions = series.options, + threshold = seriesOptions.threshold, + stackOption = seriesOptions.stack, + stacking = seriesOptions.stacking, + stackKey = series.stackKey, + negKey = '-' + stackKey, + yAxis = series.yAxis, + stacks = yAxis.stacks, + oldStacks = yAxis.oldStacks, + stacksMax = yAxis.stacksMax, + isNegative, + total, + stack, + key, + i, + x, + y; + + // loop over the non-null y values and read them into a local array + for (i = 0; i < yDataLength; i++) { + x = xData[i]; + y = yData[i]; + + // Read stacked values into a stack based on the x value, + // the sign of y and the stack key. Stacking is also handled for null values (#739) + isNegative = y < threshold; + key = isNegative ? negKey : stackKey; + + // Set default stacksMax value for this stack + if (!stacksMax[key]) { + stacksMax[key] = y; + } + + // Create empty object for this stack if it doesn't exist yet + if (!stacks[key]) { + stacks[key] = {}; + } + + // Initialize StackItem for this x + if (oldStacks[key] && oldStacks[key][x]) { + stacks[key][x] = oldStacks[key][x]; + stacks[key][x].total = null; + } else if (!stacks[key][x]) { + stacks[key][x] = new StackItem(yAxis, yAxis.options.stackLabels, isNegative, x, stackOption, stacking); + } + + // If the StackItem doesn't exist, create it first + stack = stacks[key][x]; + total = stack.total; + + + // add value to the stack total + stack.addValue(y); + + stack.cacheExtremes(series, [total, total + y]); + + + if (stack.total > stacksMax[key] && !isNegative) { + stacksMax[key] = stack.total; + } else if (stack.total < stacksMax[key] && isNegative) { + stacksMax[key] = stack.total; + } + } + + // reset old stacks + yAxis.oldStacks = {}; + }, + + /** + * Calculate x and y extremes for visible data + */ + getExtremes: function () { + var xAxis = this.xAxis, + yAxis = this.yAxis, + stackKey = this.stackKey, + options = this.options, + threshold = options.threshold, + xData = this.processedXData, + yData = this.processedYData, + yDataLength = yData.length, + activeYData = [], + activeCounter = 0, + xMin = xAxis.min, + xMax = xAxis.max, + validValue, + withinRange, + dataMin, + dataMax, + x, + y, + i, + j; + + // For stacked series, get the value from the stack + if (options.stacking) { + dataMin = yAxis.stacksMax['-' + stackKey] || threshold; + dataMax = yAxis.stacksMax[stackKey] || threshold; + } + + // If not stacking or threshold is null, iterate over values that are within the visible range + if (!defined(dataMin) || !defined(dataMax)) { + + for (i = 0; i < yDataLength; i++) { + + x = xData[i]; + y = yData[i]; + + // For points within the visible range, including the first point outside the + // visible range, consider y extremes + validValue = y !== null && y !== UNDEFINED && (!yAxis.isLog || (y.length || y > 0)); + withinRange = this.getExtremesFromAll || this.cropped || ((xData[i + 1] || x) >= xMin && + (xData[i - 1] || x) <= xMax); + + if (validValue && withinRange) { + + j = y.length; + if (j) { // array, like ohlc or range data + while (j--) { + if (y[j] !== null) { + activeYData[activeCounter++] = y[j]; + } + } + } else { + activeYData[activeCounter++] = y; + } + } + } + dataMin = pick(dataMin, arrayMin(activeYData)); + dataMax = pick(dataMax, arrayMax(activeYData)); + } + + // Set + this.dataMin = dataMin; + this.dataMax = dataMax; + }, + + /** * Translate data points from raw data values to chart specific positioning data * needed later in drawPoints, drawGraph and drawTracker. */ translate: function () { if (!this.processedXData) { // hidden series @@ -13152,31 +13482,15 @@ categories = xAxis.categories, yAxis = series.yAxis, points = series.points, dataLength = points.length, hasModifyValue = !!series.modifyValue, - isBottomSeries, - allStackSeries, i, - placeBetween = options.pointPlacement === 'between', + pointPlacement = options.pointPlacement, // docs: accept numbers + dynamicallyPlaced = pointPlacement === 'between' || isNumber(pointPlacement), threshold = options.threshold; - //nextSeriesDown; - - // Is it the last visible series? (#809, #1722). - // TODO: After merging in the 'stacking' branch, this logic should probably be moved to Chart.getStacks - allStackSeries = yAxis.series.sort(function (a, b) { - return a.index - b.index; - }); - i = allStackSeries.length; - while (i--) { - if (allStackSeries[i].visible) { - if (allStackSeries[i] === series) { // #809 - isBottomSeries = true; - } - break; - } - } + // Translate each point for (i = 0; i < dataLength; i++) { var point = points[i], xValue = point.x, @@ -13190,20 +13504,22 @@ if (yAxis.isLog && yValue <= 0) { point.y = yValue = null; } // Get the plotX translation - point.plotX = xAxis.translate(xValue, 0, 0, 0, 1, placeBetween); // Math.round fixes #591 + point.plotX = xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement); // Math.round fixes #591 // Calculate the bottom y value for stacked series if (stacking && series.visible && stack && stack[xValue]) { + + pointStack = stack[xValue]; pointStackTotal = pointStack.total; pointStack.cum = yBottom = pointStack.cum - yValue; // start from top yValue = yBottom + yValue; - if (isBottomSeries) { + if (pointStack.cum === 0) { yBottom = pick(threshold, yAxis.min); } if (yAxis.isLog && yBottom <= 0) { // #1200, #1232 yBottom = null; @@ -13215,10 +13531,14 @@ } point.percentage = pointStackTotal ? point.y * 100 / pointStackTotal : 0; point.total = point.stackTotal = pointStackTotal; point.stackY = yValue; + + // Place the stack label + pointStack.setOffset(series.pointXOffset || 0, series.barW || 0); + } // Set translated yBottom or remove it point.yBottom = defined(yBottom) ? yAxis.translate(yBottom, 0, 1, 0, 1) : @@ -13233,11 +13553,11 @@ point.plotY = (typeof yValue === 'number' && yValue !== Infinity) ? mathRound(yAxis.translate(yValue, 0, 1, 0, 1) * 10) / 10 : // Math.round fixes #591 UNDEFINED; // Set client related positions for mouse tracking - point.clientX = placeBetween ? xAxis.translate(xValue, 0, 0, 0, 1) : point.plotX; // #1514 + point.clientX = dynamicallyPlaced ? xAxis.translate(xValue, 0, 0, 0, 1) : point.plotX; // #1514 point.negative = point.y < (threshold || 0); // some API data point.category = categories && categories[point.x] !== UNDEFINED ? @@ -13259,10 +13579,11 @@ low, high, xAxis = series.xAxis, axisLength = xAxis ? (xAxis.tooltipLen || xAxis.len) : series.chart.plotSizeX, // tooltipLen and tooltipPosName used in polar point, + 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) { @@ -13282,19 +13603,28 @@ // 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]; + nextPoint = points[i + 1]; + // Set this range's low to the last range's high plus one low = points[i - 1] ? high + 1 : 0; // Now find the new high high = points[i + 1] ? - mathMax(0, mathFloor((point.clientX + (points[i + 1] ? points[i + 1].clientX : axisLength)) / 2)) : + mathMin(mathMax(0, mathFloor( // #2070 + (point.clientX + (nextPoint ? (nextPoint.wrappedClientX || nextPoint.clientX) : axisLength)) / 2 + )), axisLength) : axisLength; while (low >= 0 && low <= high) { tooltipPoints[low++] = point; } @@ -13510,11 +13840,11 @@ if (seriesMarkerOptions.enabled || series._hasPointMarkers) { i = points.length; while (i--) { point = points[i]; - plotX = point.plotX; + plotX = mathFloor(point.plotX); // #1843 plotY = point.plotY; graphic = point.graphic; pointMarkerOptions = point.marker || {}; enabled = (seriesMarkerOptions.enabled && pointMarkerOptions.enabled === UNDEFINED) || pointMarkerOptions.enabled; isInside = chart.isInsidePlot(mathRound(plotX), plotY, chart.inverted); // #1858 @@ -13736,11 +14066,11 @@ // Do the merge, with some forced options newOptions = merge(oldOptions, { animation: false, index: this.index, pointStart: this.xData[0] // when updating after addPoint - }, newOptions); + }, { data: this.options.data }, newOptions); // Destroy the series and reinsert methods from the type prototype this.remove(false); extend(this, seriesTypes[newOptions.type || oldType].prototype); @@ -13877,16 +14207,16 @@ // Individual labels are disabled if the are explicitly disabled // in the point options, or if they fall outside the plot area. } else if (enabled) { - rotation = options.rotation; - // Create individual options structure that can be extended without // affecting others options = merge(generalOptions, pointOptions); - + + rotation = options.rotation; + // Get the string labelConfig = point.getLabelConfig(); str = options.format ? format(options.format, labelConfig) : options.formatter.call(labelConfig, options); @@ -13978,11 +14308,11 @@ // Add the text size for alignment calculation extend(options, { width: bBox.width, 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, @@ -13994,11 +14324,12 @@ alignAttr = dataLabel.alignAttr; } // Show or hide based on the final aligned position dataLabel.attr({ - visibility: options.crop === false || /*chart.isInsidePlot(alignAttr.x, alignAttr.y) || */chart.isInsidePlot(plotX, plotY, inverted) ? + visibility: options.crop === false || + (chart.isInsidePlot(alignAttr.x, alignAttr.y) && chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height)) ? (chart.renderer.isSVG ? 'inherit' : VISIBLE) : HIDDEN }); }, @@ -14141,55 +14472,60 @@ */ clipNeg: function () { var options = this.options, chart = this.chart, renderer = chart.renderer, - negativeColor = options.negativeColor, + negativeColor = options.negativeColor || options.negativeFillColor, translatedThreshold, posAttr, negAttr, 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; if (negativeColor && (graph || area)) { - translatedThreshold = mathCeil(this.yAxis.len - this.yAxis.translate(options.threshold || 0)); + translatedThreshold = mathRound(yAxis.toPixels(options.threshold || 0, true)); above = { x: 0, y: 0, width: chartSizeMax, height: translatedThreshold }; below = { x: 0, y: translatedThreshold, width: chartSizeMax, - height: chartSizeMax - translatedThreshold + height: chartSizeMax }; - if (chart.inverted && renderer.isVML) { - above = { - x: chart.plotWidth - translatedThreshold - chart.plotLeft, - y: 0, - width: chartWidth, - height: chartHeight - }; - below = { - x: translatedThreshold + chart.plotLeft - chartWidth, - y: 0, - width: chart.plotLeft + translatedThreshold, - height: chartWidth - }; + if (chart.inverted) { + + above.height = below.y = chart.plotWidth - translatedThreshold; + if (renderer.isVML) { + above = { + x: chart.plotWidth - translatedThreshold - chart.plotLeft, + y: 0, + width: chartWidth, + height: chartHeight + }; + below = { + x: translatedThreshold + chart.plotLeft - chartWidth, + y: 0, + width: chart.plotLeft + translatedThreshold, + height: chartWidth + }; + } } - if (this.yAxis.reversed) { + if (yAxis.reversed) { posAttr = below; negAttr = above; } else { posAttr = above; negAttr = below; @@ -14201,11 +14537,11 @@ } else { this.posClip = posClip = renderer.clipRect(posAttr); this.negClip = negClip = renderer.clipRect(negAttr); - if (graph) { + if (graph && this.graphNeg) { graph.clip(posClip); this.graphNeg.clip(negClip); } if (area) { @@ -14258,33 +14594,36 @@ * General abstraction for creating plot groups like series.group, series.dataLabelsGroup and * series.markerGroup. On subsequent calls, the group will only be adjusted to the updated plot size. */ plotGroup: function (prop, name, visibility, zIndex, parent) { var group = this[prop], - isNew = !group, - chart = this.chart, - xAxis = this.xAxis, - yAxis = this.yAxis; + isNew = !group; // Generate it on first call if (isNew) { - this[prop] = group = chart.renderer.g(name) + this[prop] = group = this.chart.renderer.g(name) .attr({ visibility: visibility, zIndex: zIndex || 0.1 // IE8 needs this }) .add(parent); } // Place it on first and subsequent (redraw) calls - group[isNew ? 'attr' : 'animate']({ - translateX: xAxis ? xAxis.left : chart.plotLeft, - translateY: yAxis ? yAxis.top : chart.plotTop, + group[isNew ? 'attr' : 'animate'](this.getPlotBox()); + return group; + }, + + /** + * Get the translation and scale for the plot area of this series + */ + getPlotBox: function () { + return { + translateX: this.xAxis ? this.xAxis.left : this.chart.plotLeft, + translateY: this.yAxis ? this.yAxis.top : this.chart.plotTop, scaleX: 1, // #1623 scaleY: 1 - }); - return group; - + }; }, /** * Render the graph and markers */ @@ -14659,10 +14998,11 @@ stack = yAxis.stacks[this.stackKey], pointMap = {}, plotX, plotY, points = this.points, + 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. @@ -14686,11 +15026,12 @@ // 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 { plotX = xAxis.translate(x); - plotY = yAxis.toPixels(stack[x].cum, true); + val = stack[x].percent ? (stack[x].total ? stack[x].cum * 100 / stack[x].total : 0) : stack[x].cum; // #1991 + plotY = yAxis.toPixels(val, true); segment.push({ y: null, plotX: plotX, clientX: plotX, plotY: plotY, @@ -14782,14 +15123,15 @@ // Define local variables var series = this, areaPath = this.areaPath, options = this.options, negativeColor = options.negativeColor, + negativeFillColor = options.negativeFillColor, props = [['area', this.color, options.fillColor]]; // area name, main color, fill color - if (negativeColor) { - props.push(['areaNeg', options.negativeColor, options.negativeFillColor]); + if (negativeColor || negativeFillColor) { + props.push(['areaNeg', negativeColor, negativeFillColor]); } each(props, function (prop) { var areaKey = prop[0], area = series[areaKey]; @@ -14801,11 +15143,11 @@ } else { // create series[areaKey] = series.chart.renderer.path(areaPath) .attr({ fill: pick( prop[2], - Color(prop[1]).setOpacity(options.fillOpacity || 0.75).get() + Color(prop[1]).setOpacity(pick(options.fillOpacity, 0.75)).get() ), zIndex: 0 // #1069 }).add(series.group); } }); @@ -14971,11 +15313,12 @@ closedStacks: true, // instead of following the previous graph back, follow the threshold back // Mix in methods from the area series getSegmentPath: areaProto.getSegmentPath, closeSegment: areaProto.closeSegment, - drawGraph: areaProto.drawGraph + drawGraph: areaProto.drawGraph, + drawLegendSymbol: areaProto.drawLegendSymbol }); seriesTypes.areaspline = AreaSplineSeries; /** * Set the default options for column @@ -15017,11 +15360,10 @@ * ColumnSeries object */ var ColumnSeries = extendClass(Series, { type: 'column', tooltipOutsidePlot: true, - requireSorting: false, pointAttrToOptions: { // mapping between SVG attributes and the corresponding options stroke: 'borderColor', 'stroke-width': 'borderWidth', fill: 'color', r: 'borderRadius' @@ -15053,11 +15395,10 @@ * pointWidth etc. */ getColumnMetrics: function () { var series = this, - chart = series.chart, options = series.options, xAxis = this.xAxis, reversedXAxis = xAxis.reversed, stackKey, stackGroups = {}, @@ -15068,11 +15409,11 @@ // This is called on every series. Consider moving this logic to a // chart.orderStacks() function and call it on init, addSeries and removeSeries if (options.grouping === false) { columnCount = 1; } else { - each(chart.series, function (otherSeries) { + each(series.yAxis.series, function (otherSeries) { // use Y axes separately, #642 var otherOptions = otherSeries.options; if (otherSeries.type === series.type && otherSeries.visible && series.options.group === otherOptions.group) { // used in Stock charts navigator series if (otherOptions.stacking) { stackKey = otherSeries.stackKey; @@ -15119,46 +15460,39 @@ */ translate: function () { var series = this, chart = series.chart, options = series.options, - stacking = options.stacking, borderWidth = options.borderWidth, yAxis = series.yAxis, threshold = options.threshold, translatedThreshold = series.translatedThreshold = yAxis.getThreshold(threshold), minPointLength = pick(options.minPointLength, 5), metrics = series.getColumnMetrics(), pointWidth = metrics.width, - barW = mathCeil(mathMax(pointWidth, 1 + 2 * borderWidth)), // rounded and postprocessed for border width - pointXOffset = metrics.offset; + barW = series.barW = mathCeil(mathMax(pointWidth, 1 + 2 * borderWidth)), // rounded and postprocessed for border width + pointXOffset = series.pointXOffset = metrics.offset; Series.prototype.translate.apply(series); // record the new values each(series.points, function (point) { var plotY = mathMin(mathMax(-999, point.plotY), yAxis.len + 999), // Don't draw too far outside plot area (#1303) yBottom = pick(point.yBottom, translatedThreshold), barX = point.plotX + pointXOffset, barY = mathCeil(mathMin(plotY, yBottom)), barH = mathCeil(mathMax(plotY, yBottom) - barY), - stack = yAxis.stacks[(point.y < 0 ? '-' : '') + series.stackKey], shapeArgs; - // Record the offset'ed position and width of the bar to be able to align the stacking total correctly - if (stacking && series.visible && stack && stack[point.x]) { - stack[point.x].setOffset(pointXOffset, barW); - } - // handle options.minPointLength if (mathAbs(barH) < minPointLength) { if (minPointLength) { barH = minPointLength; barY = - mathAbs(barY - translatedThreshold) > minPointLength ? // stacked + mathRound(mathAbs(barY - translatedThreshold) > minPointLength ? // stacked yBottom - minPointLength : // keep position - translatedThreshold - (yAxis.translate(point.y, 0, 1, 0, 1) <= translatedThreshold ? minPointLength : 0); // use exact yAxis.translation (#1485) + translatedThreshold - (yAxis.translate(point.y, 0, 1, 0, 1) <= translatedThreshold ? minPointLength : 0)); // use exact yAxis.translation (#1485) } } point.barX = barX; point.pointWidth = pointWidth; @@ -15230,24 +15564,26 @@ * Add tracking event listener to the series group, so the point graphics * themselves act as trackers */ drawTracker: function () { var series = this, - pointer = series.chart.pointer, + chart = series.chart, + pointer = chart.pointer, cursor = series.options.cursor, css = cursor && { cursor: cursor }, onMouseOver = function (e) { var target = e.target, point; - series.onMouseOver(); - + if (chart.hoverSeries !== series) { + series.onMouseOver(); + } while (target && !point) { point = target.point; target = target.parentNode; } - if (point !== UNDEFINED) { // undefined on graph in scatterchart + if (point !== UNDEFINED && point !== chart.hoverPoint) { // undefined on graph in scatterchart point.onMouseOver(e); } }; // Add reference to the point @@ -15493,12 +15829,12 @@ visible: point.visible !== false, name: pick(point.name, 'Slice') }); // add event listener for select - toggleSlice = function () { - point.slice(); + toggleSlice = function (e) { + point.slice(e.type === 'select'); }; addEvent(point, 'select', toggleSlice); addEvent(point, 'unselect', toggleSlice); return point; @@ -15639,10 +15975,43 @@ this.generatePoints(); if (pick(redraw, true)) { this.chart.redraw(); } }, + + /** + * Extend the generatePoints method by adding total and percentage properties to each point + */ + generatePoints: function () { + var i, + total = 0, + points, + len, + point, + ignoreHiddenPoint = this.options.ignoreHiddenPoint; + + Series.prototype.generatePoints.call(this); + + // Populate local vars + points = this.points; + len = points.length; + + // Get the total sum + for (i = 0; i < len; i++) { + point = points[i]; + total += (ignoreHiddenPoint && !point.visible) ? 0 : point.y; + } + this.total = total; + + // Set each point's properties + for (i = 0; i < len; i++) { + point = points[i]; + point.percentage = (point.y / total) * 100; + point.total = total; + } + + }, /** * 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. */ @@ -15677,12 +16046,11 @@ * Do translation for pie slices */ translate: function (positions) { this.generatePoints(); - var total = 0, - series = this, + var series = this, cumulative = 0, precision = 1000, // issue #172 options = series.options, slicedOffset = options.slicedOffset, connectorOffset = slicedOffset + options.borderWidth, @@ -15690,11 +16058,10 @@ end, angle, startAngleRad = series.startAngleRad = mathPI / 180 * ((options.startAngle || 0) % 360 - 90), points = series.points, circ = 2 * mathPI, - fraction, radiusX, // the x component of the radius vector for a given point radiusY, labelDistance = options.dataLabels.distance, ignoreHiddenPoint = options.ignoreHiddenPoint, i, @@ -15716,26 +16083,19 @@ return positions[0] + (left ? -1 : 1) * (mathCos(angle) * (positions[2] / 2 + labelDistance)); }; - // get the total sum - for (i = 0; i < len; i++) { - point = points[i]; - total += (ignoreHiddenPoint && !point.visible) ? 0 : point.y; - } - // Calculate the geometry for each point for (i = 0; i < len; i++) { point = points[i]; // set start and end angle - fraction = total ? point.y / total : 0; start = mathRound((startAngleRad + (cumulative * circ)) * precision) / precision; if (!ignoreHiddenPoint || point.visible) { - cumulative += fraction; + cumulative += point.percentage / 100; } end = mathRound((startAngleRad + (cumulative * circ)) * precision) / precision; // set the shape point.shapeType = 'arc'; @@ -15781,14 +16141,10 @@ labelDistance < 0 ? // alignment 'center' : point.half ? 'right' : 'left', // alignment angle // center angle ]; - - // API properties - point.percentage = fraction * 100; - point.total = total; } this.setTooltipPoints(); @@ -15907,10 +16263,10 @@ return a.angle !== undefined && (b.angle - a.angle) * sign; }); }; // get out if not enabled - if (!options.enabled && !series._hasPointLabels) { + if (!series.visible || (!options.enabled && !series._hasPointLabels)) { return; } // run parent method Series.prototype.drawDataLabels.apply(series);