app/assets/javascripts/highcharts.js in highcharts-rails-4.2.0 vs app/assets/javascripts/highcharts.js in highcharts-rails-4.2.2

- old
+ new

@@ -1,12 +1,12 @@ // ==ClosureCompiler== // @compilation_level SIMPLE_OPTIMIZATIONS /** - * @license Highcharts JS v4.2.0 (2105-12-15) + * @license Highcharts JS v4.2.2 (2016-02-04) * - * (c) 2009-2014 Torstein Honsi + * (c) 2009-2016 Torstein Honsi * * License: www.highcharts.com/license */ (function (root, factory) { @@ -57,11 +57,11 @@ timeUnits, noop = function () {}, charts = [], chartCount = 0, PRODUCT = 'Highcharts', - VERSION = '4.2.0', + VERSION = '4.2.2', // some constants for frequently used strings DIV = 'div', ABSOLUTE = 'absolute', RELATIVE = 'relative', @@ -310,59 +310,69 @@ endLength, slice, i, start = fromD.split(' '), end = [].concat(toD), // copy - startBaseLine, - endBaseLine, + isArea = elem.isArea, + positionFactor = isArea ? 2 : 1, sixify = function (arr) { // in splines make move points have six parameters like bezier curves i = arr.length; while (i--) { - if (arr[i] === M) { + if (arr[i] === M || arr[i] === L) { arr.splice(i + 1, 0, arr[i + 1], arr[i + 2], arr[i + 1], arr[i + 2]); } } }; if (bezier) { sixify(start); sixify(end); } - // pull out the base lines before padding - if (elem.isArea) { - startBaseLine = start.splice(start.length - 6, 6); - endBaseLine = end.splice(end.length - 6, 6); - } - - // if shifting points, prepend a dummy point to the end path + // If shifting points, prepend a dummy point to the end path. For areas, + // prepend both at the beginning and end of the path. if (shift <= end.length / numParams && start.length === end.length) { while (shift--) { - end = [].concat(end).splice(0, numParams).concat(end); + end = end.slice(0, numParams).concat(end); + if (isArea) { + end = end.concat(end.slice(end.length - numParams)); + } } } elem.shift = 0; // reset for following animations - // copy and append last point until the length matches the end length + + // Copy and append last point until the length matches the end length if (start.length) { endLength = end.length; while (start.length < endLength) { - //bezier && sixify(start); - slice = [].concat(start).splice(start.length - numParams, numParams); - if (bezier) { // disable first control point + // Pull out the slice that is going to be appended or inserted. In a line graph, + // the positionFactor is 1, and the last point is sliced out. In an area graph, + // the positionFactor is 2, causing the middle two points to be sliced out, since + // an area path starts at left, follows the upper path then turns and follows the + // bottom back. + slice = start.slice().splice( + (start.length / positionFactor) - numParams, + numParams * positionFactor + ); + + // Disable first control point + if (bezier) { slice[numParams - 6] = slice[numParams - 2]; slice[numParams - 5] = slice[numParams - 1]; } - start = start.concat(slice); + + // Now insert the slice, either in the middle (for areas) or at the end (for lines) + [].splice.apply( + start, + [(start.length / positionFactor), 0].concat(slice) + ); + } } - if (startBaseLine) { // append the base lines for areas - start = start.concat(startBaseLine); - end = end.concat(endBaseLine); - } return [start, end]; } }; // End of Fx prototype @@ -999,28 +1009,59 @@ /** * Format a number and return a string based on input settings * @param {Number} number The input number to format * @param {Number} decimals The amount of decimals - * @param {String} decPoint The decimal point, defaults to the one given in the lang options + * @param {String} decimalPoint The decimal point, defaults to the one given in the lang options * @param {String} thousandsSep The thousands separator, defaults to the one given in the lang options */ - Highcharts.numberFormat = function (number, decimals, decPoint, thousandsSep) { + Highcharts.numberFormat = function (number, decimals, decimalPoint, thousandsSep) { + + number = +number || 0; + var lang = defaultOptions.lang, - // http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_number_format/ - n = +number || 0, - c = decimals === -1 ? - Math.min((n.toString().split('.')[1] || '').length, 20) : // Preserve decimals. Not huge numbers (#3793). - (isNaN(decimals = Math.abs(decimals)) ? 2 : decimals), - d = decPoint === undefined ? lang.decimalPoint : decPoint, - t = thousandsSep === undefined ? lang.thousandsSep : thousandsSep, - s = n < 0 ? '-' : '', - i = String(pInt(n = mathAbs(n).toFixed(c))), - j = i.length > 3 ? i.length % 3 : 0; + origDec = (number.toString().split('.')[1] || '').length, + decimalComponent, + strinteger, + thousands, + absNumber = Math.abs(number), + ret; - return (s + (j ? i.substr(0, j) + t : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + t) + - (c ? d + mathAbs(n - i).toFixed(c).slice(2) : '')); + if (decimals === -1) { + decimals = Math.min(origDec, 20); // Preserve decimals. Not huge numbers (#3793). + } else if (isNaN(decimals)) { + decimals = 2; + } + + // A string containing the positive integer component of the number + strinteger = String(pInt(absNumber.toFixed(decimals))); + + // Leftover after grouping into thousands. Can be 0, 1 or 3. + thousands = strinteger.length > 3 ? strinteger.length % 3 : 0; + + // Language + decimalPoint = pick(decimalPoint, lang.decimalPoint); + thousandsSep = pick(thousandsSep, lang.thousandsSep); + + // Start building the return + ret = number < 0 ? '-' : ''; + + // Add the leftover after grouping into thousands. For example, in the number 42 000 000, + // this line adds 42. + ret += thousands ? strinteger.substr(0, thousands) + thousandsSep : ''; + + // Add the remaining thousands groups, joined by the thousands separator + ret += strinteger.substr(thousands).replace(/(\d{3})(?=\d)/g, '$1' + thousandsSep); + + // Add the decimal point and the decimal component + if (+decimals) { + // Get the decimal component, and add power to avoid rounding errors with float numbers (#4573) + decimalComponent = Math.abs(absNumber - strinteger + Math.pow(10, -Math.max(decimals, origDec) - 1)); + ret += decimalPoint + decimalComponent.toFixed(decimals).slice(2); + } + + return ret; }; /** * Easing definition * @param {Number} pos Current position, ranging from 0 to 1 @@ -1031,11 +1072,22 @@ /** * Internal method to return CSS value for given element and property */ getStyle = function (el, prop) { - var style = win.getComputedStyle(el, undefined); + + var style; + + // For width and height, return the actual inner pixel size (#4913) + if (prop === 'width') { + return Math.min(el.offsetWidth, el.scrollWidth) - getStyle(el, 'padding-left') - getStyle(el, 'padding-right'); + } else if (prop === 'height') { + return Math.min(el.offsetHeight, el.scrollHeight) - getStyle(el, 'padding-top') - getStyle(el, 'padding-bottom'); + } + + // Otherwise, get the computed style + style = win.getComputedStyle(el, undefined); return style && pInt(style.getPropertyValue(prop)); }; /** * Return the index of an item in an array, or -1 if not found @@ -1479,11 +1531,11 @@ }, global: { useUTC: true, //timezoneOffset: 0, canvasToolsURL: 'http://code.highcharts.com/modules/canvas-tools.js', - VMLRadialGradientURL: 'http://code.highcharts.com/4.2.0/gfx/vml-radial-gradient.png' + VMLRadialGradientURL: 'http://code.highcharts.com/4.2.2/gfx/vml-radial-gradient.png' }, chart: { //animation: true, //alignTicks: false, //reflow: true, @@ -2179,12 +2231,10 @@ /** * Apply a polyfill to the text-stroke CSS property, by copying the text element * and apply strokes to the copy. * * Contrast checks at http://jsfiddle.net/highcharts/43soe9m1/2/ - * - * docs: update default, document the polyfill and the limitations on hex colors and pixel values, document contrast pseudo-color */ applyTextShadow: function (textShadow) { var elem = this.element, tspans, hasContrast = textShadow.indexOf('contrast') !== -1, @@ -2275,11 +2325,12 @@ var key, value, element = this.element, hasSetSymbolSize, ret = this, - skipAttr; + skipAttr, + setter; // single key-value pair if (typeof hash === 'string' && val !== UNDEFINED) { key = hash; hash = {}; @@ -2310,16 +2361,17 @@ if (this.rotation && (key === 'x' || key === 'y')) { this.doTransform = true; } if (!skipAttr) { - (this[key + 'Setter'] || this._defaultSetter).call(this, value, key, element); - } + setter = this[key + 'Setter'] || this._defaultSetter; + setter.call(this, value, key, element); - // Let the shadow follow the main element - if (this.shadows && /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(key)) { - this.updateShadows(key, value); + // Let the shadow follow the main element + if (this.shadows && /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(key)) { + this.updateShadows(key, value, setter); + } } } // Update transform. Do this outside the loop to prevent redundant updating for batch setting // of attributes. @@ -2336,19 +2388,29 @@ } return ret; }, - updateShadows: function (key, value) { + /** + * Update the shadow elements with new attributes + * @param {String} key The attribute name + * @param {String|Number} value The value of the attribute + * @param {Function} setter The setter function, inherited from the parent wrapper + * @returns {undefined} + */ + updateShadows: function (key, value, setter) { var shadows = this.shadows, i = shadows.length; + while (i--) { - shadows[i].setAttribute( - key, + setter.call( + null, key === 'height' ? - Math.max(value - (shadows[i].cutHeight || 0), 0) : - key === 'd' ? this.d : value + Math.max(value - (shadows[i].cutHeight || 0), 0) : + key === 'd' ? this.d : value, + key, + shadows[i] ); } }, /** @@ -2715,11 +2777,11 @@ /** * Get the bounding box (width, height, x and y) for the element */ getBBox: function (reload, rot) { var wrapper = this, - bBox,// = wrapper.bBox, + bBox, // = wrapper.bBox, renderer = wrapper.renderer, width, height, rotation, rad, @@ -3284,10 +3346,11 @@ renderer.allowHTML = allowHTML; renderer.forExport = forExport; renderer.gradients = {}; // Object where gradient SvgElements are stored renderer.cache = {}; // Cache for numerical bounding boxes renderer.cacheKeys = []; + renderer.imgCount = 0; renderer.setSize(width, height, false); @@ -3830,16 +3893,15 @@ */ circle: function (x, y, r) { var attr = isObject(x) ? x : { x: x, y: y, r: r }, wrapper = this.createElement('circle'); - wrapper.xSetter = function (value) { - this.element.setAttribute('cx', value); + // Setting x or y translates to cx and cy + wrapper.xSetter = wrapper.ySetter = function (value, key, element) { + element.setAttribute('c' + key, value); }; - wrapper.ySetter = function (value) { - this.element.setAttribute('cy', value); - }; + return wrapper.attr(attr); }, /** * Draw and return an arc @@ -3995,11 +4057,12 @@ * @param {Object} radius * @param {Object} options */ symbol: function (symbol, x, y, width, height, options) { - var obj, + var ren = this, + obj, // get the symbol definition function symbolFn = this.symbols[symbol], // check if there's a path defined for this symbol @@ -4089,14 +4152,21 @@ // Clean up after #2854 workaround. if (this.parentNode) { this.parentNode.removeChild(this); } + + // Fire the load event when all external images are loaded + ren.imgCount--; + if (!ren.imgCount) { + charts[ren.chartIndex].onload(); + } }, src: imageSrc }); } + this.imgCount++; } return obj; }, @@ -4770,14 +4840,14 @@ }); } if (elem.tagName === 'SPAN') { - var width, - rotation = wrapper.rotation, + var rotation = wrapper.rotation, baseline, textWidth = pInt(wrapper.textWidth), + whiteSpace = styles && styles.whiteSpace, currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth, wrapper.textAlign].join(','); if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed @@ -4786,23 +4856,28 @@ // Renderer specific handling of span rotation if (defined(rotation)) { wrapper.setSpanRotation(rotation, alignCorrection, baseline); } - width = pick(wrapper.elemWidth, elem.offsetWidth); - // Update textWidth - if (width > textWidth && /[ \-]/.test(elem.textContent || elem.innerText)) { // #983, #1254 + if (elem.offsetWidth > textWidth && /[ \-]/.test(elem.textContent || elem.innerText)) { // #983, #1254 css(elem, { width: textWidth + PX, display: 'block', - whiteSpace: (styles && styles.whiteSpace) || 'normal' // #3331 + whiteSpace: whiteSpace || 'normal' // #3331 }); - width = textWidth; + wrapper.hasTextWidth = true; + } else if (wrapper.hasTextWidth) { // #4928 + css(elem, { + width: '', + display: '', + whiteSpace: whiteSpace || 'nowrap' + }); + wrapper.hasTextWidth = false; } - wrapper.getSpanCorrection(width, baseline, alignCorrection, rotation, align); + wrapper.getSpanCorrection(wrapper.hasTextWidth ? textWidth : elem.offsetWidth, baseline, alignCorrection, rotation, align); } // apply position with correction css(elem, { left: (x + (wrapper.xCorr || 0)) + PX, @@ -4851,20 +4926,31 @@ * @param {Number} y */ html: function (str, x, y) { var wrapper = this.createElement('span'), element = wrapper.element, - renderer = wrapper.renderer; + renderer = wrapper.renderer, + addSetters = function (element, style) { + // These properties are set as attributes on the SVG group, and as + // identical CSS properties on the div. (#3542) + each(['opacity', 'visibility'], function (prop) { + wrap(element, prop + 'Setter', function (proceed, value, key, elem) { + proceed.call(this, value, key, elem); + style[key] = value; + }); + }); + }; // Text setter wrapper.textSetter = function (value) { if (value !== element.innerHTML) { delete this.bBox; } element.innerHTML = this.textStr = value; wrapper.htmlUpdateTransform(); }; + addSetters(wrapper, wrapper.element.style); // Various setters which rely on update transform wrapper.xSetter = wrapper.ySetter = wrapper.alignSetter = wrapper.rotationSetter = function (value, key) { if (key === 'align') { key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML. @@ -4950,19 +5036,11 @@ htmlGroupStyle.top = value + PX; parentGroup[key] = value; parentGroup.doTransform = true; } }); - - // These properties are set as attributes on the SVG group, and as - // identical CSS properties on the div. (#3542) - each(['opacity', 'visibility'], function (prop) { - wrap(parentGroup, prop + 'Setter', function (proceed, value, key, elem) { - proceed.call(this, value, key, elem); - htmlGroupStyle[key] = value; - }); - }); + addSetters(parentGroup, htmlGroupStyle); }); } } else { htmlGroup = container; @@ -5521,10 +5599,11 @@ renderer.box = box; renderer.boxWrapper = boxWrapper; renderer.gradients = {}; renderer.cache = {}; // Cache for numerical bounding boxes renderer.cacheKeys = []; + renderer.imgCount = 0; renderer.setSize(width, height, false); // The only way to make IE6 and IE7 print is to use a global namespace. However, @@ -5971,11 +6050,11 @@ if (options.open && !innerRadius) { ret.push( 'e', M, - x,// - innerRadius, + x, // - innerRadius, y// - innerRadius ); } ret.push( @@ -6369,17 +6448,17 @@ /** * Extendible method to return the path of the marker */ getMarkPath: function (x, y, tickLength, tickWidth, horiz, renderer) { return renderer.crispLine([ - M, - x, - y, - L, - x + (horiz ? 0 : -tickLength), - y + (horiz ? tickLength : 0) - ], tickWidth); + M, + x, + y, + L, + x + (horiz ? 0 : -tickLength), + y + (horiz ? tickLength : 0) + ], tickWidth); }, /** * Put everything in place * @@ -6559,16 +6638,12 @@ dashStyle = options.dashStyle, svgElem = plotLine.svgElem, path = [], addEvent, eventType, - xs, - ys, - x, - y, color = options.color, - zIndex = options.zIndex, + zIndex = pick(options.zIndex, 0), events = options.events, attribs = {}, renderer = axis.chart.renderer; // logarithmic conversion @@ -6600,13 +6675,11 @@ } } else { return; } // zIndex - if (defined(zIndex)) { - attribs.zIndex = zIndex; - } + attribs.zIndex = zIndex; // common for lines and bands if (svgElem) { if (path) { svgElem.show(); @@ -6644,54 +6717,70 @@ verticalAlign: !horiz && isBand && 'middle', y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4, rotation: horiz && !isBand && 90 }, optionsLabel); - // add the SVG element - if (!label) { - attribs = { - align: optionsLabel.textAlign || optionsLabel.align, - rotation: optionsLabel.rotation - }; - if (defined(zIndex)) { - attribs.zIndex = zIndex; - } - plotLine.label = label = renderer.text( - optionsLabel.text, - 0, - 0, - optionsLabel.useHTML - ) - .attr(attribs) - .css(optionsLabel.style) - .add(); - } + this.renderLabel(optionsLabel, path, isBand, zIndex); - // get the bounding box and align the label - // #3000 changed to better handle choice between plotband or plotline - xs = [path[1], path[4], (isBand ? path[6] : path[1])]; - ys = [path[2], path[5], (isBand ? path[7] : path[2])]; - x = arrayMin(xs); - y = arrayMin(ys); - - label.align(optionsLabel, false, { - x: x, - y: y, - width: arrayMax(xs) - x, - height: arrayMax(ys) - y - }); - label.show(); - } else if (label) { // move out of sight label.hide(); } // chainable return plotLine; }, /** + * Render and align label for plot line or band. + */ + renderLabel: function (optionsLabel, path, isBand, zIndex) { + var plotLine = this, + label = plotLine.label, + renderer = plotLine.axis.chart.renderer, + attribs, + xs, + ys, + x, + y; + + // add the SVG element + if (!label) { + attribs = { + align: optionsLabel.textAlign || optionsLabel.align, + rotation: optionsLabel.rotation + }; + + attribs.zIndex = zIndex; + + plotLine.label = label = renderer.text( + optionsLabel.text, + 0, + 0, + optionsLabel.useHTML + ) + .attr(attribs) + .css(optionsLabel.style) + .add(); + } + + // get the bounding box and align the label + // #3000 changed to better handle choice between plotband or plotline + xs = [path[1], path[4], (isBand ? path[6] : path[1])]; + ys = [path[2], path[5], (isBand ? path[7] : path[2])]; + x = arrayMin(xs); + y = arrayMin(ys); + + label.align(optionsLabel, false, { + x: x, + y: y, + width: arrayMax(xs) - x, + height: arrayMax(ys) - y + }); + label.show(); + }, + + /** * Remove the plot line or band */ destroy: function () { // remove it from the lookup erase(this.axis.plotLinesAndBands, this); @@ -7294,11 +7383,11 @@ cvsOffset = 0, localA = old ? axis.oldTransA : axis.transA, localMin = old ? axis.oldMin : axis.min, returnValue, minPixelPadding = axis.minPixelPadding, - doPostTranslate = (axis.doPostTranslate || (axis.isLog && handleLog)) && axis.lin2val; + doPostTranslate = (axis.isOrdinal || axis.isBroken || (axis.isLog && handleLog)) && axis.lin2val; if (!localA) { localA = axis.transA; } @@ -7959,18 +8048,22 @@ roundedMax = tickPositions[tickPositions.length - 1], minPointOffset = this.minPointOffset || 0; if (startOnTick) { this.min = roundedMin; - } else if (this.min - minPointOffset > roundedMin) { - tickPositions.shift(); + } else { + while (this.min - minPointOffset > tickPositions[0]) { + tickPositions.shift(); + } } if (endOnTick) { this.max = roundedMax; - } else if (this.max + minPointOffset < roundedMax) { - tickPositions.pop(); + } else { + while (this.max + minPointOffset < tickPositions[tickPositions.length - 1]) { + tickPositions.pop(); + } } // If no tick are left, set one tick in the middle (#3195) if (tickPositions.length === 0 && defined(roundedMin)) { tickPositions.push((roundedMax + roundedMin) / 2); @@ -8223,16 +8316,17 @@ height = pick(options.height, chart.plotHeight), top = pick(options.top, chart.plotTop), left = pick(options.left, chart.plotLeft + offsetLeft), percentRegex = /%$/; - // Check for percentage based input values + // Check for percentage based input values. Rounding fixes problems with + // column overflow and plot line filtering (#4898, #4899) if (percentRegex.test(height)) { - height = parseFloat(height) / 100 * chart.plotHeight; + height = Math.round(parseFloat(height) / 100 * chart.plotHeight); } if (percentRegex.test(top)) { - top = parseFloat(top) / 100 * chart.plotHeight + chart.plotTop; + top = Math.round(parseFloat(top) / 100 * chart.plotHeight + chart.plotTop); } // Expose basic values to use in Series object and navigator this.left = left; this.top = top; @@ -8452,11 +8546,14 @@ css.textOverflow = 'ellipsis'; } } // Set the explicit or automatic label alignment - this.labelAlign = attr.align = labelOptions.align || this.autoLabelAlign(this.labelRotation); + this.labelAlign = labelOptions.align || this.autoLabelAlign(this.labelRotation); + if (this.labelAlign) { + attr.align = this.labelAlign; + } // Apply general and specific CSS each(tickPositions, function (pos) { var tick = ticks[pos], label = tick && tick.label; @@ -8981,13 +9078,11 @@ if ( // Disabled in options !this.crosshair || // Snap - ((defined(point) || !pick(options.snap, true)) === false) || - // Not on this axis (#4095, #2888) - (point && point.series && point.series[this.coll] !== this) + ((defined(point) || !pick(options.snap, true)) === false) ) { this.hideCrosshair(); } else { @@ -9120,10 +9215,11 @@ } minYear = minDate[getFullYear](); var time = minDate.getTime(), minMonth = minDate[getMonth](), minDateDate = minDate[getDate](), + variableDayLength = !useUTC || !!getTimezoneOffset, // #4951 localTimezoneOffset = (timeUnits.day + (useUTC ? getTZOffset(minDate) : minDate.getTimezoneOffset() * 60 * 1000) ) % timeUnits.day; // #950, #3359 // iterate and add tick positions at appropriate values @@ -9138,11 +9234,11 @@ } 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 (variableDayLength && (interval === timeUnits.day || interval === timeUnits.week)) { time = makeTime(minYear, minMonth, minDateDate + i * count * (interval === timeUnits.day ? 1 : 7)); // else, the interval is fixed and we use simple addition } else { @@ -9742,15 +9838,15 @@ }); this.isHidden = false; } fireEvent(chart, 'tooltipRefresh', { - text: text, - x: x + chart.plotLeft, - y: y + chart.plotTop, - borderColor: borderColor - }); + text: text, + x: x + chart.plotLeft, + y: y + chart.plotTop, + borderColor: borderColor + }); }, /** * Find the new position and perform the move */ @@ -9970,13 +10066,13 @@ * * @param {Object} e A pointer event */ getCoordinates: function (e) { var coordinates = { - xAxis: [], - yAxis: [] - }; + xAxis: [], + yAxis: [] + }; each(this.chart.axes, function (axis) { coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({ axis: axis, value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY']) @@ -9998,18 +10094,17 @@ shared = tooltip ? tooltip.shared : false, followPointer, hoverPoint = chart.hoverPoint, hoverSeries = chart.hoverSeries, i, - distance = Number.MAX_VALUE, // #4511 + distance = [Number.MAX_VALUE, Number.MAX_VALUE], // #4511 anchor, noSharedTooltip, stickToHoverSeries, directTouch, - pointDistance, kdpoints = [], - kdpoint, + kdpoint = [], kdpointT; // For hovering over the empty parts of the plot area (hoverSeries is undefined). // If there is one series with point tracking (combo chart), don't go to nearest neighbour. if (!shared && !hoverSeries) { @@ -10023,11 +10118,11 @@ // If it has a hoverPoint and that series requires direct touch (like columns, #3899), or we're on // a noSharedTooltip series among shared tooltip series (#4546), use the hoverPoint . Otherwise, // search the k-d tree. stickToHoverSeries = hoverSeries && (shared ? hoverSeries.noSharedTooltip : hoverSeries.directTouch); if (stickToHoverSeries && hoverPoint) { - kdpoint = hoverPoint; + kdpoint = [hoverPoint]; // Handle shared tooltip or cases where a series is not yet hovered } else { // Find nearest points on all series each(series, function (s) { @@ -10041,46 +10136,54 @@ } } }); // Find absolute nearest point each(kdpoints, function (p) { - pointDistance = !shared && p.series.kdDimensions === 1 ? p.dist : p.distX; // #4645 - - if (p && typeof pointDistance === 'number' && pointDistance < distance) { - distance = pointDistance; - kdpoint = p; + if (p) { + // Store both closest points, using point.dist and point.distX comparisons (#4645): + each(['dist', 'distX'], function (dist, k) { + if (typeof p[dist] === 'number' && p[dist] < distance[k]) { + distance[k] = p[dist]; + kdpoint[k] = p; + } + }); } }); } + // Remove points with different x-positions, required for shared tooltip and crosshairs (#4645): + if (shared) { + i = kdpoints.length; + while (i--) { + if (kdpoints[i].clientX !== kdpoint[1].clientX || kdpoints[i].series.noSharedTooltip) { + kdpoints.splice(i, 1); + } + } + } + // Refresh tooltip for kdpoint if new hover point or tooltip was hidden // #3926, #4200 - if (kdpoint && (kdpoint !== this.prevKDPoint || (tooltip && tooltip.isHidden))) { + if (kdpoint[0] && (kdpoint[0] !== this.prevKDPoint || (tooltip && tooltip.isHidden))) { // Draw tooltip if necessary - if (shared && !kdpoint.series.noSharedTooltip) { - i = kdpoints.length; - while (i--) { - if (kdpoints[i].clientX !== kdpoint.clientX || kdpoints[i].series.noSharedTooltip) { - kdpoints.splice(i, 1); - } - } + if (shared && !kdpoint[0].series.noSharedTooltip) { if (kdpoints.length && tooltip) { tooltip.refresh(kdpoints, e); } // Do mouseover on all points (#3919, #3985, #4410) each(kdpoints, function (point) { - point.onMouseOver(e, point !== ((hoverSeries && hoverSeries.directTouch && hoverPoint) || kdpoint)); + point.onMouseOver(e, point !== ((hoverSeries && hoverSeries.directTouch && hoverPoint) || kdpoint[0])); }); + this.prevKDPoint = kdpoint[1]; } else { if (tooltip) { - tooltip.refresh(kdpoint, e); + tooltip.refresh(kdpoint[0], e); } if (!hoverSeries || !hoverSeries.directTouch) { // #4448 - kdpoint.onMouseOver(e); + kdpoint[0].onMouseOver(e); } + this.prevKDPoint = kdpoint[0]; } - this.prevKDPoint = kdpoint; // Update positions (regardless of kdpoint or hoverPoint) } else { followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer; if (tooltip && followPointer && !tooltip.isHidden) { @@ -10098,15 +10201,21 @@ }; addEvent(doc, 'mousemove', pointer._onDocumentMouseMove); } // Crosshair - each(chart.axes, function (axis) { - axis.drawCrosshair(e, pick(kdpoint, hoverPoint)); + each(shared ? kdpoints : [pick(kdpoint[1], hoverPoint)], function (point) { + var series = point && point.series; + if (series) { + each(['xAxis', 'yAxis', 'colorAxis'], function (coll) { + if (series[coll]) { + series[coll].drawCrosshair(e, point); + } + }); + } }); - }, /** @@ -10327,10 +10436,11 @@ chart = this.chart, hasPinched = this.hasPinched; if (this.selectionMarker) { var selectionData = { + originalEvent: e, // #4890 xAxis: [], yAxis: [] }, selectionBox = this.selectionMarker, selectionLeft = selectionBox.attr ? selectionBox.attr('x') : selectionBox.x, @@ -10420,24 +10530,26 @@ }, /** * When mouse leaves the container, hide the tooltip. */ - onContainerMouseLeave: function () { + onContainerMouseLeave: function (e) { var chart = charts[hoverChartIndex]; - if (chart) { + if (chart && (e.relatedTarget || e.toElement)) { // #4886, MS Touch end fires mouseleave but with no related target chart.pointer.reset(); chart.pointer.chartPosition = null; // also reset the chart position, used in #149 fix } }, // The mousemove, touchmove and touchstart event handler onContainerMouseMove: function (e) { var chart = this.chart; - hoverChartIndex = chart.index; + if (!defined(hoverChartIndex) || !charts[hoverChartIndex].mouseIsDown) { + hoverChartIndex = chart.index; + } e = this.normalize(e); e.returnValue = false; // #2251, #3224 if (chart.mouseIsDown === 'mousedown') { @@ -10474,11 +10586,11 @@ onTrackerMouseOut: function (e) { var series = this.chart.hoverSeries, relatedTarget = e.relatedTarget || e.toElement; - if (series && !series.options.stickyTracking && + if (series && relatedTarget && !series.options.stickyTracking && // #4886 !this.inClass(relatedTarget, PREFIX + 'tooltip') && !this.inClass(relatedTarget, PREFIX + 'series-' + series.index)) { // #2499, #4465 series.onMouseOut(); } }, @@ -11180,19 +11292,19 @@ if (legend.setItemEvents) { legend.setItemEvents(item, li, useHTML, itemStyle, itemHiddenStyle); } - // Colorize the items - legend.colorizeItem(item, item.visible); - // add the HTML checkbox on top if (showCheckbox) { legend.createCheckboxForItem(item); } } + // Colorize the items + legend.colorizeItem(item, item.visible); + // Always update the text legend.setText(item); // calculate the positions for the next line bBox = li.getBBox(); @@ -13066,12 +13178,11 @@ /** * Prepare for first rendering after all data are loaded */ firstRender: function () { var chart = this, - options = chart.options, - callback = chart.callback; + options = chart.options; // Check whether the chart is ready to render if (!chart.isReadyToRender()) { return; } @@ -13111,28 +13222,40 @@ chart.render(); // add canvas chart.renderer.draw(); - // run callbacks - if (callback) { - callback.apply(chart, [chart]); + + // Fire the load event if there are no external images + if (!chart.renderer.imgCount) { + chart.onload(); } - each(chart.callbacks, function (fn) { - if (chart.index !== UNDEFINED) { // Chart destroyed in its own callback (#3600) - fn.apply(chart, [chart]); - } - }); - // Fire the load event - fireEvent(chart, 'load'); - // If the chart was rendered outside the top container, put it back in (#3679) chart.cloneRenderTo(true); }, + /** + * On chart load + */ + onload: function () { + var chart = this; + + // Run callbacks + each([this.callback].concat(this.callbacks), function (fn) { + if (fn && chart.index !== undefined) { // Chart destroyed in its own callback (#3600) + fn.apply(chart, [chart]); + } + }); + + // Fire the load event if there are no external images + if (!chart.renderer.imgCount) { + fireEvent(chart, 'load'); + } + }, + /** * Creates arrays for spacing and margin from given options. */ splashArray: function (target, options) { var oVar = options[target], @@ -13235,10 +13358,11 @@ // For higher dimension series types. For instance, for ranges, point.y is mapped to point.low. if (pointValKey) { point.y = point[pointValKey]; } + point.isNull = point.y === null; // If no x is set by now, get auto incremented value. All points must have an // x value, however the y value can be null to create a gap in the series if (point.x === UNDEFINED && series) { point.x = x === UNDEFINED ? series.autoIncrement() : x; @@ -13630,56 +13754,12 @@ } this.xIncrement = xIncrement + pointInterval; return xIncrement; }, - + /** - * Divide the series data into segments divided by null values. - */ - getSegments: function () { - var series = this, - lastNull = -1, - segments = [], - i, - points = series.points, - pointsLength = points.length; - - if (pointsLength) { // no action required for [] - - // if connect nulls, just remove null points - if (series.options.connectNulls) { - i = pointsLength; - while (i--) { - if (points[i].y === null) { - points.splice(i, 1); - } - } - if (points.length) { - segments = [points]; - } - - // else, split on null points - } else { - each(points, function (point, i) { - if (point.y === null) { - if (i > lastNull + 1) { - segments.push(points.slice(lastNull + 1, i)); - } - lastNull = i; - } else if (i === pointsLength - 1) { // last value - segments.push(points.slice(lastNull + 1, i + 1)); - } - }); - } - } - - // register it - series.segments = segments; - }, - - /** * Set the series options by merging from the options tree * @param {Object} itemOptions */ setOptions: function (itemOptions) { var chart = this.chart, @@ -13890,12 +13970,11 @@ if (isString(yData[0])) { error(14, true); } series.data = []; - series.options.data = data; - //series.zData = zData; + series.options.data = series.userOptions.data = data; // destroy old points i = oldDataLength; while (i--) { if (oldData[i] && oldData[i].destroy) { @@ -13913,11 +13992,11 @@ animation = false; } // Typically for pie series, points need to be processed and generated // prior to rendering the legend - if (options.legendType === 'point') { // docs: legendType now supported on more series types (at least column and pie) + if (options.legendType === 'point') { this.processData(); this.generatePoints(); } if (redraw) { @@ -13944,10 +14023,12 @@ options = series.options, cropThreshold = options.cropThreshold, getExtremesFromAll = series.getExtremesFromAll || options.getExtremesFromAll, // #4599 isCartesian = series.isCartesian, xExtremes, + val2lin = xAxis && xAxis.val2lin, + isLog = xAxis && xAxis.isLog, min, max; // 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. @@ -13979,12 +14060,15 @@ } } // Find the closest distance between processed points - for (i = processedXData.length - 1; i >= 0; i--) { - distance = processedXData[i] - processedXData[i - 1]; + i = processedXData.length || 1; + while (--i) { + distance = isLog ? + val2lin(processedXData[i]) - val2lin(processedXData[i - 1]) : + processedXData[i] - processedXData[i - 1]; if (distance > 0 && (closestPointRange === UNDEFINED || distance < closestPointRange)) { closestPointRange = distance; // Unsorted data is not supported by the line tooltip, as well as data grouping and @@ -14202,11 +14286,11 @@ // Get the plotX translation point.plotX = plotX = mathMin(mathMax(-1e5, xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement, this.type === 'flags')), 1e5); // #3923 // Calculate the bottom y value for stacked series - if (stacking && series.visible && stack && stack[xValue]) { + if (stacking && series.visible && !point.isNull && stack && stack[xValue]) { stackIndicator = series.getStackIndicator(stackIndicator, xValue, series.index); pointStack = stack[xValue]; stackValues = pointStack.points[stackIndicator.key]; yBottom = stackValues[0]; yValue = stackValues[1]; @@ -14259,15 +14343,20 @@ closestPointRangePx = mathMin(closestPointRangePx, mathAbs(plotX - lastPlotX)); } lastPlotX = plotX; } - series.closestPointRangePx = closestPointRangePx; + }, - // now that we have the cropped data, build the segments - series.getSegments(); + /** + * Return the series points with null points filtered out + */ + getValidPoints: function () { + return grep(this.points, function (point) { + return !point.isNull; + }); }, /** * Set the clipping for the series. For animated series it is called twice, first to initiate * animating the clip then the second time without the animation to set the final clip. @@ -14426,10 +14515,11 @@ symbol = pick(pointMarkerOptions.symbol, series.symbol); isImage = symbol.indexOf('url') === 0; if (graphic) { // update graphic[isInside ? 'show' : 'hide'](true) // Since the marker group isn't clipped, each individual marker must be toggled + .attr(pointAttr) // #4759 .animate(extend({ x: plotX - radius, y: plotY - radius }, graphic.symbolName ? { // don't apply to image symbols #507 width: 2 * radius, @@ -14713,96 +14803,93 @@ delete series[prop]; } }, /** - * Return the graph path of a segment + * Get the graph path */ - getSegmentPath: function (segment) { + getGraphPath: function (points, nullsAsZeroes, connectCliffs) { var series = this, - segmentPath = [], - step = series.options.step; + options = series.options, + step = options.step, + graphPath = [], + gap; - // build the segment line - each(segment, function (point, i) { + points = points || series.points; + // Build the line + each(points, function (point, i) { + var plotX = point.plotX, plotY = point.plotY, - lastPoint; + lastPoint = points[i - 1], + pathToPoint; // the path to this point from the previous - if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object - segmentPath.push.apply(segmentPath, series.getPointSpline(segment, point, i)); + if ((point.leftCliff || (lastPoint && lastPoint.rightCliff)) && !connectCliffs) { + gap = true; // ... and continue + } + // Line series, nullsAsZeroes is not handled + if (point.isNull && !defined(nullsAsZeroes) && i > 0) { + gap = !options.connectNulls; + + // Area series, nullsAsZeroes is set + } else if (point.isNull && !nullsAsZeroes) { + gap = true; + } else { - // moveTo or lineTo - segmentPath.push(i ? L : M); + if (i === 0 || gap) { + pathToPoint = [M, point.plotX, point.plotY]; + + } else if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object + + pathToPoint = series.getPointSpline(points, point, i); - // step line? - if (step && i) { - lastPoint = segment[i - 1]; + } else if (step) { + if (step === 'right') { - segmentPath.push( + pathToPoint = [ + L, lastPoint.plotX, - plotY, - L - ); - + plotY + ]; + } else if (step === 'center') { - segmentPath.push( + pathToPoint = [ + L, (lastPoint.plotX + plotX) / 2, lastPoint.plotY, L, (lastPoint.plotX + plotX) / 2, - plotY, - L - ); - + plotY + ]; + } else { - segmentPath.push( + pathToPoint = [ + L, plotX, - lastPoint.plotY, - L - ); + lastPoint.plotY + ]; } + pathToPoint.push(L, plotX, plotY); + + } else { + // normal line to next point + pathToPoint = [ + L, + plotX, + plotY + ]; } - // normal line to next point - segmentPath.push( - point.plotX, - point.plotY - ); - } - }); - return segmentPath; - }, - - /** - * Get the graph path - */ - getGraphPath: function () { - var series = this, - graphPath = [], - segmentPath, - singlePoints = []; // used in drawTracker - - // Divide into segments and build graph and area paths - each(series.segments, function (segment) { - - segmentPath = series.getSegmentPath(segment); - - // add the segment to the graph, or a single point for tracking - if (segment.length > 1) { - graphPath = graphPath.concat(segmentPath); - } else { - singlePoints.push(segment[0]); + graphPath.push.apply(graphPath, pathToPoint); + gap = false; } }); - // Record it for use in drawGraph and drawTracker, and return graphPath - series.singlePoints = singlePoints; series.graphPath = graphPath; return graphPath; }, @@ -14814,11 +14901,11 @@ var series = this, options = this.options, props = [['graph', options.lineColor || this.color, options.dashStyle]], lineWidth = options.lineWidth, roundCap = options.linecap !== 'square', - graphPath = this.getGraphPath(), + graphPath = (this.gappedPath || this.getGraphPath).call(this), fillColor = (this.fillGraph && this.color) || NONE, // polygon series use filled graph zones = this.zones; each(zones, function (threshold, i) { props.push(['zoneGraph' + i, threshold.color || series.color, threshold.dashStyle || options.dashStyle]); @@ -15337,10 +15424,12 @@ // This will keep each points' extremes stored by series.index and point index this.points = {}; // Save the stack option on the series configuration object, and whether to treat it as percent this.stack = stackOption; + this.leftCliff = 0; + this.rightCliff = 0; // The align options and text align varies on whether the stack is negative and // if the chart is inverted or not. // First test the user supplied value, then use the dynamic. this.alignOptions = { @@ -15444,22 +15533,33 @@ /** * Build the stacks from top down */ Axis.prototype.buildStacks = function () { - var series = this.series, + var axisSeries = this.series, + series, reversedStacks = pick(this.options.reversedStacks, true), - i = series.length; + len = axisSeries.length, + i; if (!this.isXAxis) { this.usePercentage = false; + i = len; while (i--) { - series[reversedStacks ? i : series.length - i - 1].setStackedPoints(); + axisSeries[reversedStacks ? i : len - i - 1].setStackedPoints(); } + + i = len; + while (i--) { + series = axisSeries[reversedStacks ? i : len - i - 1]; + if (series.setStackCliffs) { + series.setStackCliffs(); + } + } // Loop up again to compute percent stack if (this.usePercentage) { - for (i = 0; i < series.length; i++) { - series[i].setPercentStacks(); + for (i = 0; i < len; i++) { + axisSeries[i].setPercentStacks(); } } } }; @@ -15606,17 +15706,20 @@ } } // If the StackItem doesn't exist, create it first stack = stacks[key][x]; - stack.points[pointKey] = [pick(stack.cum, stackThreshold)]; - stack.touched = yAxis.stacksTouched; + if (y !== null) { + stack.points[pointKey] = stack.points[series.index] = [pick(stack.cum, stackThreshold)]; + stack.touched = yAxis.stacksTouched; + - // In area charts, if there are multiple points on the same X value, let the - // area fill the full span of those points - if (stackIndicator.index > 0 && series.singleStacks === false) { - stack.points[pointKey][0] = stack.points[series.index + ',' + x + ',0'][0]; + // In area charts, if there are multiple points on the same X value, let the + // area fill the full span of those points + if (stackIndicator.index > 0 && series.singleStacks === false) { + stack.points[pointKey][0] = stack.points[series.index + ',' + x + ',0'][0]; + } } // Add value to the stack total if (stacking === 'percent') { @@ -15634,11 +15737,13 @@ stack.total = correctFloat(stack.total + (y || 0)); } stack.cum = pick(stack.cum, stackThreshold) + (y || 0); - stack.points[pointKey].push(stack.cum); + if (y !== null) { + stack.points[pointKey].push(stack.cum); + } stackedYData[i] = stack.cum; } if (stacking === 'percent') { @@ -16038,11 +16143,11 @@ point = data[i], points = series.points, chart = series.chart, remove = function () { - if (data.length === points.length) { + if (points && points.length === data.length) { // #4935 points.splice(i, 1); } data.splice(i, 1); series.options.data.splice(i, 1); series.updateParallelArrays(point || { series: series }, 'splice', i, 1); @@ -16244,33 +16349,32 @@ * AreaSeries object */ var AreaSeries = extendClass(Series, { type: 'area', singleStacks: false, - /** - * For stacks, don't split segments on null values. Instead, draw null values with - * no marker. Also insert dummy points for any X position that exists in other series - * in the stack. + /** + * Return an array of stacked points, where null and missing points are replaced by + * dummy points in order for gaps to be drawn correctly in stacks. */ - getSegments: function () { + getStackPoints: function () { var series = this, - segments = [], segment = [], keys = [], xAxis = this.xAxis, yAxis = this.yAxis, stack = yAxis.stacks[this.stackKey], pointMap = {}, - plotX, - plotY, points = this.points, - connectNulls = this.options.connectNulls, - stackIndicator, + seriesIndex = series.index, + yAxisSeries = yAxis.series, + seriesLength = yAxisSeries.length, + visibleSeries, + upOrDown = pick(yAxis.options.reversedStacks, true) ? 1 : -1, i, x; - if (this.options.stacking && !this.cropped) { // cropped causes artefacts in Stock, and perf issue + if (this.options.stacking) { // Create a map where we can quickly look up the points by their X value. for (i = 0; i < points.length; i++) { pointMap[points[i].x] = points[i]; } @@ -16282,117 +16386,196 @@ } keys.sort(function (a, b) { return a - b; }); - each(keys, function (x) { - var threshold = null, + visibleSeries = map(yAxisSeries, function () { + return this.visible; + }); + + each(keys, function (x, idx) { + var y = 0, stackPoint, - skip = connectNulls && (!pointMap[x] || pointMap[x].y === null); // #1836 + stackedValues; - if (!skip) { + if (pointMap[x] && !pointMap[x].isNull) { + segment.push(pointMap[x]); - // The point exists, push it to the segment - if (pointMap[x]) { - segment.push(pointMap[x]); + // Find left and right cliff. -1 goes left, 1 goes right. + each([-1, 1], function (direction) { + var nullName = direction === 1 ? 'rightNull' : 'leftNull', + cliffName = direction === 1 ? 'rightCliff' : 'leftCliff', + cliff = 0, + otherStack = stack[keys[idx + direction]]; - // There is no point for this X value in this series, so we - // insert a dummy point in order for the areas to be drawn - // correctly. - } else { + // If there is a stack next to this one, to the left or to the right... + if (otherStack) { + i = seriesIndex; + while (i >= 0 && i < seriesLength) { // Can go either up or down, depending on reversedStacks + stackPoint = otherStack.points[i]; + if (!stackPoint) { + // If the next point in this series is missing, mark the point + // with point.leftNull or point.rightNull = true. + if (i === seriesIndex) { + pointMap[x][nullName] = true; - // Loop down the stack to find the series below this one that has - // a value (#1991) - for (i = series.index; i <= yAxis.series.length; i++) { - stackIndicator = series.getStackIndicator(null, x, i); - stackPoint = stack[x].points[stackIndicator.key]; - if (stackPoint) { - threshold = stackPoint[1]; - break; - } + // If there are missing points in the next stack in any of the + // series below this one, we need to substract the missing values + // and add a hiatus to the left or right. + } else if (visibleSeries[i]) { + stackedValues = stack[x].points[i]; + if (stackedValues) { + cliff -= stackedValues[1] - stackedValues[0]; + } + } + } + // When reversedStacks is true, loop up, else loop down + i += upOrDown; + } } + pointMap[x][cliffName] = cliff; + }); - plotX = xAxis.translate(x); - plotY = yAxis.getThreshold(threshold); - segment.push({ - y: null, - plotX: plotX, - clientX: plotX, - plotY: plotY, - yBottom: plotY, - onMouseOver: noop - }); + + // 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) + i = seriesIndex; + while (i >= 0 && i < seriesLength) { + stackPoint = stack[x].points[i]; + if (stackPoint) { + y = stackPoint[1]; + break; + } + // When reversedStacks is true, loop up, else loop down + i += upOrDown; } + + y = yAxis.toPixels(y, true); + segment.push({ + isNull: true, + plotX: xAxis.toPixels(x, true), + plotY: y, + yBottom: y + }); } }); - if (segment.length) { - segments.push(segment); - } + } - } else { - Series.prototype.getSegments.call(this); - segments = this.segments; - } - - this.segments = segments; + return segment; }, - /** - * Extend the base Series getSegmentPath method by adding the path for the area. - * This path is pushed to the series.areaPath property. - */ - getSegmentPath: function (segment) { - - var segmentPath = Series.prototype.getSegmentPath.call(this, segment), // call base method - areaSegmentPath = [].concat(segmentPath), // work on a copy for the area path - i, + getGraphPath: function (points) { + var getGraphPath = Series.prototype.getGraphPath, + graphPath, options = this.options, - segLength = segmentPath.length, - translatedThreshold = this.yAxis.getThreshold(options.threshold), // #2181 - yBottom; + stacking = options.stacking, + yAxis = this.yAxis, + topPath, + //topPoints = [], + bottomPath, + bottomPoints = [], + graphPoints = [], + seriesIndex = this.index, + i, + areaPath, + plotX, + stacks = yAxis.stacks[this.stackKey], + threshold = options.threshold, + translatedThreshold = yAxis.getThreshold(options.threshold), + isNull, + yBottom, + connectNulls = options.connectNulls || stacking === 'percent', + /** + * To display null points in underlying stacked series, this series graph must be + * broken, and the area also fall down to fill the gap left by the null point. #2069 + */ + addDummyPoints = function (i, otherI, side) { + var point = points[i], + stackedValues = stacking && stacks[point.x].points[seriesIndex], + nullVal = point[side + 'Null'] || 0, + cliffVal = point[side + 'Cliff'] || 0, + top, + bottom, + isNull = true; - if (segLength === 3) { // for animation from 1 to two points - areaSegmentPath.push(L, segmentPath[1], segmentPath[2]); + if (cliffVal || nullVal) { + + top = (nullVal ? stackedValues[0] : stackedValues[1]) + cliffVal; + bottom = stackedValues[0] + cliffVal; + isNull = !!nullVal; + + } else if (!stacking && points[otherI] && points[otherI].isNull) { + top = bottom = threshold; + } + + // Add to the top and bottom line of the area + if (top !== undefined) { + graphPoints.push({ + plotX: plotX, + plotY: top === null ? translatedThreshold : yAxis.toPixels(top, true), + isNull: isNull + }); + bottomPoints.push({ + plotX: plotX, + plotY: bottom === null ? translatedThreshold : yAxis.toPixels(bottom, true) + }); + } + }; + + // Find what points to use + points = points || this.points; + + + // Fill in missing points + if (stacking) { + points = this.getStackPoints(); } - if (options.stacking && !this.closedStacks) { - // Follow stack back. Later, implement areaspline. A general solution could be to - // reverse the entire graphPath of the previous series, though may be hard with - // splines and with series with different extremes - for (i = segment.length - 1; i >= 0; i--) { + for (i = 0; i < points.length; i++) { + isNull = points[i].isNull; + plotX = pick(points[i].rectPlotX, points[i].plotX); + yBottom = pick(points[i].yBottom, translatedThreshold); - yBottom = pick(segment[i].yBottom, translatedThreshold); + if (!isNull || connectNulls) { - // step line? - if (i < segment.length - 1 && options.step) { - areaSegmentPath.push(segment[i + 1].plotX, yBottom); + if (!connectNulls) { + addDummyPoints(i, i - 1, 'left'); } - areaSegmentPath.push(segment[i].plotX, yBottom); + if (!(isNull && !stacking && connectNulls)) { // Skip null point when stacking is false and connectNulls true + graphPoints.push(points[i]); + bottomPoints.push({ + x: i, + plotX: plotX, + plotY: yBottom + }); + } + + if (!connectNulls) { + addDummyPoints(i, i + 1, 'right'); + } } + } - } else { // follow zero line back - this.closeSegment(areaSegmentPath, segment, translatedThreshold); + topPath = getGraphPath.call(this, graphPoints, true, true); + + bottomPath = getGraphPath.call(this, bottomPoints.reverse(), true, true); + if (bottomPath.length) { + bottomPath[0] = L; } - this.areaPath = this.areaPath.concat(areaSegmentPath); - return segmentPath; - }, - /** - * Extendable method to close the segment path of an area. This is overridden in polar - * charts. - */ - closeSegment: function (path, segment, translatedThreshold) { - path.push( - L, - segment[segment.length - 1].plotX, - translatedThreshold, - L, - segment[0].plotX, - translatedThreshold - ); + areaPath = topPath.concat(bottomPath); + graphPath = getGraphPath.call(this, graphPoints, false, connectNulls); // TODO: don't set leftCliff and rightCliff when connectNulls? + + this.areaPath = areaPath; + return graphPath; }, /** * Draw the graph and the underlying area. This method calls the Series base * function and adds the area. The areaPath is calculated in the getSegmentPath @@ -16429,11 +16612,11 @@ attr = { fill: prop[2] || prop[1], zIndex: 0 // #1069 }; if (!prop[2]) { - attr['fill-opacity'] = options.fillOpacity || 0.75; + attr['fill-opacity'] = pick(options.fillOpacity, 0.75); } series[areaKey] = series.chart.renderer.path(areaPath) .attr(attr) .add(series.group); } @@ -16456,26 +16639,25 @@ type: 'spline', /** * Get the spline segment from a given point's previous neighbour to the given point */ - getPointSpline: function (segment, point, i) { + getPointSpline: function (points, point, i) { var smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc denom = smoothing + 1, plotX = point.plotX, plotY = point.plotY, - lastPoint = segment[i - 1], - nextPoint = segment[i + 1], + lastPoint = points[i - 1], + nextPoint = points[i + 1], leftContX, leftContY, rightContX, rightContY, ret; - // find control points - if (lastPoint && nextPoint) { - + // Find control points + if (lastPoint && !lastPoint.isNull && nextPoint && !nextPoint.isNull) { var lastX = lastPoint.plotX, lastY = lastPoint.plotY, nextX = nextPoint.plotX, nextY = nextPoint.plotY, correction; @@ -16511,10 +16693,11 @@ // record for drawing in next point point.rightContX = rightContX; point.rightContY = rightContY; + } // Visualize control points for debugging /* if (leftContX) { @@ -16545,27 +16728,21 @@ stroke: 'green', 'stroke-width': 1 }) .add(); } - */ - - // moveTo or lineTo - if (!i) { - ret = [M, plotX, plotY]; - } else { // curve from last point to this - ret = [ - 'C', - lastPoint.rightContX || lastPoint.plotX, - lastPoint.rightContY || lastPoint.plotY, - leftContX || plotX, - leftContY || plotY, - plotX, - plotY - ]; - lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later - } + // */ + ret = [ + 'C', + pick(lastPoint.rightContX, lastPoint.plotX), + pick(lastPoint.rightContY, lastPoint.plotY), + pick(leftContX, plotX), + pick(leftContY, plotY), + plotX, + plotY + ]; + lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later return ret; } }); seriesTypes.spline = SplineSeries; @@ -16578,15 +16755,13 @@ * AreaSplineSeries object */ var areaProto = AreaSeries.prototype, AreaSplineSeries = extendClass(SplineSeries, { type: 'areaspline', - closedStacks: true, // instead of following the previous graph back, follow the threshold back - - // Mix in methods from the area series - getSegmentPath: areaProto.getSegmentPath, - closeSegment: areaProto.closeSegment, + getStackPoints: areaProto.getStackPoints, + getGraphPath: areaProto.getGraphPath, + setStackCliffs: areaProto.setStackCliffs, drawGraph: areaProto.drawGraph, drawLegendSymbol: LegendSymbolMixin.drawRectangle }); seriesTypes.areaspline = AreaSplineSeries; @@ -17648,15 +17823,20 @@ inverted = chart.inverted, plotX = pick(point.plotX, -9999), plotY = pick(point.plotY, -9999), bBox = dataLabel.getBBox(), baseline = chart.renderer.fontMetrics(options.style.fontSize).b, + rotation = options.rotation, + normRotation, + negRotation, + align = options.align, rotCorr, // rotation correction // Math.round for rounding errors (#2683), alignTo to allow column labels (#2700) visible = this.visible && (point.series.forceDL || chart.isInsidePlot(plotX, mathRound(plotY), inverted) || (alignTo && chart.isInsidePlot(plotX, inverted ? alignTo.x + 1 : alignTo.y + alignTo.height - 1, inverted))), - alignAttr; // the final position; + alignAttr, // the final position; + justify = pick(options.overflow, 'justify') === 'justify'; if (visible) { // The alignment box is a singular point alignTo = extend({ @@ -17671,42 +17851,59 @@ 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 - rotCorr = chart.renderer.rotCorr(baseline, options.rotation); // #3723 - dataLabel[isNew ? 'attr' : 'animate']({ - x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x, - y: alignTo.y + options.y + alignTo.height / 2 - }) + if (rotation) { + justify = false; // Not supported for rotated text + rotCorr = chart.renderer.rotCorr(baseline, rotation); // #3723 + alignAttr = { + x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x, + y: alignTo.y + options.y + alignTo.height / 2 + }; + dataLabel + [isNew ? 'attr' : 'animate'](alignAttr) .attr({ // #3003 align: options.align }); - } else { - dataLabel.align(options, null, alignTo); - alignAttr = dataLabel.alignAttr; - // Handle justify or crop - if (pick(options.overflow, 'justify') === 'justify') { - this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew); + // Compensate for the rotated label sticking out on the sides + normRotation = (rotation + 720) % 360; + negRotation = normRotation > 180 && normRotation < 360; - } else if (pick(options.crop, true)) { - // Now check that the data label is within the plot area - visible = chart.isInsidePlot(alignAttr.x, alignAttr.y) && chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height); - + if (align === 'left') { + alignAttr.y -= negRotation ? bBox.height : 0; + } else if (align === 'center') { + alignAttr.x -= bBox.width / 2; + alignAttr.y -= bBox.height / 2; + } else if (align === 'right') { + alignAttr.x -= bBox.width; + alignAttr.y -= negRotation ? 0 : bBox.height; } + - // When we're using a shape, make it possible with a connector or an arrow pointing to thie point - if (options.shape) { - dataLabel.attr({ - anchorX: point.plotX, - anchorY: point.plotY - }); - } + } else { + dataLabel.align(options, null, alignTo); + alignAttr = dataLabel.alignAttr; + } + // Handle justify or crop + if (justify) { + this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew); + + // Now check that the data label is within the plot area + } else if (pick(options.crop, true)) { + visible = chart.isInsidePlot(alignAttr.x, alignAttr.y) && chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height); } + + // When we're using a shape, make it possible with a connector or an arrow pointing to thie point + if (options.shape && !rotation) { + dataLabel.attr({ + anchorX: point.plotX, + anchorY: point.plotY + }); + } } // Show or hide based on the final aligned position if (!visible) { stop(dataLabel); @@ -17825,14 +18022,18 @@ } // run parent method Series.prototype.drawDataLabels.apply(series); - // arrange points for detection collision each(data, function (point) { if (point.dataLabel && point.visible) { // #407, #2510 + + // Arrange points for detection collision halves[point.half].push(point); + + // Reset positions (#4905) + point.dataLabel._pos = null; } }); /* Loop over the points in each half, starting from the top and bottom * of the pie to detect overlapping labels. @@ -18163,16 +18364,11 @@ // If the size must be decreased, we need to run translate and drawDataLabels again if (newSize < center[2]) { center[2] = newSize; center[3] = Math.min(relativeLength(options.innerSize || 0, newSize), newSize); // #3632 this.translate(center); - each(this.points, function (point) { - if (point.dataLabel) { - point.dataLabel._pos = null; // reset - } - }); - + if (this.drawDataLabels) { this.drawDataLabels(); } // Else, return true to indicate that the pie and its labels is within the plot area } else { @@ -18301,10 +18497,12 @@ label1, label2, isIntersecting, pos1, pos2, + parent1, + parent2, padding, intersectRect = function (x1, y1, w1, h1, x2, y2, w2, h2) { return !( x2 > x1 + w1 || x2 + w2 < x1 || @@ -18336,18 +18534,20 @@ for (j = i + 1; j < len; ++j) { label2 = labels[j]; if (label1 && label2 && label1.placed && label2.placed && label1.newOpacity !== 0 && label2.newOpacity !== 0) { pos1 = label1.alignAttr; pos2 = label2.alignAttr; + parent1 = label1.parentGroup; // Different panes have different positions + parent2 = label2.parentGroup; padding = 2 * (label1.box ? 0 : label1.padding); // Substract the padding if no background or border (#4333) isIntersecting = intersectRect( - pos1.x, - pos1.y, + pos1.x + parent1.translateX, + pos1.y + parent1.translateY, label1.width - padding, label1.height - padding, - pos2.x, - pos2.y, + pos2.x + parent2.translateX, + pos2.y + parent2.translateY, label2.width - padding, label2.height - padding ); if (isIntersecting) { @@ -18459,12 +18659,10 @@ renderer = chart.renderer, snap = chart.options.tooltip.snap, tracker = series.tracker, cursor = options.cursor, css = cursor && { cursor: cursor }, - singlePoints = series.singlePoints, - singlePoint, i, onMouseOver = function () { if (chart.hoverSeries !== series) { series.onMouseOver(); } @@ -18496,15 +18694,15 @@ } } } // handle single points - for (i = 0; i < singlePoints.length; i++) { + /*for (i = 0; i < singlePoints.length; i++) { singlePoint = singlePoints[i]; trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY, L, singlePoint.plotX + snap, singlePoint.plotY); - } + }*/ // draw the tracker if (tracker) { tracker.attr({ d: trackerPath }); } else { // create @@ -18732,27 +18930,29 @@ point.setState(); }); } each(panning === 'xy' ? [1, 0] : [1], function (isX) { // xy is used in maps - var mousePos = e[isX ? 'chartX' : 'chartY'], - axis = chart[isX ? 'xAxis' : 'yAxis'][0], - startPos = chart[isX ? 'mouseDownX' : 'mouseDownY'], + var axis = chart[isX ? 'xAxis' : 'yAxis'][0], + horiz = axis.horiz, + mousePos = e[horiz ? 'chartX' : 'chartY'], + mouseDown = horiz ? 'mouseDownX' : 'mouseDownY', + startPos = chart[mouseDown], halfPointRange = (axis.pointRange || 0) / 2, extremes = axis.getExtremes(), newMin = axis.toValue(startPos - mousePos, true) + halfPointRange, - newMax = axis.toValue(startPos + chart[isX ? 'plotWidth' : 'plotHeight'] - mousePos, true) - halfPointRange, + newMax = axis.toValue(startPos + axis.len - mousePos, true) - halfPointRange, goingLeft = startPos > mousePos; // #3613 if (axis.series.length && (goingLeft || newMin > mathMin(extremes.dataMin, extremes.min)) && (!goingLeft || newMax < mathMax(extremes.dataMax, extremes.max))) { axis.setExtremes(newMin, newMax, false, false, { trigger: 'pan' }); doRedraw = true; } - chart[isX ? 'mouseDownX' : 'mouseDownY'] = mousePos; // set new reference for next run + chart[mouseDown] = mousePos; // set new reference for next run }); if (doRedraw) { chart.redraw(false); } @@ -18983,11 +19183,12 @@ if (!halo) { series.halo = halo = chart.renderer.path() .add(chart.seriesGroup); } halo.attr(extend({ - fill: point.color || series.color, - 'fill-opacity': haloOptions.opacity + 'fill': point.color || series.color, + 'fill-opacity': haloOptions.opacity, + 'zIndex': -1 // #4929, IE8 added halo above everything }, haloOptions.attributes))[move ? 'animate' : 'attr']({ d: point.haloPath(haloOptions.size) }); } else if (halo) {