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) {