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