app/assets/javascripts/highcharts.js in highcharts-rails-3.0.9 vs app/assets/javascripts/highcharts.js in highcharts-rails-3.0.10
- old
+ new
@@ -1,10 +1,10 @@
// ==ClosureCompiler==
// @compilation_level SIMPLE_OPTIMIZATIONS
/**
- * @license Highcharts JS v3.0.9 (2014-01-15)
+ * @license Highcharts JS v3.0.10 (2014-03-10)
*
* (c) 2009-2014 Torstein Honsi
*
* License: www.highcharts.com/license
*/
@@ -41,11 +41,11 @@
SVG_NS = 'http://www.w3.org/2000/svg',
hasSVG = !!doc.createElementNS && !!doc.createElementNS(SVG_NS, 'svg').createSVGRect,
hasBidiBug = isFirefox && parseInt(userAgent.split('Firefox/')[1], 10) < 4, // issue #38
useCanVG = !hasSVG && !isIE && !!doc.createElement('canvas').getContext,
Renderer,
- hasTouch = doc.documentElement.ontouchstart !== UNDEFINED,
+ hasTouch,
symbolSizes = {},
idCounter = 0,
garbageBin,
defaultOptions,
dateFormat, // function
@@ -53,11 +53,11 @@
pathAnim,
timeUnits,
noop = function () {},
charts = [],
PRODUCT = 'Highcharts',
- VERSION = '3.0.9',
+ VERSION = '3.0.10',
// some constants for frequently used strings
DIV = 'div',
ABSOLUTE = 'absolute',
RELATIVE = 'relative',
@@ -67,24 +67,10 @@
PX = 'px',
NONE = 'none',
M = 'M',
L = 'L',
numRegex = /^[0-9]+$/,
- /*
- * Empirical lowest possible opacities for TRACKER_FILL
- * IE6: 0.002
- * IE7: 0.002
- * IE8: 0.002
- * IE9: 0.00000000001 (unlimited)
- * IE10: 0.0001 (exporting only)
- * FF: 0.00000000001 (unlimited)
- * Chrome: 0.000001
- * Safari: 0.000001
- * Opera: 0.00000000001 (unlimited)
- */
- TRACKER_FILL = 'rgba(192,192,192,' + (hasSVG ? 0.0001 : 0.002) + ')', // invisible but clickable
- //TRACKER_FILL = 'rgba(192,192,192,0.5)',
NORMAL_STATE = '',
HOVER_STATE = 'hover',
SELECT_STATE = 'select',
MILLISECOND = 'millisecond',
SECOND = 'second',
@@ -97,12 +83,10 @@
// Object for extending Axis
AxisPlotLineOrBandExtension,
// constants for attributes
- LINEAR_GRADIENT = 'linearGradient',
- STOPS = 'stops',
STROKE_WIDTH = 'stroke-width',
// time methods, changed based on whether or not UTC is used
makeTime,
timezoneOffset,
@@ -121,11 +105,11 @@
// lookup over the types and the associated classes
seriesTypes = {};
// The Highcharts namespace
-win.Highcharts = win.Highcharts ? error(16, true) : {};
+var Highcharts = win.Highcharts = win.Highcharts ? error(16, true) : {};
/**
* Extend an object with the members of another
* @param {Object} a The object to be extended
* @param {Object} b The object to add to the first one
@@ -165,11 +149,11 @@
if (original.hasOwnProperty(key)) {
value = original[key];
// 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') {
+ && key !== 'renderTo' && typeof value.nodeType !== 'number') {
copy[key] = doCopy(copy[key] || {}, value);
// Primitives and arrays are copied over directly
} else {
copy[key] = original[key];
@@ -344,11 +328,11 @@
* Set CSS on a given element
* @param {Object} el
* @param {Object} styles Style object with camel case property names
*/
function css(el, styles) {
- if (isIE) {
+ if (isIE && !hasSVG) { // #2686
if (styles && styles.opacity !== UNDEFINED) {
styles.filter = 'alpha(opacity=' + (styles.opacity * 100) + ')';
}
}
extend(el.style, styles);
@@ -1308,10 +1292,11 @@
};
defaultOptions = {
colors: ['#2f7ed8', '#0d233a', '#8bbc21', '#910000', '#1aadce', '#492970',
'#f28f43', '#77a1e5', '#c42525', '#a6c96a'],
+ //colors: ['#8085e8', '#252530', '#90ee7e', '#8d4654', '#2b908f', '#76758e', '#f6a45c', '#7eb5ee', '#f45b5b', '#9ff0cf'],
symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'],
lang: {
loading: 'Loading...',
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July',
'August', 'September', 'October', 'November', 'December'],
@@ -1324,12 +1309,12 @@
thousandsSep: ','
},
global: {
useUTC: true,
//timezoneOffset: 0,
- canvasToolsURL: 'http://code.highcharts.com/3.0.9/modules/canvas-tools.js',
- VMLRadialGradientURL: 'http://code.highcharts.com/3.0.9/gfx/vml-radial-gradient.png'
+ canvasToolsURL: 'http://code.highcharts.com/3.0.10/modules/canvas-tools.js',
+ VMLRadialGradientURL: 'http://code.highcharts.com/3.0.10/gfx/vml-radial-gradient.png'
},
chart: {
//animation: true,
//alignTicks: false,
//reflow: true,
@@ -1350,14 +1335,14 @@
spacing: [10, 10, 15, 10],
//spacingTop: 10,
//spacingRight: 10,
//spacingBottom: 15,
//spacingLeft: 10,
- style: {
- fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font
- fontSize: '12px'
- },
+ //style: {
+ // fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font
+ // fontSize: '12px'
+ //},
backgroundColor: '#FFFFFF',
//plotBackgroundColor: null,
plotBorderColor: '#C0C0C0',
//plotBorderWidth: 0,
//plotShadow: false,
@@ -1517,12 +1502,11 @@
shadow: false,
// backgroundColor: null,
/*style: {
padding: '5px'
},*/
- itemStyle: {
- cursor: 'pointer',
+ itemStyle: {
color: '#274b6d',
fontSize: '12px'
},
itemHoverStyle: {
//cursor: 'pointer', removed as of #601
@@ -1851,11 +1835,11 @@
*/
animate: function (params, options, complete) {
var animOptions = pick(options, globalAnimation, true);
stop(this); // stop regardless of animation actually running, or reverting to .attr (#607)
if (animOptions) {
- animOptions = merge(animOptions);
+ animOptions = merge(animOptions, {}); //#2625
if (complete) { // allows using a callback with the global animation without overwriting it
animOptions.complete = complete;
}
animate(this, params, animOptions);
} else {
@@ -2073,20 +2057,25 @@
// Record for animation and quick access without polling the DOM
wrapper[key] = value;
if (key === 'text') {
- // Delete bBox memo when the text changes
if (value !== wrapper.textStr) {
+
+ // Delete bBox memo when the text changes
delete wrapper.bBox;
+
+ wrapper.textStr = value;
+ if (wrapper.added) {
+ renderer.buildText(wrapper);
+ }
}
- wrapper.textStr = value;
- if (wrapper.added) {
- renderer.buildText(wrapper);
- }
} else if (!skipAttr) {
- attr(element, key, value);
+ //attr(element, key, value);
+ if (value !== undefined) {
+ element.setAttribute(key, value);
+ }
}
}
}
@@ -2164,31 +2153,30 @@
* @param {Number} x
* @param {Number} y
* @param {Number} width
* @param {Number} height
*/
- crisp: function (strokeWidth, x, y, width, height) {
+ crisp: function (rect) {
var wrapper = this,
key,
attribs = {},
- values = {},
- normalizer;
+ normalizer,
+ strokeWidth = rect.strokeWidth || wrapper.strokeWidth || (wrapper.attr && wrapper.attr('stroke-width')) || 0;
- strokeWidth = strokeWidth || wrapper.strokeWidth || (wrapper.attr && wrapper.attr('stroke-width')) || 0;
normalizer = mathRound(strokeWidth) % 2 / 2; // mathRound because strokeWidth can sometimes have roundoff errors
// normalize for crisp edges
- values.x = mathFloor(x || wrapper.x || 0) + normalizer;
- values.y = mathFloor(y || wrapper.y || 0) + normalizer;
- values.width = mathFloor((width || wrapper.width || 0) - 2 * normalizer);
- values.height = mathFloor((height || wrapper.height || 0) - 2 * normalizer);
- values.strokeWidth = strokeWidth;
+ rect.x = mathFloor(rect.x || wrapper.x || 0) + normalizer;
+ rect.y = mathFloor(rect.y || wrapper.y || 0) + normalizer;
+ rect.width = mathFloor((rect.width || wrapper.width || 0) - 2 * normalizer);
+ rect.height = mathFloor((rect.height || wrapper.height || 0) - 2 * normalizer);
+ rect.strokeWidth = strokeWidth;
- for (key in values) {
- if (wrapper[key] !== values[key]) { // only set attribute if changed
- wrapper[key] = attribs[key] = values[key];
+ for (key in rect) {
+ if (wrapper[key] !== rect[key]) { // only set attribute if changed
+ wrapper[key] = attribs[key] = rect[key];
}
}
return attribs;
},
@@ -2196,51 +2184,70 @@
/**
* Set styles for the element
* @param {Object} styles
*/
css: function (styles) {
- /*jslint unparam: true*//* allow unused param a in the regexp function below */
var elemWrapper = this,
+ oldStyles = elemWrapper.styles,
+ newStyles = {},
elem = elemWrapper.element,
- textWidth = elemWrapper.textWidth = styles && styles.width && elem.nodeName.toLowerCase() === 'text' && pInt(styles.width),
+ textWidth,
n,
serializedCss = '',
- hyphenate = function (a, b) { return '-' + b.toLowerCase(); };
- /*jslint unparam: false*/
+ hyphenate,
+ hasNew = !oldStyles;
// convert legacy
if (styles && styles.color) {
styles.fill = styles.color;
}
- // Merge the new styles with the old ones
- styles = extend(
- elemWrapper.styles,
- styles
- );
+ // Filter out existing styles to increase performance (#2640)
+ if (oldStyles) {
+ for (n in styles) {
+ if (styles[n] !== oldStyles[n]) {
+ newStyles[n] = styles[n];
+ hasNew = true;
+ }
+ }
+ }
+ if (hasNew) {
+ textWidth = elemWrapper.textWidth = styles && styles.width && elem.nodeName.toLowerCase() === 'text' && pInt(styles.width);
- // store object
- elemWrapper.styles = styles;
+ // Merge the new styles with the old ones
+ if (oldStyles) {
+ styles = extend(
+ oldStyles,
+ newStyles
+ );
+ }
- if (textWidth) {
- delete styles.width;
- }
+ // store object
+ elemWrapper.styles = styles;
- // serialize and set style attribute
- if (isIE && !hasSVG) {
- css(elemWrapper.element, styles);
- } else {
- for (n in styles) {
- serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + styles[n] + ';';
+ if (textWidth && (useCanVG || (!hasSVG && elemWrapper.renderer.forExport))) {
+ delete styles.width;
}
- attr(elem, 'style', serializedCss); // #1881
- }
+ // serialize and set style attribute
+ if (isIE && !hasSVG) {
+ css(elemWrapper.element, styles);
+ } else {
+ /*jslint unparam: true*/
+ hyphenate = function (a, b) { return '-' + b.toLowerCase(); };
+ /*jslint unparam: false*/
+ for (n in styles) {
+ serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + styles[n] + ';';
+ }
+ attr(elem, 'style', serializedCss); // #1881
+ }
- // re-build text
- if (textWidth && elemWrapper.added) {
- elemWrapper.renderer.buildText(elemWrapper);
+
+ // re-build text
+ if (textWidth && elemWrapper.added) {
+ elemWrapper.renderer.buildText(elemWrapper);
+ }
}
return elemWrapper;
},
@@ -2444,11 +2451,11 @@
// Since numbers are monospaced, and numerical labels appear a lot in a chart,
// we assume that a label of n characters has the same bounding box as others
// of the same length.
if (textStr === '' || numRegex.test(textStr)) {
- numKey = textStr.length + '|' + styles.fontSize + '|' + styles.fontFamily;
+ numKey = textStr.toString().length + (styles ? ('|' + styles.fontSize + '|' + styles.fontFamily) : '');
bBox = renderer.cache[numKey];
}
// No cache found
if (!bBox) {
@@ -2510,12 +2517,12 @@
},
/**
* Show the element
*/
- show: function () {
- return this.attr({ visibility: VISIBLE });
+ show: function (inherit) {
+ return this.attr({ visibility: inherit ? 'inherit' : VISIBLE });
},
/**
* Hide the element
*/
@@ -2543,13 +2550,13 @@
add: function (parent) {
var renderer = this.renderer,
parentWrapper = parent || renderer,
parentNode = parentWrapper.element || renderer.box,
- childNodes = parentNode.childNodes,
+ childNodes,
element = this.element,
- zIndex = attr(element, 'zIndex'),
+ zIndex = this.zIndex,
otherElement,
otherZIndex,
i,
inserted;
@@ -2571,10 +2578,11 @@
zIndex = pInt(zIndex);
}
// insert according to this and other elements' zIndex
if (parentWrapper.handleZ) { // this element or any of its siblings has a z index
+ childNodes = parentNode.childNodes;
for (i = 0; i < childNodes.length; i++) {
otherElement = childNodes[i];
otherZIndex = attr(otherElement, 'zIndex');
if (otherElement !== element && (
// insert before the first element with a higher zIndex
@@ -2597,11 +2605,13 @@
// mark as added
this.added = true;
// fire an event for internal hooks
- fireEvent(this, 'add');
+ if (this.onAdd) {
+ this.onAdd();
+ }
return this;
},
/**
@@ -2743,21 +2753,22 @@
* @param {Object} container
* @param {Number} width
* @param {Number} height
* @param {Boolean} forExport
*/
- init: function (container, width, height, forExport) {
+ init: function (container, width, height, style, forExport) {
var renderer = this,
loc = location,
boxWrapper,
element,
desc;
boxWrapper = renderer.createElement('svg')
.attr({
version: '1.1'
- });
+ })
+ .css(this.getStyle(style));
element = boxWrapper.element;
container.appendChild(element);
// For browsers other than IE, add the namespace attribute (#1978)
if (container.innerHTML.indexOf('xmlns') === -1) {
@@ -2815,10 +2826,17 @@
// run it on resize
addEvent(win, 'resize', subPixelFix);
}
},
+ getStyle: function (style) {
+ return (this.style = extend({
+ fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font
+ fontSize: '12px'
+ }, style));
+ },
+
/**
* Detect whether the renderer is hidden. This happens when one of the parent elements
* has display: none. #608.
*/
isHidden: function () {
@@ -2885,22 +2903,22 @@
.replace(/<(i|em)>/g, '<span style="font-style:italic">')
.replace(/<a/g, '<span')
.replace(/<\/(b|strong|i|em|a)>/g, '</span>')
.split(/<br.*?>/g),
childNodes = textNode.childNodes,
- styleRegex = /style="([^"]+)"/,
- hrefRegex = /href="(http[^"]+)"/,
+ styleRegex = /<.*style="([^"]+)".*>/,
+ hrefRegex = /<.*href="(http[^"]+)".*>/,
parentX = attr(textNode, 'x'),
textStyles = wrapper.styles,
width = wrapper.textWidth,
textLineHeight = textStyles && textStyles.lineHeight,
i = childNodes.length,
getLineHeight = function (tspan) {
return textLineHeight ?
pInt(textLineHeight) :
renderer.fontMetrics(
- /px$/.test(tspan && tspan.style.fontSize) ?
+ /(px|em)$/.test(tspan && tspan.style.fontSize) ?
tspan.style.fontSize :
(textStyles.fontSize || 11)
).h;
};
@@ -3262,21 +3280,28 @@
*/
rect: function (x, y, width, height, r, strokeWidth) {
r = isObject(x) ? x.r : r;
- var wrapper = this.createElement('rect').attr({
- rx: r,
- ry: r,
- fill: NONE
- });
- return wrapper.attr(
- isObject(x) ?
- x :
- // do not crispify when an object is passed in (as in column charts)
- wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0))
- );
+ var wrapper = this.createElement('rect'),
+ attr = isObject(x) ? x : x === UNDEFINED ? {} : {
+ x: x,
+ y: y,
+ width: mathMax(width, 0),
+ height: mathMax(height, 0)
+ };
+
+ if (strokeWidth !== UNDEFINED) {
+ attr.strokeWidth = strokeWidth;
+ attr = wrapper.crisp(attr);
+ }
+
+ if (r) {
+ attr.r = r;
+ }
+
+ return wrapper.attr(attr);
},
/**
* Resize the box and re-align all aligned elements
* @param {Object} width
@@ -3702,11 +3727,10 @@
*/
text: function (str, x, y, useHTML) {
// declare variables
var renderer = this,
- defaultChartStyle = defaultOptions.chart.style,
fakeSVG = useCanVG || (!hasSVG && renderer.forExport),
wrapper;
if (useHTML && !renderer.forExport) {
return renderer.html(str, x, y);
@@ -3718,14 +3742,10 @@
wrapper = renderer.createElement('text')
.attr({
x: x,
y: y,
text: str
- })
- .css({
- fontFamily: defaultChartStyle.fontFamily,
- fontSize: defaultChartStyle.fontSize
});
// Prevent wrapping from creating false offsetWidths in export in legacy IE (#1079, #1063)
if (fakeSVG) {
wrapper.css({
@@ -3740,11 +3760,12 @@
/**
* Utility to return the baseline offset and total line height from the font size
*/
fontMetrics: function (fontSize) {
- fontSize = pInt(fontSize || 11);
+ fontSize = fontSize || this.style.fontSize;
+ fontSize = /px/.test(fontSize) ? pInt(fontSize) : /em/.test(fontSize) ? parseFloat(fontSize) * 12 : 12;
// Empirical values found by comparing font size and bounding box height.
// Applies to the default font family. http://jsfiddle.net/highcharts/7xvn7/
var lineHeight = fontSize < 24 ? fontSize + 4 : mathRound(fontSize * 1.2),
baseline = mathRound(lineHeight * 0.8);
@@ -3801,11 +3822,11 @@
function updateBoxSize() {
var boxX,
boxY,
style = text.element.style;
- bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) &&
+ bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) && text.textStr &&
text.getBBox();
wrapper.width = (width || bBox.width || 0) + 2 * padding + paddingLeft;
wrapper.height = (height || bBox.height || 0) + 2 * padding;
// update the label-scoped y offset
@@ -3819,11 +3840,11 @@
boxY = baseline ? -baselineOffset : 0;
wrapper.box = box = shape ?
renderer.symbol(shape, boxX, boxY, wrapper.width, wrapper.height, deferredAttr) :
renderer.rect(boxX, boxY, wrapper.width, wrapper.height, 0, deferredAttr[STROKE_WIDTH]);
- box.add(wrapper);
+ box.attr('fill', NONE).add(wrapper);
}
// apply the box attributes
if (!box.isImg) { // #1630
box.attr(merge({
@@ -3846,11 +3867,11 @@
// determin y based on the baseline
y = baseline ? 0 : baselineOffset;
// compensate for alignment
- if (defined(width) && (textAlign === 'center' || textAlign === 'right')) {
+ if (defined(width) && bBox && (textAlign === 'center' || textAlign === 'right')) {
x += { center: 0.5, right: 1 }[textAlign] * (width - bBox.width);
}
// update if anything changed
if (x !== text.x || y !== text.y) {
@@ -3876,11 +3897,15 @@
} else {
deferredAttr[key] = value;
}
}
- function getSizeAfterAdd() {
+ /**
+ * After the text element is added, get the desired size of the border box
+ * and add it before the text in the DOM.
+ */
+ wrapper.onAdd = function () {
text.add(wrapper);
wrapper.attr({
text: str, // alignment is available now
x: x,
y: y
@@ -3890,18 +3915,12 @@
wrapper.attr({
anchorX: anchorX,
anchorY: anchorY
});
}
- }
+ };
- /**
- * After the text element is added, get the desired size of the border box
- * and add it before the text in the DOM.
- */
- addEvent(wrapper, 'add', getSizeAfterAdd);
-
/*
* Add specific attribute setters.
*/
// only change local variables
@@ -3943,17 +3962,19 @@
return false;
};
// apply these to the box but not to the text
attrSetters[STROKE_WIDTH] = function (value, key) {
- needsBox = true;
+ if (value) {
+ needsBox = true;
+ }
crispAdjust = value % 2 / 2;
boxAttr(key, value);
return false;
};
attrSetters.stroke = attrSetters.fill = attrSetters.r = function (value, key) {
- if (key === 'fill') {
+ if (key === 'fill' && value) {
needsBox = true;
}
boxAttr(key, value);
return false;
};
@@ -4025,11 +4046,10 @@
},
/**
* Destroy and release memory.
*/
destroy: function () {
- removeEvent(wrapper, 'add', getSizeAfterAdd);
// Added by button implementation
removeEvent(wrapper.element, 'mouseenter');
removeEvent(wrapper.element, 'mouseleave');
@@ -4041,11 +4061,11 @@
}
// Call base implementation to destroy the rest
SVGElement.prototype.destroy.call(wrapper);
// Release local pointers (#1298)
- wrapper = renderer = updateBoxSize = updateTextPadding = boxAttr = getSizeAfterAdd = null;
+ wrapper = renderer = updateBoxSize = updateTextPadding = boxAttr = null;
}
});
}
}; // end SVGRenderer
@@ -4204,11 +4224,11 @@
setSpanRotation: function (rotation, alignCorrection, baseline) {
var rotationStyle = {},
cssTransformKey = isIE ? '-ms-transform' : isWebKit ? '-webkit-transform' : isFirefox ? 'MozTransform' : isOpera ? '-o-transform' : '';
rotationStyle[cssTransformKey] = rotationStyle.transform = 'rotate(' + rotation + 'deg)';
- rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] = (alignCorrection * 100) + '% ' + baseline + 'px';
+ rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] = rotationStyle.transformOrigin = (alignCorrection * 100) + '% ' + baseline + 'px';
css(this.element, rotationStyle);
},
/**
* Get the correction in X and Y positioning as the element is rotated.
@@ -4228,32 +4248,32 @@
* @param {String} str
* @param {Number} x
* @param {Number} y
*/
html: function (str, x, y) {
- var defaultChartStyle = defaultOptions.chart.style,
- wrapper = this.createElement('span'),
+ var wrapper = this.createElement('span'),
attrSetters = wrapper.attrSetters,
element = wrapper.element,
renderer = wrapper.renderer;
// Text setter
attrSetters.text = function (value) {
if (value !== element.innerHTML) {
delete this.bBox;
}
- element.innerHTML = value;
+ element.innerHTML = this.textStr = value;
return false;
};
// Various setters which rely on update transform
attrSetters.x = attrSetters.y = attrSetters.align = attrSetters.rotation = function (value, key) {
if (key === 'align') {
key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML.
}
wrapper[key] = value;
wrapper.htmlUpdateTransform();
+
return false;
};
// Set the default attributes
wrapper.attr({
@@ -4262,12 +4282,12 @@
y: mathRound(y)
})
.css({
position: ABSOLUTE,
whiteSpace: 'nowrap',
- fontFamily: defaultChartStyle.fontFamily,
- fontSize: defaultChartStyle.fontSize
+ fontFamily: this.style.fontFamily,
+ fontSize: this.style.fontSize
});
// Use the HTML specific .css method
wrapper.css = wrapper.htmlCss;
@@ -4431,11 +4451,13 @@
if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) {
wrapper.updateTransform();
}
// fire an event for internal hooks
- fireEvent(wrapper, 'add');
+ if (wrapper.onAdd) {
+ wrapper.onAdd();
+ }
return wrapper;
},
/**
@@ -4627,11 +4649,16 @@
skipAttr = true;
// handle visibility
} else if (key === 'visibility') {
- // let the shadow follow the main element
+ // Handle inherited visibility
+ if (value === 'inherit') {
+ value = VISIBLE;
+ }
+
+ // Let the shadow follow the main element
if (shadows) {
i = shadows.length;
while (i--) {
shadows[i].style[key] = value;
}
@@ -4944,21 +4971,21 @@
* Initialize the VMLRenderer
* @param {Object} container
* @param {Number} width
* @param {Number} height
*/
- init: function (container, width, height) {
+ init: function (container, width, height, style) {
var renderer = this,
boxWrapper,
box,
css;
renderer.alignedObjects = [];
- boxWrapper = renderer.createElement(DIV);
+ boxWrapper = renderer.createElement(DIV)
+ .css(extend(this.getStyle(style), { position: RELATIVE}));
box = boxWrapper.element;
- box.style.position = RELATIVE; // for freeform drawing using renderer directly
container.appendChild(boxWrapper.element);
// generate the containing box
renderer.isVML = true;
@@ -5192,11 +5219,11 @@
// Apply radial gradient
if (wrapper.added) {
applyRadialGradient();
} else {
// We need to know the bounding box to get the size and position right
- addEvent(wrapper, 'add', applyRadialGradient);
+ wrapper.onAdd = applyRadialGradient;
}
// The fill element's color attribute is broken in IE8 standards mode, so we
// need to set the parent shape's fillcolor attribute instead.
ret = color1;
@@ -5345,39 +5372,37 @@
}
return obj;
},
/**
- * VML uses a shape for rect to overcome bugs and rotation problems
+ * For rectangles, VML uses a shape for rect to overcome bugs and rotation problems
*/
- rect: function (x, y, width, height, r, strokeWidth) {
-
- var wrapper = this.symbol('rect');
- wrapper.r = isObject(x) ? x.r : r;
-
- //return wrapper.attr(wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0)));
- return wrapper.attr(
- isObject(x) ?
- x :
- // do not crispify when an object is passed in (as in column charts)
- wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0))
- );
+ createElement: function (nodeName) {
+ return nodeName === 'rect' ? this.symbol(nodeName) : SVGRenderer.prototype.createElement.call(this, nodeName);
},
/**
* In the VML renderer, each child of an inverted div (group) is inverted
* @param {Object} element
* @param {Object} parentNode
*/
invertChild: function (element, parentNode) {
- var parentStyle = parentNode.style;
+ var ren = this,
+ parentStyle = parentNode.style,
+ imgStyle = element.tagName === 'IMG' && element.style; // #1111
+
css(element, {
flip: 'x',
- left: pInt(parentStyle.width) - 1,
- top: pInt(parentStyle.height) - 1,
+ left: pInt(parentStyle.width) - (imgStyle ? pInt(imgStyle.top) : 1),
+ top: pInt(parentStyle.height) - (imgStyle ? pInt(imgStyle.left) : 1),
rotation: -90
});
+
+ // Recursively invert child elements, needed for nested composite shapes like box plots and error bars. #1680, #1806.
+ each(element.childNodes, function (child) {
+ ren.invertChild(child, element);
+ });
},
/**
* Symbol definitions that override the parent SVG renderer's symbols
*
@@ -5672,22 +5697,21 @@
isFirst = pos === tickPositions[0],
isLast = pos === tickPositions[tickPositions.length - 1],
css,
attr,
value = categories ?
- pick(categories[pos], names[pos], pos) :
+ pick(categories[pos], names[pos], pos) :
pos,
label = tick.label,
tickPositionInfo = tickPositions.info,
dateTimeLabelFormat;
// Set the datetime label format. If a higher rank is set for this position, use that. If not,
// use the general format.
if (axis.isDatetimeAxis && tickPositionInfo) {
dateTimeLabelFormat = options.dateTimeLabelFormats[tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName];
}
-
// set properties for access in render method
tick.isFirst = isFirst;
tick.isLast = isLast;
// get the string
@@ -5760,14 +5784,17 @@
horiz = axis.horiz,
options = axis.options,
labelOptions = options.labels,
size = horiz ? bBox.width : bBox.height,
leftSide = horiz ?
- size * { left: 0, center: 0.5, right: 1 }[axis.labelAlign] - labelOptions.x :
+ labelOptions.x - size * { left: 0, center: 0.5, right: 1 }[axis.labelAlign] :
+ 0,
+ rightSide = horiz ?
+ size + leftSide :
size;
- return [-leftSide, size - leftSide];
+ return [leftSide, rightSide];
},
/**
* Handle the label overflow by adjusting the labels to the left and right edge, or
* hide them if they collide into the neighbour label.
@@ -5782,28 +5809,38 @@
reversed = axis.reversed,
tickPositions = axis.tickPositions,
sides = this.getLabelSides(),
leftSide = sides[0],
rightSide = sides[1],
- axisLeft = axis.pos,
- axisRight = axisLeft + axis.len,
+ axisLeft,
+ axisRight,
neighbour,
neighbourEdge,
line = this.label.line || 0,
labelEdge = axis.labelEdge,
- justifyLabel = axis.justifyLabels && (isFirst || isLast);
+ justifyLabel = axis.justifyLabels && (isFirst || isLast),
+ justifyToPlot;
// Hide it if it now overlaps the neighbour label
if (labelEdge[line] === UNDEFINED || pxPos + leftSide > labelEdge[line]) {
labelEdge[line] = pxPos + rightSide;
-
+
} else if (!justifyLabel) {
show = false;
}
-
+
if (justifyLabel) {
- neighbour = axis.ticks[tickPositions[index + (isFirst ? 1 : -1)]];
+ justifyToPlot = axis.justifyToPlot;
+ axisLeft = justifyToPlot ? axis.pos : 0;
+ axisRight = justifyToPlot ? axisLeft + axis.len : axis.chart.chartWidth;
+
+ // Find the firsth neighbour on the same line
+ do {
+ index += (isFirst ? 1 : -1);
+ neighbour = axis.ticks[tickPositions[index]];
+ } while (tickPositions[index] && (!neighbour || neighbour.label.line !== line));
+
neighbourEdge = neighbour && neighbour.label.xy && neighbour.label.xy.x + neighbour.getLabelSides()[isFirst ? 0 : 1];
if ((isFirst && !reversed) || (isLast && reversed)) {
// Is the label spilling out to the left of the plot area?
if (pxPos + leftSide < axisLeft) {
@@ -5843,61 +5880,61 @@
*/
getPosition: function (horiz, pos, tickmarkOffset, old) {
var axis = this.axis,
chart = axis.chart,
cHeight = (old && chart.oldChartHeight) || chart.chartHeight;
-
+
return {
x: horiz ?
axis.translate(pos + tickmarkOffset, null, null, old) + axis.transB :
axis.left + axis.offset + (axis.opposite ? ((old && chart.oldChartWidth) || chart.chartWidth) - axis.right - axis.left : 0),
y: horiz ?
cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) :
cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB
};
-
+
},
-
+
/**
* Get the x, y position of the tick label
*/
getLabelPosition: function (x, y, label, horiz, labelOptions, tickmarkOffset, index, step) {
var axis = this.axis,
transA = axis.transA,
reversed = axis.reversed,
staggerLines = axis.staggerLines,
baseline = axis.chart.renderer.fontMetrics(labelOptions.style.fontSize).b,
rotation = labelOptions.rotation;
-
+
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) && !rotation) { // #1951
y += baseline - label.getBBox().height / 2;
}
-
+
// Correct for staggered labels
if (staggerLines) {
label.line = (index / (step || 1) % staggerLines);
y += label.line * (axis.labelOffset / staggerLines);
}
-
+
return {
x: x,
y: y
};
},
-
+
/**
* Extendible method to return the path of the marker
*/
getMarkPath: function (x, y, tickLength, tickWidth, horiz, renderer) {
return renderer.crispLine([
@@ -5948,11 +5985,11 @@
x = xy.x,
y = xy.y,
reverseCrisp = ((horiz && x === axis.pos + axis.len) || (!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687
this.isActive = true;
-
+
// create the grid line
if (gridLineWidth) {
gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLineWidth * reverseCrisp, old, true);
if (gridLine === UNDEFINED) {
@@ -5996,11 +6033,10 @@
if (axis.opposite) {
tickLength = -tickLength;
}
markPath = tick.getMarkPath(x, y, tickLength, tickWidth * reverseCrisp, horiz, renderer);
-
if (mark) { // updating
mark.animate({
d: markPath,
opacity: opacity
});
@@ -6017,11 +6053,11 @@
// the label is created on init - now move it into place
if (label && !isNaN(x)) {
label.xy = xy = tick.getLabelPosition(x, y, label, horiz, labelOptions, tickmarkOffset, index, step);
- // Apply show first and show last. If the tick is both first and last, it is
+ // Apply show first and show last. If the tick is both first and last, it is
// a single centered tick, in which case we show the label anyway (#2100).
if ((tick.isFirst && !tick.isLast && !pick(options.showFirstLabel, 1)) ||
(tick.isLast && !tick.isFirst && !pick(options.showLastLabel, 1))) {
show = false;
@@ -6033,11 +6069,11 @@
// apply step
if (step && index % step) {
// show those indices dividable by step
show = false;
}
-
+
// Set the new position, and show or hide
if (show && !isNaN(xy.y)) {
xy.opacity = opacity;
label[tick.isNew ? 'attr' : 'animate'](xy);
tick.isNew = false;
@@ -6057,20 +6093,20 @@
/**
* The object wrapper for plot lines and plot bands
* @param {Object} options
*/
-var PlotLineOrBand = function (axis, options) {
+Highcharts.PlotLineOrBand = function (axis, options) {
this.axis = axis;
if (options) {
this.options = options;
this.id = options.id;
}
};
-PlotLineOrBand.prototype = {
+Highcharts.PlotLineOrBand.prototype = {
/**
* Render the plot line or plot band. If it is already existing,
* move it.
*/
@@ -6274,11 +6310,11 @@
* Add a plot band or plot line after render time
*
* @param options {Object} The plotBand or plotLine configuration object
*/
addPlotBandOrLine: function (options, coll) {
- var obj = new PlotLineOrBand(this, options).render(),
+ var obj = new Highcharts.PlotLineOrBand(this, options).render(),
userOptions = this.userOptions;
if (obj) { // #2189
// Add it to the user options for exporting and Axis.update
if (coll) {
@@ -6514,11 +6550,11 @@
axis.horiz = chart.inverted ? !isXAxis : isXAxis;
// Flag, isXAxis
axis.isXAxis = isXAxis;
axis.coll = isXAxis ? 'xAxis' : 'yAxis';
-
+
axis.opposite = userOptions.opposite; // needed in setOptions
axis.side = userOptions.side || (axis.horiz ?
(axis.opposite ? 0 : 2) : // top : bottom
(axis.opposite ? 1 : 3)); // right : left
@@ -6606,14 +6642,11 @@
// Dictionary for stacks
axis.stacks = {};
axis.oldStacks = {};
-
- // Dictionary for stacks max values
- axis.stackExtremes = {};
-
+
// Min and max in the data
//axis.dataMin = UNDEFINED,
//axis.dataMax = UNDEFINED,
// The axis range
@@ -6631,11 +6664,16 @@
var eventType,
events = axis.options.events;
// Register
if (inArray(axis, chart.axes) === -1) { // don't add it again on Axis.update()
- chart.axes.push(axis);
+ if (isXAxis && !this.isColorAxis) { // #2713
+ chart.axes.splice(chart.xAxis.length, 0, axis);
+ } else {
+ chart.axes.push(axis);
+ }
+
chart[axis.coll].push(axis);
}
axis.series = axis.series || []; // populated by Series
@@ -6735,16 +6773,15 @@
axis.hasVisibleSeries = false;
// reset dataMin and dataMax in case we're redrawing
axis.dataMin = axis.dataMax = null;
+
+ if (axis.buildStacks) {
+ axis.buildStacks();
+ }
- // reset cached stacking extremes
- axis.stackExtremes = {};
-
- axis.buildStacks();
-
// loop through this axis' series
each(axis.series, function (series) {
if (series.visible || !chart.options.chart.ignoreHiddenSeries) {
@@ -6804,11 +6841,10 @@
* Translate from axis value to pixel position on the chart, or back
*
*/
translate: function (val, backwards, cvsCoord, old, handleLog, pointPlacement) {
var axis = this,
- axisLength = axis.len,
sign = 1,
cvsOffset = 0,
localA = old ? axis.oldTransA : axis.transA,
localMin = old ? axis.oldMin : axis.min,
returnValue,
@@ -6821,17 +6857,17 @@
// In vertical axes, the canvas coordinates start from 0 at the top like in
// SVG.
if (cvsCoord) {
sign *= -1; // canvas coordinates inverts the value
- cvsOffset = axisLength;
+ cvsOffset = axis.len;
}
// Handle reversed axis
if (axis.reversed) {
sign *= -1;
- cvsOffset -= sign * axisLength;
+ cvsOffset -= sign * (axis.sector || axis.len);
}
// From pixels to value
if (backwards) { // reverse translation
@@ -7078,28 +7114,28 @@
* Update translation information
*/
setAxisTranslation: function (saveOld) {
var axis = this,
range = axis.max - axis.min,
- pointRange = 0,
+ pointRange = axis.axisPointRange || 0,
closestPointRange,
minPointOffset = 0,
pointRangePadding = 0,
linkedParent = axis.linkedParent,
ordinalCorrection,
hasCategories = !!axis.categories,
transA = axis.transA;
// Adjust translation for padding. Y axis with categories need to go through the same (#1784).
- if (axis.isXAxis || hasCategories) {
+ if (axis.isXAxis || hasCategories || pointRange) {
if (linkedParent) {
minPointOffset = linkedParent.minPointOffset;
pointRangePadding = linkedParent.pointRangePadding;
} else {
each(axis.series, function (series) {
- var seriesPointRange = mathMax(series.pointRange, +hasCategories),
+ var seriesPointRange = mathMax(axis.isXAxis ? series.pointRange : (axis.axisPointRange || 0), +hasCategories),
pointPlacement = series.options.pointPlacement,
seriesClosestPointRange = series.closestPointRange;
if (seriesPointRange > range) { // #1446
seriesPointRange = 0;
@@ -7201,11 +7237,11 @@
// handle zoomed range
if (axis.range && defined(axis.max)) {
axis.userMin = axis.min = mathMax(axis.min, axis.max - axis.range); // #618
axis.userMax = axis.max;
-
+
axis.range = null; // don't use it when running setExtremes
}
// Hook for adjusting this.min and this.max. Used by bubble series.
if (axis.beforePadding) {
@@ -7215,11 +7251,11 @@
// adjust min and max for the minimum range
axis.adjustForMinRange();
// Pad the values to get clear of the chart's edges. To avoid tickInterval taking the padding
// into account, we do this after computing tick interval (#1337).
- if (!categories && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) {
+ if (!categories && !axis.axisPointRange && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) {
length = axis.max - axis.min;
if (length) {
if (!defined(options.min) && !defined(axis.userMin) && minPadding && (axis.dataMin < 0 || !axis.ignoreMinPadding)) {
axis.min -= length * minPadding;
}
@@ -7243,11 +7279,11 @@
// don't let it be more than the data range
(axis.max - axis.min) * tickPixelIntervalOption / mathMax(axis.len, tickPixelIntervalOption)
);
// For squished axes, set only two ticks
if (!defined(tickIntervalOption) && axis.len < tickPixelIntervalOption && !this.isRadial &&
- !categories && options.startOnTick && options.endOnTick) {
+ !this.isLog && !categories && options.startOnTick && options.endOnTick) {
keepTwoTicksOnly = true;
axis.tickInterval /= 4; // tick extremes closer to the real values
}
}
@@ -7349,11 +7385,11 @@
// When there is only one point, or all points have the same value on this axis, then min
// and max are equal and tickPositions.length is 1. In this case, add some padding
// in order to center the point, but leave it with one tick. #1337.
if (tickPositions.length === 1) {
- singlePad = 0.001; // The lowest possible number to avoid extra padding on columns
+ singlePad = mathAbs(axis.max || 1) * 0.001; // The lowest possible number to avoid extra padding on columns (#2619)
axis.min -= singlePad;
axis.max += singlePad;
}
}
},
@@ -7365,11 +7401,11 @@
var chart = this.chart,
maxTicks = chart.maxTicks || {},
tickPositions = this.tickPositions,
key = this._maxTicksKey = [this.coll, this.pos, this.len].join('-');
-
+
if (!this.isLinked && !this.isDatetimeAxis && tickPositions && tickPositions.length > (maxTicks[key] || 0) && this.options.alignTicks !== false) {
maxTicks[key] = tickPositions.length;
}
chart.maxTicks = maxTicks;
},
@@ -7528,17 +7564,20 @@
/**
* Overridable method for zooming chart. Pulled out in a separate method to allow overriding
* in stock charts.
*/
zoom: function (newMin, newMax) {
+ var dataMin = this.dataMin,
+ dataMax = this.dataMax,
+ options = this.options;
- // Prevent pinch zooming out of range. Check for defined is for #1946.
+ // Prevent pinch zooming out of range. Check for defined is for #1946. #1734.
if (!this.allowZoomOutside) {
- if (defined(this.dataMin) && newMin <= this.dataMin) {
+ if (defined(dataMin) && newMin <= mathMin(dataMin, pick(options.min, dataMin))) {
newMin = UNDEFINED;
}
- if (defined(this.dataMax) && newMax >= this.dataMax) {
+ if (defined(dataMax) && newMax >= mathMax(dataMax, pick(options.max, dataMax))) {
newMax = UNDEFINED;
}
}
// In full view, displaying the reset zoom button is not required
@@ -7671,28 +7710,29 @@
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 })
.add();
axis.axisGroup = renderer.g('axis')
.attr({ zIndex: options.zIndex || 2 })
.add();
axis.labelGroup = renderer.g('axis-labels')
.attr({ zIndex: labelOptions.zIndex || 7 })
+ .addClass(PREFIX + axis.coll.toLowerCase() + '-labels')
.add();
}
if (hasData || axis.isLinked) {
@@ -7780,10 +7820,11 @@
rotation: axisTitleOptions.rotation || 0,
align:
axisTitleOptions.textAlign ||
{ low: 'left', middle: 'center', high: 'right' }[axisTitleOptions.align]
})
+ .addClass(PREFIX + this.coll.toLowerCase() + '-title')
.css(axisTitleOptions.style)
.add(axis.axisGroup);
axis.axisTitle.isNew = true;
}
@@ -7898,12 +7939,11 @@
options = axis.options,
isLog = axis.isLog,
isLinked = axis.isLinked,
tickPositions = axis.tickPositions,
sortedPositions,
- axisTitle = axis.axisTitle,
- stacks = axis.stacks,
+ axisTitle = axis.axisTitle,
ticks = axis.ticks,
minorTicks = axis.minorTicks,
alternateBands = axis.alternateBands,
stackLabelOptions = options.stackLabels,
alternateGridColor = options.alternateGridColor,
@@ -7913,15 +7953,17 @@
hasRendered = chart.hasRendered,
slideInTicks = hasRendered && defined(axis.oldMin) && !isNaN(axis.oldMin),
hasData = axis.hasData,
showAxis = axis.showAxis,
from,
- justifyLabels = axis.justifyLabels = !axis.staggerLines && horiz && options.labels.overflow === 'justify',
+ overflow = options.labels.overflow,
+ justifyLabels = axis.justifyLabels = horiz && overflow !== false,
to;
// Reset
axis.labelEdge.length = 0;
+ axis.justifyToPlot = overflow === 'justify';
// Mark all elements inActive before we go over and mark the active ones
each([ticks, minorTicks, alternateBands], function (coll) {
var pos;
for (pos in coll) {
@@ -7995,11 +8037,11 @@
// alternate grid color
if (alternateGridColor) {
each(tickPositions, function (pos, i) {
if (i % 2 === 0 && pos < axis.max) {
if (!alternateBands[pos]) {
- alternateBands[pos] = new PlotLineOrBand(axis);
+ alternateBands[pos] = new Highcharts.PlotLineOrBand(axis);
}
from = pos + tickmarkOffset; // #949
to = tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] + tickmarkOffset : axis.max;
alternateBands[pos].options = {
from: isLog ? lin2log(from) : from,
@@ -8088,35 +8130,11 @@
axisTitle.isNew = false;
}
// Stacked totals:
if (stackLabelOptions && stackLabelOptions.enabled) {
- var stackKey, oneStack, stackCategory,
- stackTotalGroup = axis.stackTotalGroup;
-
- // Create a separate group for the stack total labels
- if (!stackTotalGroup) {
- axis.stackTotalGroup = stackTotalGroup =
- renderer.g('stack-labels')
- .attr({
- visibility: VISIBLE,
- zIndex: 6
- })
- .add();
- }
-
- // plotLeft/Top will change when y axis gets wider so we need to translate the
- // stackTotalGroup at every render call. See bug #506 and #516
- stackTotalGroup.translate(chart.plotLeft, chart.plotTop);
-
- // Render each stack total
- for (stackKey in stacks) {
- oneStack = stacks[stackKey];
- for (stackCategory in oneStack) {
- oneStack[stackCategory].render(stackTotalGroup);
- }
- }
+ axis.renderStackTotals();
}
// End stacked totals
axis.isDirty = false;
},
@@ -8128,11 +8146,11 @@
var axis = this,
chart = axis.chart,
pointer = chart.pointer;
// hide tooltip and hover states
- if (pointer.reset) {
+ if (pointer) {
pointer.reset(true);
}
// render the axis
axis.render();
@@ -8148,29 +8166,10 @@
});
},
/**
- * Build the stacks from top down
- */
- buildStacks: function () {
- var series = this.series,
- i = series.length;
- if (!this.isXAxis) {
- while (i--) {
- series[i].setStackedPoints();
- }
- // Loop up again to compute percent stack
- if (this.usePercentage) {
- for (i = 0; i < series.length; i++) {
- series[i].setPercentStacks();
- }
- }
- }
- },
-
- /**
* Destroys an Axis instance.
*/
destroy: function (keepEvents) {
var axis = this,
stacks = axis.stacks,
@@ -8274,112 +8273,12 @@
}
}
}; // end Axis
extend(Axis.prototype, AxisPlotLineOrBandExtension);
-/**
- * Methods defined on the Axis prototype
- */
/**
- * Set the tick positions of a logarithmic axis
- */
-Axis.prototype.getLogTickPositions = function (interval, min, max, minor) {
- var axis = this,
- options = axis.options,
- axisLength = axis.len,
- // Since we use this method for both major and minor ticks,
- // use a local variable and return the result
- positions = [];
-
- // Reset
- if (!minor) {
- axis._minorAutoInterval = null;
- }
-
- // First case: All ticks fall on whole logarithms: 1, 10, 100 etc.
- if (interval >= 0.5) {
- interval = mathRound(interval);
- positions = axis.getLinearTickPositions(interval, min, max);
-
- // Second case: We need intermediary ticks. For example
- // 1, 2, 4, 6, 8, 10, 20, 40 etc.
- } else if (interval >= 0.08) {
- var roundedMin = mathFloor(min),
- intermediate,
- i,
- j,
- len,
- pos,
- lastPos,
- break2;
-
- if (interval > 0.3) {
- intermediate = [1, 2, 4];
- } else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc
- intermediate = [1, 2, 4, 6, 8];
- } else { // 0.1 equals ten minor ticks per 1, 10, 100 etc
- intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
- }
-
- for (i = roundedMin; i < max + 1 && !break2; i++) {
- len = intermediate.length;
- for (j = 0; j < len && !break2; j++) {
- pos = log2lin(lin2log(i) * intermediate[j]);
-
- if (pos > min && (!minor || lastPos <= max)) { // #1670
- positions.push(lastPos);
- }
-
- if (lastPos > max) {
- break2 = true;
- }
- lastPos = pos;
- }
- }
-
- // Third case: We are so deep in between whole logarithmic values that
- // we might as well handle the tick positions like a linear axis. For
- // example 1.01, 1.02, 1.03, 1.04.
- } else {
- var realMin = lin2log(min),
- realMax = lin2log(max),
- tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'],
- filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption,
- tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1),
- totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength;
-
- interval = pick(
- filteredTickIntervalOption,
- axis._minorAutoInterval,
- (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1)
- );
-
- interval = normalizeTickInterval(
- interval,
- null,
- getMagnitude(interval)
- );
-
- positions = map(axis.getLinearTickPositions(
- interval,
- realMin,
- realMax
- ), log2lin);
-
- if (!minor) {
- axis._minorAutoInterval = interval / 5;
- }
- }
-
- // Set the axis-level tickInterval variable
- if (!minor) {
- axis.tickInterval = interval;
- }
- return positions;
-};
-/**
* Set the tick positions to a time unit that makes sense, for example
* on the first of each month or on every Monday. Return an array
* with the time positions. Used in datetime axes as well as for grouping
* data on a datetime axis.
*
@@ -8575,126 +8474,117 @@
unitRange: interval,
count: count,
unitName: unit[0]
};
};/**
- * The class for stack items
+ * Methods defined on the Axis prototype
*/
-function StackItem(axis, options, isNegative, x, stackOption, stacking) {
-
- var inverted = axis.chart.inverted;
- this.axis = axis;
-
- // Tells if the stack is negative
- this.isNegative = isNegative;
-
- // Save the options to be able to style the label
- this.options = options;
-
- // Save the x value to be able to position the label later
- this.x = x;
-
- // Initialize total value
- this.total = null;
-
- // 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
- // if the chart is inverted or not.
- // First test the user supplied value, then use the dynamic.
- this.alignOptions = {
- align: options.align || (inverted ? (isNegative ? 'left' : 'right') : 'center'),
- verticalAlign: options.verticalAlign || (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')),
- y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)),
- x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0)
- };
-
- this.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center');
-}
-
-StackItem.prototype = {
- destroy: function () {
- destroyObjectProperties(this, this.axis);
- },
-
- /**
- * Renders the stack total label and adds it to the stack label group.
- */
- render: function (group) {
- var options = this.options,
- 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
- if (this.label) {
- this.label.attr({text: str, visibility: HIDDEN});
- // Create new label
- } else {
- this.label =
- this.axis.chart.renderer.text(str, 0, 0, options.useHTML) // dummy positions, actual position updated with setOffset method in columnseries
- .css(options.style) // apply style
- .attr({
- align: this.textAlign, // fix the text-anchor
- rotation: options.rotation, // rotation
- visibility: HIDDEN // hidden until setOffset is called
- })
- .add(group); // add to the labels-group
+/**
+ * Set the tick positions of a logarithmic axis
+ */
+Axis.prototype.getLogTickPositions = function (interval, min, max, minor) {
+ var axis = this,
+ options = axis.options,
+ axisLength = axis.len,
+ // Since we use this method for both major and minor ticks,
+ // use a local variable and return the result
+ positions = [];
+
+ // Reset
+ if (!minor) {
+ axis._minorAutoInterval = null;
+ }
+
+ // First case: All ticks fall on whole logarithms: 1, 10, 100 etc.
+ if (interval >= 0.5) {
+ interval = mathRound(interval);
+ positions = axis.getLinearTickPositions(interval, min, max);
+
+ // Second case: We need intermediary ticks. For example
+ // 1, 2, 4, 6, 8, 10, 20, 40 etc.
+ } else if (interval >= 0.08) {
+ var roundedMin = mathFloor(min),
+ intermediate,
+ i,
+ j,
+ len,
+ pos,
+ lastPos,
+ break2;
+
+ if (interval > 0.3) {
+ intermediate = [1, 2, 4];
+ } else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc
+ intermediate = [1, 2, 4, 6, 8];
+ } else { // 0.1 equals ten minor ticks per 1, 10, 100 etc
+ intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
}
- },
-
- /**
- * Sets the offset that the stack has from the x value and repositions the label.
- */
- setOffset: function (xOffset, xWidth) {
- var stackItem = this,
- axis = stackItem.axis,
- chart = axis.chart,
- inverted = chart.inverted,
- neg = this.isNegative, // special treatment is needed for negative stacks
- y = axis.translate(this.percent ? 100 : this.total, 0, 0, 0, 1), // stack value translated mapped to chart coordinates
- yZero = axis.translate(0), // stack origin
- h = mathAbs(y - yZero), // stack height
- x = chart.xAxis[0].translate(this.x) + xOffset, // stack x position
- plotHeight = chart.plotHeight,
- stackBox = { // this is the box for the complete stack
- x: inverted ? (neg ? y : y - h) : x,
- y: inverted ? plotHeight - x - xWidth : (neg ? (plotHeight - y - h) : plotHeight - y),
- width: inverted ? h : xWidth,
- height: inverted ? xWidth : h
- },
- label = this.label,
- alignAttr;
- if (label) {
- label.align(this.alignOptions, null, stackBox); // align the label to the box
+ for (i = roundedMin; i < max + 1 && !break2; i++) {
+ len = intermediate.length;
+ for (j = 0; j < len && !break2; j++) {
+ pos = log2lin(lin2log(i) * intermediate[j]);
- // Set visibility (#678)
- alignAttr = label.alignAttr;
- label.attr({
- visibility: this.options.crop === false || chart.isInsidePlot(alignAttr.x, alignAttr.y) ?
- (hasSVG ? 'inherit' : VISIBLE) :
- HIDDEN
- });
+ if (pos > min && (!minor || lastPos <= max)) { // #1670
+ positions.push(lastPos);
+ }
+
+ if (lastPos > max) {
+ break2 = true;
+ }
+ lastPos = pos;
+ }
}
+
+ // Third case: We are so deep in between whole logarithmic values that
+ // we might as well handle the tick positions like a linear axis. For
+ // example 1.01, 1.02, 1.03, 1.04.
+ } else {
+ var realMin = lin2log(min),
+ realMax = lin2log(max),
+ tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'],
+ filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption,
+ tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1),
+ totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength;
+
+ interval = pick(
+ filteredTickIntervalOption,
+ axis._minorAutoInterval,
+ (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1)
+ );
+
+ interval = normalizeTickInterval(
+ interval,
+ null,
+ getMagnitude(interval)
+ );
+
+ positions = map(axis.getLinearTickPositions(
+ interval,
+ realMin,
+ realMax
+ ), log2lin);
+
+ if (!minor) {
+ axis._minorAutoInterval = interval / 5;
+ }
}
-};
-/**
+
+ // Set the axis-level tickInterval variable
+ if (!minor) {
+ axis.tickInterval = interval;
+ }
+ return positions;
+};/**
* The tooltip object
* @param {Object} chart The chart instance
* @param {Object} options Tooltip options
*/
-function Tooltip() {
+var Tooltip = Highcharts.Tooltip = function () {
this.init.apply(this, arguments);
-}
+};
Tooltip.prototype = {
init: function (chart, options) {
@@ -8729,11 +8619,11 @@
zIndex: 8
})
.css(style)
.css({ padding: 0 }) // Remove it from VML, the padding is applied as an attribute instead (#1117)
.add()
- .attr({ y: -999 }); // #2301
+ .attr({ y: -9999 }); // #2301, #2657
// When using canVG the shadow shows up as a gray circle
// even if the tooltip is hidden.
if (!useCanVG) {
this.label.shadow(options.shadow);
@@ -8885,16 +8775,16 @@
plotLeft = chart.plotLeft,
plotTop = chart.plotTop,
plotWidth = chart.plotWidth,
plotHeight = chart.plotHeight,
distance = pick(this.options.distance, 12),
- pointX = point.plotX,
+ pointX = (isNaN(point.plotX) ? 0 : point.plotX), //#2599
pointY = point.plotY,
x = pointX + plotLeft + (chart.inverted ? distance : -boxWidth - distance),
y = pointY - boxHeight + plotTop + 15, // 15 means the point is 15 pixels up from the bottom of the tooltip
alignedRight;
-
+
// It is too far to the left, adjust it
if (x < 7) {
x = plotLeft + mathMax(pointX, 0) + distance;
}
@@ -8933,11 +8823,11 @@
var items = this.points || splat(this),
series = items[0].series,
s;
// build the header
- s = [series.tooltipHeaderFormatter(items[0])];
+ s = [tooltip.tooltipHeaderFormatter(items[0])];
// build the values
each(items, function (item) {
series = item.series;
s.push((series.tooltipFormatter && series.tooltipFormatter(item)) ||
@@ -9065,12 +8955,65 @@
mathRound(pos.x),
mathRound(pos.y),
point.plotX + chart.plotLeft,
point.plotY + chart.plotTop
);
+ },
+
+
+ /**
+ * Format the header of the tooltip
+ */
+ tooltipHeaderFormatter: function (point) {
+ var series = point.series,
+ tooltipOptions = series.tooltipOptions,
+ dateTimeLabelFormats = tooltipOptions.dateTimeLabelFormats,
+ xDateFormat = tooltipOptions.xDateFormat,
+ xAxis = series.xAxis,
+ isDateTime = xAxis && xAxis.options.type === 'datetime' && isNumber(point.key),
+ headerFormat = tooltipOptions.headerFormat,
+ closestPointRange = xAxis && xAxis.closestPointRange,
+ n;
+
+ // Guess the best date format based on the closest point distance (#568)
+ if (isDateTime && !xDateFormat) {
+ if (closestPointRange) {
+ for (n in timeUnits) {
+ if (timeUnits[n] >= closestPointRange ||
+ // If the point is placed every day at 23:59, we need to show
+ // the minutes as well. This logic only works for time units less than
+ // a day, since all higher time units are dividable by those. #2637.
+ (timeUnits[n] <= timeUnits[DAY] && point.key % timeUnits[n] > 0)) {
+ xDateFormat = dateTimeLabelFormats[n];
+ break;
+ }
+ }
+ } else {
+ xDateFormat = dateTimeLabelFormats.day;
+ }
+
+ xDateFormat = xDateFormat || dateTimeLabelFormats.year; // #2546, 2581
+
+ }
+
+ // Insert the header date format if any
+ if (isDateTime && xDateFormat) {
+ headerFormat = headerFormat.replace('{point.key}', '{point.key:' + xDateFormat + '}');
+ }
+
+ return format(headerFormat, {
+ point: point,
+ series: series
+ });
}
};
+
+var hoverChartIndex;
+
+// Global flag for touch support
+hasTouch = doc.documentElement.ontouchstart !== UNDEFINED;
+
/**
* The mouse tracker object. All methods starting with "on" are primary DOM event handlers.
* Subsequent methods should be named differently from what they are doing.
* @param {Object} chart The Chart instance
* @param {Object} options The root options object
@@ -9106,11 +9049,11 @@
this.runChartClick = chartEvents && !!chartEvents.click;
this.pinchDown = [];
this.lastValidTouch = {};
- if (options.tooltip.enabled) {
+ if (Highcharts.Tooltip && options.tooltip.enabled) {
chart.tooltip = new Tooltip(chart, options.tooltip);
}
this.setDOMEvents();
},
@@ -9124,16 +9067,18 @@
chartY,
ePos;
// common IE normalizing
e = e || win.event;
- if (!e.target) {
- e.target = e.srcElement;
- }
// Framework specific normalizing (#1165)
e = washMouseEvent(e);
+
+ // More IE normalizing, needs to go after washMouseEvent
+ if (!e.target) {
+ e.target = e.srcElement;
+ }
// iOS
ePos = e.touches ? e.touches.item(0) : e;
// Get mouse position
@@ -9214,11 +9159,11 @@
// loop over all series and find the ones with points closest to the mouse
i = series.length;
for (j = 0; j < i; j++) {
if (series[j].visible &&
series[j].options.enableMouseTracking !== false &&
- !series[j].noSharedTooltip && series[j].tooltipPoints.length) {
+ !series[j].noSharedTooltip && series[j].singularTooltips !== true && series[j].tooltipPoints.length) {
point = series[j].tooltipPoints[index];
if (point && point.series) { // not a dummy point, #1544
point._dist = mathAbs(index - point.clientX);
distance = mathMin(distance, point._dist);
points.push(point);
@@ -9238,11 +9183,11 @@
pointer.hoverX = points[0].clientX;
}
}
// separate tooltip and general mouse events
- if (hoverSeries && hoverSeries.tracker) { // only use for line-type series with common tracker
+ if (hoverSeries && hoverSeries.tracker && (!tooltip || !tooltip.followPointer)) { // only use for line-type series with common tracker and while not following the pointer #2584
// get the point
point = hoverSeries.tooltipPoints[index];
// a new point is hovered, refresh the tooltip
@@ -9259,11 +9204,13 @@
}
// Start the event listener to pick up the tooltip
if (tooltip && !pointer._onDocumentMouseMove) {
pointer._onDocumentMouseMove = function (e) {
- pointer.onDocumentMouseMove(e);
+ if (defined(hoverChartIndex)) {
+ charts[hoverChartIndex].pointer.onDocumentMouseMove(e);
+ }
};
addEvent(doc, 'mousemove', pointer._onDocumentMouseMove);
}
// Draw independent crosshairs
@@ -9358,180 +9305,10 @@
// Clip
chart.clipRect.attr(clip || chart.clipBox);
},
/**
- * Run translation operations
- */
- pinchTranslate: function (zoomHor, zoomVert, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) {
- if (zoomHor) {
- this.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
- }
- if (zoomVert) {
- this.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
- }
- },
-
- /**
- * Run translation operations for each direction (horizontal and vertical) independently
- */
- pinchTranslateDirection: function (horiz, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch, forcedScale) {
- var chart = this.chart,
- xy = horiz ? 'x' : 'y',
- XY = horiz ? 'X' : 'Y',
- sChartXY = 'chart' + XY,
- wh = horiz ? 'width' : 'height',
- plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')],
- selectionWH,
- selectionXY,
- clipXY,
- scale = forcedScale || 1,
- inverted = chart.inverted,
- bounds = chart.bounds[horiz ? 'h' : 'v'],
- singleTouch = pinchDown.length === 1,
- touch0Start = pinchDown[0][sChartXY],
- touch0Now = touches[0][sChartXY],
- touch1Start = !singleTouch && pinchDown[1][sChartXY],
- touch1Now = !singleTouch && touches[1][sChartXY],
- outOfBounds,
- transformScale,
- scaleKey,
- setScale = function () {
- if (!singleTouch && mathAbs(touch0Start - touch1Start) > 20) { // Don't zoom if fingers are too close on this axis
- scale = forcedScale || mathAbs(touch0Now - touch1Now) / mathAbs(touch0Start - touch1Start);
- }
-
- clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start;
- selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale;
- };
-
- // Set the scale, first pass
- setScale();
-
- selectionXY = clipXY; // the clip position (x or y) is altered if out of bounds, the selection position is not
-
- // Out of bounds
- if (selectionXY < bounds.min) {
- selectionXY = bounds.min;
- outOfBounds = true;
- } else if (selectionXY + selectionWH > bounds.max) {
- selectionXY = bounds.max - selectionWH;
- outOfBounds = true;
- }
-
- // Is the chart dragged off its bounds, determined by dataMin and dataMax?
- if (outOfBounds) {
-
- // Modify the touchNow position in order to create an elastic drag movement. This indicates
- // to the user that the chart is responsive but can't be dragged further.
- touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]);
- if (!singleTouch) {
- touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]);
- }
-
- // Set the scale, second pass to adapt to the modified touchNow positions
- setScale();
-
- } else {
- lastValidTouch[xy] = [touch0Now, touch1Now];
- }
-
- // Set geometry for clipping, selection and transformation
- if (!inverted) { // TODO: implement clipping for inverted charts
- clip[xy] = clipXY - plotLeftTop;
- clip[wh] = selectionWH;
- }
- scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY;
- transformScale = inverted ? 1 / scale : scale;
-
- selectionMarker[wh] = selectionWH;
- selectionMarker[xy] = selectionXY;
- transform[scaleKey] = scale;
- transform['translate' + XY] = (transformScale * plotLeftTop) + (touch0Now - (transformScale * touch0Start));
- },
-
- /**
- * Handle touch events with two touches
- */
- pinch: function (e) {
-
- var self = this,
- chart = self.chart,
- pinchDown = self.pinchDown,
- followTouchMove = chart.tooltip && chart.tooltip.options.followTouchMove,
- touches = e.touches,
- touchesLength = touches.length,
- lastValidTouch = self.lastValidTouch,
- zoomHor = self.zoomHor || self.pinchHor,
- zoomVert = self.zoomVert || self.pinchVert,
- hasZoom = zoomHor || zoomVert,
- selectionMarker = self.selectionMarker,
- transform = {},
- fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, PREFIX + 'tracker') &&
- chart.runTrackerClick) || chart.runChartClick),
- clip = {};
-
- // On touch devices, only proceed to trigger click if a handler is defined
- if ((hasZoom || followTouchMove) && !fireClickEvent) {
- e.preventDefault();
- }
-
- // Normalize each touch
- map(touches, function (e) {
- return self.normalize(e);
- });
-
- // Register the touch start position
- if (e.type === 'touchstart') {
- each(touches, function (e, i) {
- pinchDown[i] = { chartX: e.chartX, chartY: e.chartY };
- });
- lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && pinchDown[1].chartX];
- lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && pinchDown[1].chartY];
-
- // Identify the data bounds in pixels
- each(chart.axes, function (axis) {
- if (axis.zoomEnabled) {
- var bounds = chart.bounds[axis.horiz ? 'h' : 'v'],
- minPixelPadding = axis.minPixelPadding,
- min = axis.toPixels(axis.dataMin),
- max = axis.toPixels(axis.dataMax),
- absMin = mathMin(min, max),
- absMax = mathMax(min, max);
-
- // Store the bounds for use in the touchmove handler
- bounds.min = mathMin(axis.pos, absMin - minPixelPadding);
- bounds.max = mathMax(axis.pos + axis.len, absMax + minPixelPadding);
- }
- });
-
- // Event type is touchmove, handle panning and pinching
- } else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first
-
-
- // Set the marker
- if (!selectionMarker) {
- self.selectionMarker = selectionMarker = extend({
- destroy: noop
- }, chart.plotBox);
- }
-
- self.pinchTranslate(zoomHor, zoomVert, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
-
- self.hasPinched = hasZoom;
-
- // Scale and translate the groups to provide visual feedback during pinching
- self.scaleGroups(transform, clip);
-
- // Optionally move the tooltip on touchmove
- if (!hasZoom && followTouchMove && touchesLength === 1) {
- this.runPointActions(self.normalize(e));
- }
- }
- },
-
- /**
* Start a drag operation
*/
dragStart: function (e) {
var chart = this.chart;
@@ -9579,10 +9356,11 @@
// determine if the mouse has moved more than 10px
this.hasDragged = Math.sqrt(
Math.pow(mouseDownX - chartX, 2) +
Math.pow(mouseDownY - chartY, 2)
);
+
if (this.hasDragged > 10) {
clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop);
// make a selection
if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside) {
@@ -9700,11 +9478,13 @@
},
onDocumentMouseUp: function (e) {
- this.drop(e);
+ if (defined(hoverChartIndex)) {
+ charts[hoverChartIndex].pointer.drop(e);
+ }
},
/**
* Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea.
* Issue #149 workaround. The mouseleave event does not always fire.
@@ -9725,19 +9505,25 @@
/**
* When mouse leaves the container, hide the tooltip.
*/
onContainerMouseLeave: function () {
- this.reset();
- this.chartPosition = null; // also reset the chart position, used in #149 fix
+ var chart = charts[hoverChartIndex];
+ if (chart) {
+ chart.pointer.reset();
+ chart.pointer.chartPosition = null; // also reset the chart position, used in #149 fix
+ }
+ hoverChartIndex = null;
},
// The mousemove, touchmove and touchstart event handler
onContainerMouseMove: function (e) {
var chart = this.chart;
+ hoverChartIndex = chart.index;
+
// normalize
e = this.normalize(e);
if (chart.mouseIsDown === 'mousedown') {
this.drag(e);
@@ -9832,318 +9618,368 @@
}
},
- onContainerTouchStart: function (e) {
- var chart = this.chart;
-
- if (e.touches.length === 1) {
-
- e = this.normalize(e);
-
- if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
-
- // Prevent the click pseudo event from firing unless it is set in the options
- /*if (!chart.runChartClick) {
- e.preventDefault();
- }*/
-
- // 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);
- }
- },
-
- onContainerTouchMove: function (e) {
- if (e.touches.length === 1 || e.touches.length === 2) {
- this.pinch(e);
- }
- },
-
- onDocumentTouchEnd: function (e) {
- this.drop(e);
- },
-
/**
* Set the JS DOM events on the container and document. This method should contain
* a one-to-one assignment between methods and their handlers. Any advanced logic should
* be moved to the handler reflecting the event's name.
*/
setDOMEvents: function () {
var pointer = this,
- container = pointer.chart.container,
- events;
+ container = pointer.chart.container;
- this._events = events = [
- [container, 'onmousedown', 'onContainerMouseDown'],
- [container, 'onmousemove', 'onContainerMouseMove'],
- [container, 'onclick', 'onContainerClick'],
- [container, 'mouseleave', 'onContainerMouseLeave'],
- [doc, 'mouseup', 'onDocumentMouseUp']
- ];
+ container.onmousedown = function (e) {
+ pointer.onContainerMouseDown(e);
+ };
+ container.onmousemove = function (e) {
+ pointer.onContainerMouseMove(e);
+ };
+ container.onclick = function (e) {
+ pointer.onContainerClick(e);
+ };
+ addEvent(container, 'mouseleave', pointer.onContainerMouseLeave);
+ addEvent(doc, 'mouseup', pointer.onDocumentMouseUp);
if (hasTouch) {
- events.push(
- [container, 'ontouchstart', 'onContainerTouchStart'],
- [container, 'ontouchmove', 'onContainerTouchMove'],
- [doc, 'touchend', 'onDocumentTouchEnd']
- );
- }
-
- each(events, function (eventConfig) {
-
- // First, create the callback function that in turn calls the method on Pointer
- pointer['_' + eventConfig[2]] = function (e) {
- pointer[eventConfig[2]](e);
+ container.ontouchstart = function (e) {
+ pointer.onContainerTouchStart(e);
};
-
- // Now attach the function, either as a direct property or through addEvent
- if (eventConfig[1].indexOf('on') === 0) {
- eventConfig[0][eventConfig[1]] = pointer['_' + eventConfig[2]];
- } else {
- addEvent(eventConfig[0], eventConfig[1], pointer['_' + eventConfig[2]]);
- }
- });
-
+ container.ontouchmove = function (e) {
+ pointer.onContainerTouchMove(e);
+ };
+ addEvent(doc, 'touchend', pointer.onDocumentTouchEnd);
+ }
},
/**
* Destroys the Pointer object and disconnects DOM events.
*/
destroy: function () {
- var pointer = this;
+ var prop;
- // Release all DOM events
- each(pointer._events, function (eventConfig) {
- if (eventConfig[1].indexOf('on') === 0) {
- eventConfig[0][eventConfig[1]] = null; // delete breaks oldIE
- } else {
- removeEvent(eventConfig[0], eventConfig[1], pointer['_' + eventConfig[2]]);
- }
- });
- delete pointer._events;
-
+ removeEvent(this.chart.container, 'mouseleave', this.onContainerMouseLeave);
+ removeEvent(doc, 'mouseup', this.onDocumentMouseUp);
+ removeEvent(doc, 'touchend', this.onDocumentTouchEnd);
+
// memory and CPU leak
- clearInterval(pointer.tooltipTimeout);
+ clearInterval(this.tooltipTimeout);
+
+ for (prop in this) {
+ this[prop] = null;
+ }
}
};
-/**
- * PointTrackerMixin
- */
+/* Support for touch devices */
+extend(Highcharts.Pointer.prototype, {
-var TrackerMixin = Highcharts.TrackerMixin = {
- drawTrackerPoint: function () {
- var series = this,
- chart = series.chart,
- pointer = chart.pointer,
- cursor = series.options.cursor,
- css = cursor && { cursor: cursor },
- onMouseOver = function (e) {
- var target = e.target,
- point;
+ /**
+ * Run translation operations
+ */
+ pinchTranslate: function (zoomHor, zoomVert, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) {
+ if (zoomHor) {
+ this.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
+ }
+ if (zoomVert) {
+ this.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
+ }
+ },
- if (chart.hoverSeries !== series) {
- series.onMouseOver();
+ /**
+ * Run translation operations for each direction (horizontal and vertical) independently
+ */
+ pinchTranslateDirection: function (horiz, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch, forcedScale) {
+ var chart = this.chart,
+ xy = horiz ? 'x' : 'y',
+ XY = horiz ? 'X' : 'Y',
+ sChartXY = 'chart' + XY,
+ wh = horiz ? 'width' : 'height',
+ plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')],
+ selectionWH,
+ selectionXY,
+ clipXY,
+ scale = forcedScale || 1,
+ inverted = chart.inverted,
+ bounds = chart.bounds[horiz ? 'h' : 'v'],
+ singleTouch = pinchDown.length === 1,
+ touch0Start = pinchDown[0][sChartXY],
+ touch0Now = touches[0][sChartXY],
+ touch1Start = !singleTouch && pinchDown[1][sChartXY],
+ touch1Now = !singleTouch && touches[1][sChartXY],
+ outOfBounds,
+ transformScale,
+ scaleKey,
+ setScale = function () {
+ if (!singleTouch && mathAbs(touch0Start - touch1Start) > 20) { // Don't zoom if fingers are too close on this axis
+ scale = forcedScale || mathAbs(touch0Now - touch1Now) / mathAbs(touch0Start - touch1Start);
}
- while (target && !point) {
- point = target.point;
- target = target.parentNode;
- }
- if (point !== UNDEFINED && point !== chart.hoverPoint) { // undefined on graph in scatterchart
- point.onMouseOver(e);
- }
+
+ clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start;
+ selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale;
};
- // Add reference to the point
- each(series.points, function (point) {
- if (point.graphic) {
- point.graphic.element.point = point;
+ // Set the scale, first pass
+ setScale();
+
+ selectionXY = clipXY; // the clip position (x or y) is altered if out of bounds, the selection position is not
+
+ // Out of bounds
+ if (selectionXY < bounds.min) {
+ selectionXY = bounds.min;
+ outOfBounds = true;
+ } else if (selectionXY + selectionWH > bounds.max) {
+ selectionXY = bounds.max - selectionWH;
+ outOfBounds = true;
+ }
+
+ // Is the chart dragged off its bounds, determined by dataMin and dataMax?
+ if (outOfBounds) {
+
+ // Modify the touchNow position in order to create an elastic drag movement. This indicates
+ // to the user that the chart is responsive but can't be dragged further.
+ touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]);
+ if (!singleTouch) {
+ touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]);
}
- if (point.dataLabel) {
- point.dataLabel.element.point = point;
- }
- });
- // Add the event listeners, we need to do this only once
- if (!series._hasTracking) {
- each(series.trackerGroups, function (key) {
- if (series[key]) { // we don't always have dataLabelsGroup
- series[key]
- .addClass(PREFIX + 'tracker')
- .on('mouseover', onMouseOver)
- .on('mouseout', function (e) { pointer.onTrackerMouseOut(e); })
- .css(css);
- if (hasTouch) {
- series[key].on('touchstart', onMouseOver);
- }
- }
- });
- series._hasTracking = true;
+ // Set the scale, second pass to adapt to the modified touchNow positions
+ setScale();
+
+ } else {
+ lastValidTouch[xy] = [touch0Now, touch1Now];
}
- },
+ // Set geometry for clipping, selection and transformation
+ if (!inverted) { // TODO: implement clipping for inverted charts
+ clip[xy] = clipXY - plotLeftTop;
+ clip[wh] = selectionWH;
+ }
+ scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY;
+ transformScale = inverted ? 1 / scale : scale;
+
+ selectionMarker[wh] = selectionWH;
+ selectionMarker[xy] = selectionXY;
+ transform[scaleKey] = scale;
+ transform['translate' + XY] = (transformScale * plotLeftTop) + (touch0Now - (transformScale * touch0Start));
+ },
+
/**
- * Draw the tracker object that sits above all data labels and markers to
- * track mouse events on the graph or points. For the line type charts
- * the tracker uses the same graphPath, but with a greater stroke width
- * for better control.
+ * Handle touch events with two touches
*/
- drawTrackerGraph: function () {
- var series = this,
- options = series.options,
- trackByArea = options.trackByArea,
- trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath),
- trackerPathLength = trackerPath.length,
- chart = series.chart,
- pointer = chart.pointer,
- 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();
- }
- };
+ pinch: function (e) {
- // Extend end points. A better way would be to use round linecaps,
- // but those are not clickable in VML.
- if (trackerPathLength && !trackByArea) {
- i = trackerPathLength + 1;
- while (i--) {
- if (trackerPath[i] === M) { // extend left side
- trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L);
+ var self = this,
+ chart = self.chart,
+ pinchDown = self.pinchDown,
+ followTouchMove = chart.tooltip && chart.tooltip.options.followTouchMove,
+ touches = e.touches,
+ touchesLength = touches.length,
+ lastValidTouch = self.lastValidTouch,
+ zoomHor = self.zoomHor || self.pinchHor,
+ zoomVert = self.zoomVert || self.pinchVert,
+ hasZoom = zoomHor || zoomVert,
+ selectionMarker = self.selectionMarker,
+ transform = {},
+ fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, PREFIX + 'tracker') &&
+ chart.runTrackerClick) || chart.runChartClick),
+ clip = {};
+
+ // On touch devices, only proceed to trigger click if a handler is defined
+ if ((hasZoom || followTouchMove) && !fireClickEvent) {
+ e.preventDefault();
+ }
+
+ // Normalize each touch
+ map(touches, function (e) {
+ return self.normalize(e);
+ });
+
+ // Register the touch start position
+ if (e.type === 'touchstart') {
+ each(touches, function (e, i) {
+ pinchDown[i] = { chartX: e.chartX, chartY: e.chartY };
+ });
+ lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && pinchDown[1].chartX];
+ lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && pinchDown[1].chartY];
+
+ // Identify the data bounds in pixels
+ each(chart.axes, function (axis) {
+ if (axis.zoomEnabled) {
+ var bounds = chart.bounds[axis.horiz ? 'h' : 'v'],
+ minPixelPadding = axis.minPixelPadding,
+ min = axis.toPixels(axis.dataMin),
+ max = axis.toPixels(axis.dataMax),
+ absMin = mathMin(min, max),
+ absMax = mathMax(min, max);
+
+ // Store the bounds for use in the touchmove handler
+ bounds.min = mathMin(axis.pos, absMin - minPixelPadding);
+ bounds.max = mathMax(axis.pos + axis.len, absMax + minPixelPadding);
}
- if ((i && trackerPath[i] === M) || i === trackerPathLength) { // extend right side
- trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]);
- }
+ });
+
+ // Event type is touchmove, handle panning and pinching
+ } else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first
+
+
+ // Set the marker
+ if (!selectionMarker) {
+ self.selectionMarker = selectionMarker = extend({
+ destroy: noop
+ }, chart.plotBox);
}
- }
+
+ self.pinchTranslate(zoomHor, zoomVert, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
- // handle single points
- for (i = 0; i < singlePoints.length; i++) {
- singlePoint = singlePoints[i];
- trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY,
- L, singlePoint.plotX + snap, singlePoint.plotY);
+ self.hasPinched = hasZoom;
+
+ // Scale and translate the groups to provide visual feedback during pinching
+ self.scaleGroups(transform, clip);
+
+ // Optionally move the tooltip on touchmove
+ if (!hasZoom && followTouchMove && touchesLength === 1) {
+ this.runPointActions(self.normalize(e));
+ }
}
+ },
- // draw the tracker
- if (tracker) {
- tracker.attr({ d: trackerPath });
+ onContainerTouchStart: function (e) {
+ var chart = this.chart;
- } else { // create
+ hoverChartIndex = chart.index;
- series.tracker = renderer.path(trackerPath)
- .attr({
- 'stroke-linejoin': 'round', // #1225
- visibility: series.visible ? VISIBLE : HIDDEN,
- stroke: TRACKER_FILL,
- fill: trackByArea ? TRACKER_FILL : NONE,
- 'stroke-width' : options.lineWidth + (trackByArea ? 0 : 2 * snap),
- zIndex: 2
- })
- .add(series.group);
+ if (e.touches.length === 1) {
- // The tracker is added to the series group, which is clipped, but is covered
- // by the marker group. So the marker group also needs to capture events.
- each([series.tracker, series.markerGroup], function (tracker) {
- tracker.addClass(PREFIX + 'tracker')
- .on('mouseover', onMouseOver)
- .on('mouseout', function (e) { pointer.onTrackerMouseOut(e); })
- .css(css);
+ e = this.normalize(e);
- if (hasTouch) {
- tracker.on('touchstart', onMouseOver);
- }
- });
+ if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
+
+ // Prevent the click pseudo event from firing unless it is set in the options
+ /*if (!chart.runChartClick) {
+ e.preventDefault();
+ }*/
+
+ // 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);
+ }
+ },
+
+ onContainerTouchMove: function (e) {
+ if (e.touches.length === 1 || e.touches.length === 2) {
+ this.pinch(e);
}
+ },
- }
-};
+ onDocumentTouchEnd: function (e) {
+ if (defined(hoverChartIndex)) {
+ charts[hoverChartIndex].pointer.drop(e);
+ }
+ }
+
+});
if (win.PointerEvent || win.MSPointerEvent) {
// The touches object keeps track of the points being touched at all times
- var touches = {};
-
- // Emulate a Webkit TouchList
- Pointer.prototype.getWebkitTouches = function () {
- var key, fake = [];
- fake.item = function (i) { return this[i]; };
- for (key in touches) {
- if (touches.hasOwnProperty(key)) {
- fake.push({
- pageX: touches[key].pageX,
- pageY: touches[key].pageY,
- target: touches[key].target
- });
+ var touches = {},
+ hasPointerEvent = !!win.PointerEvent,
+ getWebkitTouches = function () {
+ var key, fake = [];
+ fake.item = function (i) { return this[i]; };
+ for (key in touches) {
+ if (touches.hasOwnProperty(key)) {
+ fake.push({
+ pageX: touches[key].pageX,
+ pageY: touches[key].pageY,
+ target: touches[key].target
+ });
+ }
}
+ return fake;
+ },
+ translateMSPointer = function (e, method, wktype, callback) {
+ var p;
+ e = e.originalEvent || e;
+ if ((e.pointerType === 'touch' || e.pointerType === e.MSPOINTER_TYPE_TOUCH) && charts[hoverChartIndex]) {
+ callback(e);
+ p = charts[hoverChartIndex].pointer;
+ p[method]({
+ type: wktype,
+ target: e.currentTarget,
+ preventDefault: noop,
+ touches: getWebkitTouches()
+ });
+ }
+ };
+
+ /**
+ * Extend the Pointer prototype with methods for each event handler and more
+ */
+ extend(Pointer.prototype, {
+ onContainerPointerDown: function (e) {
+ translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function (e) {
+ touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY, target: e.currentTarget };
+ });
+ },
+ onContainerPointerMove: function (e) {
+ translateMSPointer(e, 'onContainerTouchMove', 'touchmove', function (e) {
+ touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY };
+ if (!touches[e.pointerId].target) {
+ touches[e.pointerId].target = e.currentTarget;
+ }
+ });
+ },
+ onDocumentPointerUp: function (e) {
+ translateMSPointer(e, 'onContainerTouchEnd', 'touchend', function (e) {
+ delete touches[e.pointerId];
+ });
+ },
+
+ /**
+ * Add or remove the MS Pointer specific events
+ */
+ batchMSEvents: function (fn) {
+ fn(this.chart.container, hasPointerEvent ? 'pointerdown' : 'MSPointerDown', this.onContainerPointerDown);
+ fn(this.chart.container, hasPointerEvent ? 'pointermove' : 'MSPointerMove', this.onContainerPointerMove);
+ fn(doc, hasPointerEvent ? 'pointerup' : 'MSPointerUp', this.onDocumentPointerUp);
}
- return fake;
- };
+ });
// Disable default IE actions for pinch and such on chart element
wrap(Pointer.prototype, 'init', function (proceed, chart, options) {
- chart.container.style["-ms-touch-action"] = chart.container.style["touch-action"] = "none";
+ css(chart.container, {
+ '-ms-touch-action': NONE,
+ 'touch-action': NONE
+ });
proceed.call(this, chart, options);
});
// Add IE specific touch events to chart
wrap(Pointer.prototype, 'setDOMEvents', function (proceed) {
- var pointer = this, eventmap;
- proceed.apply(this, Array.prototype.slice.call(arguments, 1));
- eventmap = [
- [this.chart.container, "PointerDown", "touchstart", "onContainerTouchStart", function (e) {
- touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY, target: e.currentTarget };
- }],
- [this.chart.container, "PointerMove", "touchmove", "onContainerTouchMove", function (e) {
- touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY };
- if (!touches[e.pointerId].target) {
- touches[e.pointerId].target = e.currentTarget;
- }
- }],
- [document, "PointerUp", "touchend", "onDocumentTouchEnd", function (e) {
- delete touches[e.pointerId];
- }]
- ];
-
- each(eventmap, function (eventConfig) {
- addEvent(eventConfig[0], window.PointerEvent ? eventConfig[1].toLowerCase() : "MS" + eventConfig[1], function (e) {
- e = e.originalEvent;
- if (e.pointerType === "touch" || e.pointerType === e.MSPOINTER_TYPE_TOUCH) {
- eventConfig[4](e);
-
- // This event corresponds to ontouchstart - call onContainerTouchStart
- pointer[eventConfig[3]]({
- type: eventConfig[2],
- target: e.currentTarget,
- preventDefault: noop,
- touches: pointer.getWebkitTouches()
- });
- }
- });
- });
-
+ proceed.apply(this);
+ this.batchMSEvents(addEvent);
});
-}
+ // Destroy MS events also
+ wrap(Pointer.prototype, 'destroy', function (proceed) {
+ this.batchMSEvents(removeEvent);
+ proceed.call(this);
+ });
+}
/**
* The overview of the chart's series
*/
var Legend = Highcharts.Legend = function (chart, options) {
this.init(chart, options);
@@ -10203,11 +10039,11 @@
legendItem = item.legendItem,
legendLine = item.legendLine,
legendSymbol = item.legendSymbol,
hiddenColor = legend.itemHiddenStyle.color,
textColor = visible ? options.itemStyle.color : hiddenColor,
- symbolColor = visible ? (item.legendColor || item.color) : hiddenColor,
+ symbolColor = visible ? (item.legendColor || item.color || '#CCC') : hiddenColor,
markerOptions = item.options && item.options.marker,
symbolAttr = {
stroke: symbolColor,
fill: symbolColor
},
@@ -10377,11 +10213,11 @@
bBox,
itemWidth,
li = item.legendItem,
series = item.series && item.series.drawLegendSymbol ? item.series : item,
seriesOptions = series.options,
- showCheckbox = seriesOptions && seriesOptions.showCheckbox,
+ showCheckbox = legend.createCheckboxForItem && seriesOptions && seriesOptions.showCheckbox,
useHTML = options.useHTML;
if (!li) { // generate it once, later move it
// Generate the group box
@@ -10405,59 +10241,20 @@
align: ltr ? 'left' : 'right',
zIndex: 2
})
.add(item.legendGroup);
- // Set the events on the item group, or in case of useHTML, the item itself (#1249)
- (useHTML ? li : item.legendGroup).on('mouseover', function () {
- item.setState(HOVER_STATE);
- li.css(legend.options.itemHoverStyle);
- })
- .on('mouseout', function () {
- li.css(item.visible ? itemStyle : itemHiddenStyle);
- item.setState();
- })
- .on('click', function (event) {
- var strLegendItemClick = 'legendItemClick',
- fnLegendItemClick = function () {
- item.setVisible();
- };
-
- // Pass over the click/touch event. #4.
- event = {
- browserEvent: event
- };
+ if (legend.setItemEvents) {
+ legend.setItemEvents(item, li, useHTML, itemStyle, itemHiddenStyle);
+ }
- // click the name or symbol
- if (item.firePointEvent) { // point
- item.firePointEvent(strLegendItemClick, event, fnLegendItemClick);
- } else {
- fireEvent(item, strLegendItemClick, event, fnLegendItemClick);
- }
- });
-
// Colorize the items
legend.colorizeItem(item, item.visible);
// add the HTML checkbox on top
if (showCheckbox) {
- item.checkbox = createElement('input', {
- type: 'checkbox',
- checked: item.selected,
- defaultChecked: item.selected // required by IE7
- }, options.itemCheckboxStyle, chart.container);
-
- addEvent(item.checkbox, 'click', function (event) {
- var target = event.target;
- fireEvent(item, 'checkboxClick', {
- checked: target.checked
- },
- function () {
- item.select();
- }
- );
- });
+ legend.createCheckboxForItem(item);
}
}
// calculate the positions for the next line
bBox = li.getBBox();
@@ -10467,11 +10264,11 @@
(showCheckbox ? 20 : 0);
legend.itemHeight = itemHeight = mathRound(item.legendItemHeight || bBox.height);
// if the item exceeds the width, start a new line
if (horizontal && legend.itemX - initialItemX + itemWidth >
- (widthOption || (chart.chartWidth - 2 * padding - initialItemX))) {
+ (widthOption || (chart.chartWidth - 2 * padding - initialItemX - options.x))) {
legend.itemX = initialItemX;
legend.itemY += itemMarginTop + legend.lastLineHeight + itemMarginBottom;
legend.lastLineHeight = 0; // reset for next line
}
@@ -10618,11 +10415,11 @@
.shadow(options.shadow);
box.isNew = true;
} else if (legendWidth > 0 && legendHeight > 0) {
box[box.isNew ? 'attr' : 'animate'](
- box.crisp(null, null, null, legendWidth, legendHeight)
+ box.crisp({ width: legendWidth, height: legendHeight })
);
box.isNew = false;
}
// hide the border if no items
@@ -10703,15 +10500,16 @@
// Fill pages with Y positions so that the top of each a legend item defines
// the scroll top for each page (#2098)
each(allItems, function (item, i) {
var y = item._legendItemPos[1],
- h = mathRound(item.legendItem.bBox.height),
+ h = mathRound(item.legendItem.getBBox().height),
len = pages.length;
- if (!len || (y - pages[len - 1] > clipHeight)) {
+ if (!len || (y - pages[len - 1] > clipHeight && (lastY || y) !== pages[len - 1])) {
pages.push(lastY || y);
+ len++;
}
if (i === allItems.length - 1 && y + h - pages[len - 1] > clipHeight) {
pages.push(y);
}
@@ -10910,14 +10708,15 @@
legendSymbol.isMarker = true;
}
}
};
-// Workaround for #2030, horizontal legend items not displaying in IE11 Preview.
-// TODO: When IE11 is released, check again for this bug, and remove the fix
-// or make a better one.
-if (/Trident\/7\.0/.test(userAgent)) {
+// Workaround for #2030, horizontal legend items not displaying in IE11 Preview,
+// and for #2580, a similar drawing flaw in Firefox 26.
+// TODO: Explore if there's a general cause for this. The problem may be related
+// to nested group elements, as the legend item texts are within 4 group elements.
+if (/Trident\/7\.0/.test(userAgent) || isFirefox) {
wrap(Legend.prototype, 'positionItem', function (proceed, item) {
var legend = this,
runPositionItem = function () { // If chart destroyed in sync, this is undefined (#2030)
if (item._legendItemPos) {
proceed.call(legend, item);
@@ -11218,11 +11017,11 @@
serie.redraw();
}
});
// move tooltip or reset
- if (pointer && pointer.reset) {
+ if (pointer) {
pointer.reset(true);
}
// redraw if canvas
renderer.draw();
@@ -11349,140 +11148,20 @@
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,
- btnOptions = chart.options.chart.resetZoomButton,
- theme = btnOptions.theme,
- states = theme.states,
- alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox';
-
- this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, function () { chart.zoomOut(); }, theme, states && states.hover)
- .attr({
- align: btnOptions.position.align,
- title: lang.resetZoomTitle
- })
- .add()
- .align(btnOptions.position, false, alignTo);
-
- },
-
- /**
- * Zoom out to 1:1
- */
- zoomOut: function () {
- var chart = this;
- fireEvent(chart, 'selection', { resetSelection: true }, function () {
- chart.zoom();
- });
- },
-
- /**
- * Zoom into a given portion of the chart given by axis coordinates
- * @param {Object} event
- */
- zoom: function (event) {
- var chart = this,
- hasZoomed,
- pointer = chart.pointer,
- displayButton = false,
- resetZoomButton;
-
- // If zoom is called with no arguments, reset the axes
- if (!event || event.resetSelection) {
- each(chart.axes, function (axis) {
- hasZoomed = axis.zoom();
- });
- } else { // else, zoom in on all axes
- each(event.xAxis.concat(event.yAxis), function (axisData) {
- var axis = axisData.axis,
- isXAxis = axis.isXAxis;
-
- // don't zoom more than minRange
- if (pointer[isXAxis ? 'zoomX' : 'zoomY'] || pointer[isXAxis ? 'pinchX' : 'pinchY']) {
- hasZoomed = axis.zoom(axisData.min, axisData.max);
- if (axis.displayBtn) {
- displayButton = true;
- }
- }
- });
- }
-
- // Show or hide the Reset zoom button
- resetZoomButton = chart.resetZoomButton;
- if (displayButton && !resetZoomButton) {
- chart.showResetZoom();
- } else if (!displayButton && isObject(resetZoomButton)) {
- chart.resetZoomButton = resetZoomButton.destroy();
- }
-
-
- // Redraw
- if (hasZoomed) {
- chart.redraw(
- pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation
- );
- }
- },
-
- /**
- * Pan the chart by dragging the mouse across the pane. This function is called
- * on mouse move, and the distance to pan is computed from chartX compared to
- * the first chartX position in the dragging operation.
- */
- pan: function (e, panning) {
-
- var chart = this,
- hoverPoints = chart.hoverPoints,
- doRedraw;
-
- // remove active points for shared tooltip
- if (hoverPoints) {
- each(hoverPoints, function (point) {
- 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'],
- 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;
-
- if (axis.series.length && newMin > mathMin(extremes.dataMin, extremes.min) && 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
- });
-
- if (doRedraw) {
- chart.redraw(false);
- }
- css(chart.container, { cursor: 'move' });
- },
-
- /**
* Show the title and subtitle of the chart
*
* @param titleOptions {Object} New title options
* @param subtitleOptions {Object} New subtitle options
*
*/
- setTitle: function (titleOptions, subtitleOptions) {
+ setTitle: function (titleOptions, subtitleOptions, redraw) {
var chart = this,
options = chart.options,
chartTitleOptions,
chartSubtitleOptions;
@@ -11517,23 +11196,24 @@
})
.css(chartTitleOptions.style)
.add();
}
});
- chart.layOutTitles();
+ chart.layOutTitles(redraw);
},
/**
* Lay out the chart titles and cache the full offset height for use in getMargins
*/
- layOutTitles: function () {
+ layOutTitles: function (redraw) {
var titleOffset = 0,
title = this.title,
subtitle = this.subtitle,
options = this.options,
titleOptions = options.title,
subtitleOptions = options.subtitle,
+ requiresDirtyBox,
autoWidth = this.spacingBox.width - 44; // 44 makes room for default context button
if (title) {
title
.css({ width: (titleOptions.width || autoWidth) + PX })
@@ -11556,27 +11236,42 @@
if (!subtitleOptions.floating && !subtitleOptions.verticalAlign) {
titleOffset = mathCeil(titleOffset + subtitle.getBBox().height);
}
}
+ requiresDirtyBox = this.titleOffset !== titleOffset;
this.titleOffset = titleOffset; // used in getMargins
+
+ if (!this.isDirtyBox && requiresDirtyBox) {
+ this.isDirtyBox = requiresDirtyBox;
+ // Redraw if necessary (#2719, #2744)
+ if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) {
+ this.redraw();
+ }
+ }
},
/**
* Get chart width and height according to options and container size
*/
getChartSize: function () {
var chart = this,
optionsChart = chart.options.chart,
+ widthOption = optionsChart.width,
+ heightOption = optionsChart.height,
renderTo = chart.renderToClone || chart.renderTo;
// get inner width and height from jQuery (#824)
- chart.containerWidth = adapterRun(renderTo, 'width');
- chart.containerHeight = adapterRun(renderTo, 'height');
+ if (!defined(widthOption)) {
+ chart.containerWidth = adapterRun(renderTo, 'width');
+ }
+ if (!defined(heightOption)) {
+ chart.containerHeight = adapterRun(renderTo, 'height');
+ }
- chart.chartWidth = mathMax(0, optionsChart.width || chart.containerWidth || 600); // #1393, 1460
- chart.chartHeight = mathMax(0, pick(optionsChart.height,
+ chart.chartWidth = mathMax(0, widthOption || chart.containerWidth || 600); // #1393, 1460
+ chart.chartHeight = mathMax(0, pick(heightOption,
// the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7:
chart.containerHeight > 19 ? chart.containerHeight : 400));
},
/**
@@ -11604,10 +11299,13 @@
css(clone, {
position: ABSOLUTE,
top: '-9999px',
display: 'block' // #833
});
+ if (clone.style.setProperty) { // #2631
+ clone.style.setProperty('display', 'block', 'important');
+ }
doc.body.appendChild(clone);
if (container) {
clone.appendChild(container);
}
}
@@ -11638,13 +11336,16 @@
// Display an error if the renderTo is wrong
if (!renderTo) {
error(13, true);
}
- // If the container already holds a chart, destroy it
+ // If the container already holds a chart, destroy it. The check for hasRendered is there
+ // because web pages that are saved to disk from the browser, will preserve the data-highcharts-chart
+ // attribute and the SVG contents, but not an interactive chart. So in this case,
+ // charts[oldChartIndex] will point to the wrong chart if any (#2609).
oldChartIndex = pInt(attr(renderTo, indexAttrName));
- if (!isNaN(oldChartIndex) && charts[oldChartIndex]) {
+ if (!isNaN(oldChartIndex) && charts[oldChartIndex] && charts[oldChartIndex].hasRendered) {
charts[oldChartIndex].destroy();
}
// Make a reference to the chart from the div
attr(renderTo, indexAttrName, chart.index);
@@ -11653,12 +11354,13 @@
renderTo.innerHTML = '';
// If the container doesn't have an offsetWidth, it has or is a child of a node
// that has display:none. We need to temporarily move it out to a visible
// state to determine the size, else the legend and tooltips won't render
- // properly
- if (!renderTo.offsetWidth) {
+ // properly. The allowClone option is used in sparklines as a micro optimization,
+ // saving about 1-2 ms each chart.
+ if (!optionsChart.skipClone && !renderTo.offsetWidth) {
chart.cloneRenderTo();
}
// get the width and height
chart.getChartSize();
@@ -11685,14 +11387,15 @@
);
// cache the cursor (#1650)
chart._cursor = container.style.cursor;
+ // Initialize the renderer
chart.renderer =
optionsChart.forExport ? // force SVG, used for SVG export
- new SVGRenderer(container, chartWidth, chartHeight, true) :
- new Renderer(container, chartWidth, chartHeight);
+ new SVGRenderer(container, chartWidth, chartHeight, optionsChart.style, true) :
+ new Renderer(container, chartWidth, chartHeight, optionsChart.style);
if (useCanVG) {
// If we need canvg library, extend and configure the renderer
// to get the tracker for translating mouse events
chart.renderer.create(chart, container, chartWidth, chartHeight);
@@ -11811,11 +11514,10 @@
};
// Width and height checks for display:none. Target is doc in IE8 and Opera,
// win in Firefox, Chrome and IE9.
if (!chart.hasUserSize && width && height && (target === win || target === doc)) {
-
if (width !== chart.containerWidth || height !== chart.containerHeight) {
clearTimeout(chart.reflowTimeout);
if (e) { // Called from window.resize
chart.reflowTimeout = setTimeout(doReflow, 100);
} else { // Called directly (#2224)
@@ -12040,16 +11742,17 @@
bgAttr['stroke-width'] = chartBorderWidth;
}
chart.chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn,
optionsChart.borderRadius, chartBorderWidth)
.attr(bgAttr)
+ .addClass(PREFIX + 'background')
.add()
.shadow(optionsChart.shadow);
} else { // resize
chartBackground.animate(
- chartBackground.crisp(null, null, null, chartWidth - mgn, chartHeight - mgn)
+ chartBackground.crisp({ width: chartWidth - mgn, height: chartHeight - mgn })
);
}
}
@@ -12090,16 +11793,17 @@
if (!plotBorder) {
chart.plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, -plotBorderWidth)
.attr({
stroke: optionsChart.plotBorderColor,
'stroke-width': plotBorderWidth,
+ fill: NONE,
zIndex: 1
})
.add();
} else {
plotBorder.animate(
- plotBorder.crisp(null, plotLeft, plotTop, plotWidth, plotHeight)
+ plotBorder.crisp({ x: plotLeft, y: plotTop, width: plotWidth, height: plotHeight })
);
}
}
// reset
@@ -12176,10 +11880,23 @@
}
});
},
/**
+ * Render series for the chart
+ */
+ renderSeries: function () {
+ each(this.series, function (serie) {
+ serie.translate();
+ if (serie.setTooltipPoints) {
+ serie.setTooltipPoints();
+ }
+ serie.render();
+ });
+ },
+
+ /**
* Render all graphics for the chart
*/
render: function () {
var chart = this,
axes = chart.axes,
@@ -12231,15 +11948,11 @@
if (!chart.seriesGroup) {
chart.seriesGroup = renderer.g('series-group')
.attr({ zIndex: 3 })
.add();
}
- each(chart.series, function (serie) {
- serie.translate();
- serie.setTooltipPoints();
- serie.render();
- });
+ chart.renderSeries();
// Labels
if (labels.items) {
each(labels.items, function (label) {
var style = extend(labels.style, label.style),
@@ -12419,11 +12132,13 @@
// the series data is indexed and cached in the xData and yData arrays, so we can access
// those before rendering. Used in Highstock.
fireEvent(chart, 'beforeRender');
// depends on inverted and on margins being set
- chart.pointer = new Pointer(chart, options);
+ if (Highcharts.Pointer) {
+ chart.pointer = new Pointer(chart, options);
+ }
chart.render();
// add canvas
chart.renderer.draw();
@@ -12436,11 +12151,11 @@
});
// If the chart was rendered outside the top container, put it back in
chart.cloneRenderTo(true);
-
+
fireEvent(chart, 'load');
},
/**
@@ -12670,90 +12385,13 @@
series: point.series,
point: point,
percentage: point.percentage,
total: point.total || point.stackTotal
};
- },
+ },
/**
- * Toggle the selection status of a point
- * @param {Boolean} selected Whether to select or unselect the point.
- * @param {Boolean} accumulate Whether to add to the previous selection. By default,
- * this happens if the control key (Cmd on Mac) was pressed during clicking.
- */
- select: function (selected, accumulate) {
- var point = this,
- series = point.series,
- chart = series.chart;
-
- selected = pick(selected, !point.selected);
-
- // fire the event with the defalut handler
- point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () {
- point.selected = point.options.selected = selected;
- series.options.data[inArray(point, series.data)] = point.options;
-
- point.setState(selected && SELECT_STATE);
-
- // unselect all other points unless Ctrl or Cmd + click
- if (!accumulate) {
- each(chart.getSelectedPoints(), function (loopPoint) {
- if (loopPoint.selected && loopPoint !== point) {
- loopPoint.selected = loopPoint.options.selected = false;
- series.options.data[inArray(loopPoint, series.data)] = loopPoint.options;
- loopPoint.setState(NORMAL_STATE);
- loopPoint.firePointEvent('unselect');
- }
- });
- }
- });
- },
-
- /**
- * Runs on mouse over the point
- */
- onMouseOver: function (e) {
- var point = this,
- series = point.series,
- chart = series.chart,
- tooltip = chart.tooltip,
- hoverPoint = chart.hoverPoint;
-
- // set normal state to previous series
- if (hoverPoint && hoverPoint !== point) {
- hoverPoint.onMouseOut();
- }
-
- // trigger the event
- point.firePointEvent('mouseOver');
-
- // update the tooltip
- if (tooltip && (!tooltip.shared || series.noSharedTooltip)) {
- tooltip.refresh(point, e);
- }
-
- // hover this
- point.setState(HOVER_STATE);
- chart.hoverPoint = point;
- },
-
- /**
- * Runs on mouse out from the point
- */
- onMouseOut: function () {
- var chart = this.series.chart,
- hoverPoints = chart.hoverPoints;
-
- if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887
- this.firePointEvent('mouseOut');
-
- this.setState();
- chart.hoverPoint = null;
- }
- },
-
- /**
* Extendable method for formatting each point's tooltip line
*
* @return {String} A string to be concatenated in to the common tooltip text
*/
tooltipFormatter: function (pointFormat) {
@@ -12776,154 +12414,10 @@
return format(pointFormat, {
point: this,
series: this.series
});
- },
-
- /**
- * Fire an event on the Point object. Must not be renamed to fireEvent, as this
- * causes a name clash in MooTools
- * @param {String} eventType
- * @param {Object} eventArgs Additional event arguments
- * @param {Function} defaultFunction Default event handler
- */
- firePointEvent: function (eventType, eventArgs, defaultFunction) {
- var point = this,
- series = this.series,
- seriesOptions = series.options;
-
- // load event handlers on demand to save time on mouseover/out
- if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) {
- this.importEvents();
- }
-
- // add default handler if in selection mode
- if (eventType === 'click' && seriesOptions.allowPointSelect) {
- defaultFunction = function (event) {
- // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera
- point.select(null, event.ctrlKey || event.metaKey || event.shiftKey);
- };
- }
-
- fireEvent(this, eventType, eventArgs, defaultFunction);
- },
- /**
- * Import events from the series' and point's options. Only do it on
- * demand, to save processing time on hovering.
- */
- importEvents: function () {
- if (!this.hasImportedEvents) {
- var point = this,
- options = merge(point.series.options.point, point.options),
- events = options.events,
- eventType;
-
- point.events = events;
-
- for (eventType in events) {
- addEvent(point, eventType, events[eventType]);
- }
- this.hasImportedEvents = true;
-
- }
- },
-
- /**
- * Set the point's state
- * @param {String} state
- */
- setState: function (state, move) {
- var point = this,
- plotX = point.plotX,
- plotY = point.plotY,
- series = point.series,
- stateOptions = series.options.states,
- markerOptions = defaultPlotOptions[series.type].marker && series.options.marker,
- normalDisabled = markerOptions && !markerOptions.enabled,
- markerStateOptions = markerOptions && markerOptions.states[state],
- stateDisabled = markerStateOptions && markerStateOptions.enabled === false,
- stateMarkerGraphic = series.stateMarkerGraphic,
- pointMarker = point.marker || {},
- chart = series.chart,
- radius,
- newSymbol,
- pointAttr = point.pointAttr;
-
- state = state || NORMAL_STATE; // empty string
- move = move && stateMarkerGraphic;
-
- if (
- // already has this state
- (state === point.state && !move) ||
- // selected points don't respond to hover
- (point.selected && state !== SELECT_STATE) ||
- // series' state options is disabled
- (stateOptions[state] && stateOptions[state].enabled === false) ||
- // general point marker's state options is disabled
- (state && (stateDisabled || (normalDisabled && !markerStateOptions.enabled))) ||
- // individual point marker's state options is disabled
- (state && pointMarker.states && pointMarker.states[state] && pointMarker.states[state].enabled === false) // #1610
-
- ) {
- return;
- }
-
-
- // apply hover styles to the existing point
- if (point.graphic) {
- radius = markerOptions && point.graphic.symbolName && pointAttr[state].r;
- point.graphic.attr(merge(
- pointAttr[state],
- radius ? { // new symbol attributes (#507, #612)
- x: plotX - radius,
- y: plotY - radius,
- width: 2 * radius,
- height: 2 * radius
- } : {}
- ));
- } else {
- // if a graphic is not applied to each point in the normal state, create a shared
- // graphic for the hover state
- if (state && markerStateOptions) {
- radius = markerStateOptions.radius;
- newSymbol = pointMarker.symbol || series.symbol;
-
- // If the point has another symbol than the previous one, throw away the
- // state marker graphic and force a new one (#1459)
- if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) {
- stateMarkerGraphic = stateMarkerGraphic.destroy();
- }
-
- // Add a new state marker graphic
- if (!stateMarkerGraphic) {
- series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol(
- newSymbol,
- plotX - radius,
- plotY - radius,
- 2 * radius,
- 2 * radius
- )
- .attr(pointAttr[state])
- .add(series.markerGroup);
- stateMarkerGraphic.currentSymbol = newSymbol;
-
- // Move the existing graphic
- } else {
- stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054
- x: plotX - radius,
- y: plotY - radius
- });
- }
- }
-
- if (stateMarkerGraphic) {
- stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? 'show' : 'hide'](); // #2450
- }
- }
-
- point.state = state;
}
};/**
* @classDescription The base function which all other series types inherit from. The data in the series is stored
* in various arrays.
*
@@ -13258,120 +12752,139 @@
/**
* Replace the series data with a new set of data
* @param {Object} data
* @param {Object} redraw
*/
- setData: function (data, redraw) {
+ setData: function (data, redraw, animation, updatePoints) {
var series = this,
oldData = series.points,
+ oldDataLength = (oldData && oldData.length) || 0,
+ dataLength,
options = series.options,
chart = series.chart,
firstPoint = null,
xAxis = series.xAxis,
hasCategories = xAxis && !!xAxis.categories,
- i;
-
- // reset properties
- series.xIncrement = null;
- series.pointRange = hasCategories ? 1 : options.pointRange;
-
- series.colorCounter = 0; // for series with colorByPoint (#1547)
- data = data || [];
-
- // parallel arrays
- var dataLength = data.length,
+ tooltipPoints = series.tooltipPoints,
+ i,
turboThreshold = options.turboThreshold,
pt,
xData = this.xData,
yData = this.yData,
pointArrayMap = series.pointArrayMap,
valueCount = pointArrayMap && pointArrayMap.length;
- each(this.parallelArrays, function (key) {
- series[key + 'Data'].length = 0;
- });
+ data = data || [];
+ dataLength = data.length;
+ redraw = pick(redraw, true);
- // 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 (turboThreshold && dataLength > turboThreshold) {
+ // If the point count is the same as is was, just run Point.update which is
+ // cheaper, allows animation, and keeps references to points.
+ if (updatePoints !== false && dataLength && oldDataLength === dataLength && !series.cropped && !series.hasGroupedData) {
+ each(data, function (point, i) {
+ oldData[i].update(point, false);
+ });
- // find the first non-null point
- i = 0;
- while (firstPoint === null && i < dataLength) {
- firstPoint = data[i];
- i++;
- }
+ } else {
+ // Reset properties
+ series.xIncrement = null;
+ series.pointRange = hasCategories ? 1 : options.pointRange;
- if (isNumber(firstPoint)) { // assume all points are numbers
- var x = pick(options.pointStart, 0),
- pointInterval = pick(options.pointInterval, 1);
+ series.colorCounter = 0; // for series with colorByPoint (#1547)
+
+ // Update parallel arrays
+ each(this.parallelArrays, function (key) {
+ series[key + 'Data'].length = 0;
+ });
- for (i = 0; i < dataLength; i++) {
- xData[i] = x;
- yData[i] = data[i];
- x += pointInterval;
+ // 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 (turboThreshold && dataLength > turboThreshold) {
+
+ // find the first non-null point
+ i = 0;
+ while (firstPoint === null && i < dataLength) {
+ firstPoint = data[i];
+ i++;
}
- series.xIncrement = x;
- } else if (isArray(firstPoint)) { // assume all points are arrays
- if (valueCount) { // [x, low, high] or [x, o, h, l, c]
+
+
+ if (isNumber(firstPoint)) { // assume all points are numbers
+ var x = pick(options.pointStart, 0),
+ pointInterval = pick(options.pointInterval, 1);
+
for (i = 0; i < dataLength; i++) {
- pt = data[i];
- xData[i] = pt[0];
- yData[i] = pt.slice(1, valueCount + 1);
+ xData[i] = x;
+ yData[i] = data[i];
+ x += pointInterval;
}
- } else { // [x, y]
- for (i = 0; i < dataLength; i++) {
- pt = data[i];
- xData[i] = pt[0];
- yData[i] = pt[1];
+ series.xIncrement = x;
+ } else if (isArray(firstPoint)) { // assume all points are arrays
+ if (valueCount) { // [x, low, high] or [x, o, h, l, c]
+ for (i = 0; i < dataLength; i++) {
+ pt = data[i];
+ xData[i] = pt[0];
+ yData[i] = pt.slice(1, valueCount + 1);
+ }
+ } else { // [x, y]
+ for (i = 0; i < dataLength; i++) {
+ pt = data[i];
+ xData[i] = pt[0];
+ yData[i] = pt[1];
+ }
}
+ } else {
+ error(12); // Highcharts expects configs to be numbers or arrays in turbo mode
}
} else {
- error(12); // Highcharts expects configs to be numbers or arrays in turbo mode
- }
- } else {
- for (i = 0; i < dataLength; i++) {
- if (data[i] !== UNDEFINED) { // stray commas in oldIE
- pt = { series: series };
- series.pointClass.prototype.applyOptions.apply(pt, [data[i]]);
- series.updateParallelArrays(pt, i);
- if (hasCategories && pt.name) {
- xAxis.names[pt.x] = pt.name; // #2046
+ for (i = 0; i < dataLength; i++) {
+ if (data[i] !== UNDEFINED) { // stray commas in oldIE
+ pt = { series: series };
+ series.pointClass.prototype.applyOptions.apply(pt, [data[i]]);
+ series.updateParallelArrays(pt, i);
+ if (hasCategories && pt.name) {
+ xAxis.names[pt.x] = pt.name; // #2046
+ }
}
}
}
- }
- // Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON
- if (isString(yData[0])) {
- error(14, true);
- }
+ // Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON
+ if (isString(yData[0])) {
+ error(14, true);
+ }
- series.data = [];
- series.options.data = data;
- //series.zData = zData;
+ series.data = [];
+ series.options.data = data;
+ //series.zData = zData;
- // destroy old points
- i = (oldData && oldData.length) || 0;
- while (i--) {
- if (oldData[i] && oldData[i].destroy) {
- oldData[i].destroy();
+ // destroy old points
+ i = oldDataLength;
+ while (i--) {
+ if (oldData[i] && oldData[i].destroy) {
+ oldData[i].destroy();
+ }
}
- }
+ if (tooltipPoints) { // #2594
+ tooltipPoints.length = 0;
+ }
- // reset minRange (#878)
- if (xAxis) {
- xAxis.minRange = xAxis.userMinRange;
+ // reset minRange (#878)
+ if (xAxis) {
+ xAxis.minRange = xAxis.userMinRange;
+ }
+
+ // redraw
+ series.isDirty = series.isDirtyData = chart.isDirtyBox = true;
+ animation = false;
}
- // redraw
- series.isDirty = series.isDirtyData = chart.isDirtyBox = true;
- if (pick(redraw, true)) {
- chart.redraw(false);
+ if (redraw) {
+ chart.redraw(animation);
}
},
/**
* Process the data by cropping away unused data points if the series is longer
@@ -13542,133 +13055,10 @@
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,
- stackedYData = [],
- yDataLength = yData.length,
- seriesOptions = series.options,
- threshold = seriesOptions.threshold,
- stackOption = seriesOptions.stack,
- stacking = seriesOptions.stacking,
- stackKey = series.stackKey,
- negKey = '-' + stackKey,
- negStacks = series.negStacks,
- yAxis = series.yAxis,
- stacks = yAxis.stacks,
- oldStacks = yAxis.oldStacks,
- isNegative,
- stack,
- other,
- 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 = negStacks && y < threshold;
- key = isNegative ? negKey : stackKey;
-
- // Create empty object for this stack if it doesn't exist yet
- if (!stacks[key]) {
- stacks[key] = {};
- }
-
- // Initialize StackItem for this x
- if (!stacks[key][x]) {
- if (oldStacks[key] && oldStacks[key][x]) {
- stacks[key][x] = oldStacks[key][x];
- stacks[key][x].total = null;
- } else {
- 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];
- stack.points[series.index] = [stack.cum || 0];
-
- // Add value to the stack total
- if (stacking === 'percent') {
-
- // Percent stacked column, totals are the same for the positive and negative stacks
- other = isNegative ? stackKey : negKey;
- if (negStacks && stacks[other] && stacks[other][x]) {
- other = stacks[other][x];
- stack.total = other.total = mathMax(other.total, stack.total) + mathAbs(y) || 0;
-
- // Percent stacked areas
- } else {
- stack.total = correctFloat(stack.total + (mathAbs(y) || 0));
- }
- } else {
- stack.total = correctFloat(stack.total + (y || 0));
- }
-
- stack.cum = (stack.cum || 0) + (y || 0);
-
- stack.points[series.index].push(stack.cum);
- stackedYData[i] = stack.cum;
-
- }
-
- if (stacking === 'percent') {
- yAxis.usePercentage = true;
- }
-
- this.stackedYData = stackedYData; // To be used in getExtremes
-
- // Reset old stacks
- yAxis.oldStacks = {};
- },
-
- /**
- * Iterate over all stacks and compute the absolute values to percent
- */
- setPercentStacks: function () {
- var series = this,
- stackKey = series.stackKey,
- stacks = series.yAxis.stacks;
-
- each([stackKey, '-' + stackKey], function (key) {
- var i = series.xData.length,
- x,
- stack,
- pointExtremes,
- totalFactor;
-
- while (i--) {
- x = series.xData[i];
- stack = stacks[key] && stacks[key][x];
- pointExtremes = stack && stack.points[series.index];
- if (pointExtremes) {
- totalFactor = stack.total ? 100 / stack.total : 0;
- pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor); // Y bottom value
- pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor); // Y value
- series.stackedYData[i] = pointExtremes[1];
- }
- }
- });
- },
-
- /**
* Calculate Y extremes for visible data
*/
getExtremes: function (yData) {
var xAxis = this.xAxis,
yAxis = this.yAxis,
@@ -13776,11 +13166,11 @@
if (yAxis.isLog && yBottom <= 0) { // #1200, #1232
yBottom = null;
}
point.total = point.stackTotal = pointStack.total;
- point.percentage = stacking === 'percent' && (point.y / pointStack.total * 100);
+ point.percentage = pointStack.total && (point.y / pointStack.total * 100);
point.stackY = yValue;
// Place the stack label
pointStack.setOffset(series.pointXOffset || 0, series.barW || 0);
@@ -13815,177 +13205,12 @@
}
// now that we have the cropped data, build the segments
series.getSegments();
},
- /**
- * Memoize tooltip texts and positions
- */
- setTooltipPoints: function (renew) {
- var series = this,
- points = [],
- pointsLength,
- low,
- high,
- xAxis = series.xAxis,
- xExtremes = xAxis && xAxis.getExtremes(),
- axisLength = xAxis ? (xAxis.tooltipLen || xAxis.len) : series.chart.plotSizeX, // tooltipLen and tooltipPosName used in polar
- point,
- pointX,
- 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) {
- return;
- }
-
- // renew
- if (renew) {
- series.tooltipPoints = null;
- }
-
- // concat segments to overcome null values
- each(series.segments || series.points, function (segment) {
- points = points.concat(segment);
- });
-
- // 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];
- pointX = point.x;
- if (pointX >= xExtremes.min && pointX <= xExtremes.max) { // #1149
- nextPoint = points[i + 1];
-
- // Set this range's low to the last range's high plus one
- low = high === UNDEFINED ? 0 : high + 1;
- // Now find the new high
- high = points[i + 1] ?
- mathMin(mathMax(0, mathFloor( // #2070
- (point.clientX + (nextPoint ? (nextPoint.wrappedClientX || nextPoint.clientX) : axisLength)) / 2
- )), axisLength) :
- axisLength;
-
- while (low >= 0 && low <= high) {
- tooltipPoints[low++] = point;
- }
- }
- }
- series.tooltipPoints = tooltipPoints;
- },
-
/**
- * Format the header of the tooltip
- */
- tooltipHeaderFormatter: function (point) {
- var series = this,
- tooltipOptions = series.tooltipOptions,
- dateTimeLabelFormats = tooltipOptions.dateTimeLabelFormats,
- xDateFormat = tooltipOptions.xDateFormat,
- xAxis = series.xAxis,
- isDateTime = xAxis && xAxis.options.type === 'datetime',
- headerFormat = tooltipOptions.headerFormat,
- closestPointRange = xAxis && xAxis.closestPointRange,
- n;
-
- // Guess the best date format based on the closest point distance (#568)
- if (isDateTime && !xDateFormat) {
- if (closestPointRange) {
- for (n in timeUnits) {
- if (timeUnits[n] >= closestPointRange) {
- xDateFormat = dateTimeLabelFormats[n];
- break;
- }
- }
- } else {
- xDateFormat = dateTimeLabelFormats.day;
- }
-
- xDateFormat = xDateFormat || dateTimeLabelFormats.year; // #2546, 2581
-
- }
-
- // Insert the header date format if any
- if (isDateTime && xDateFormat && isNumber(point.key)) {
- headerFormat = headerFormat.replace('{point.key}', '{point.key:' + xDateFormat + '}');
- }
-
- return format(headerFormat, {
- point: point,
- series: series
- });
- },
-
- /**
- * Series mouse over handler
- */
- onMouseOver: function () {
- var series = this,
- chart = series.chart,
- hoverSeries = chart.hoverSeries;
-
- // set normal state to previous series
- if (hoverSeries && hoverSeries !== series) {
- hoverSeries.onMouseOut();
- }
-
- // trigger the event, but to save processing time,
- // only if defined
- if (series.options.events.mouseOver) {
- fireEvent(series, 'mouseOver');
- }
-
- // hover this
- series.setState(HOVER_STATE);
- chart.hoverSeries = series;
- },
-
- /**
- * Series mouse out handler
- */
- onMouseOut: function () {
- // trigger the event only if listeners exist
- var series = this,
- options = series.options,
- chart = series.chart,
- tooltip = chart.tooltip,
- hoverPoint = chart.hoverPoint;
-
- // trigger mouse out on the point, which must be in this series
- if (hoverPoint) {
- hoverPoint.onMouseOut();
- }
-
- // fire the mouse out event
- if (series && options.events.mouseOut) {
- fireEvent(series, 'mouseOut');
- }
-
-
- // hide the tooltip
- if (tooltip && !options.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) {
- tooltip.hide();
- }
-
- // set normal state
- series.setState();
- chart.hoverSeries = null;
- },
-
- /**
* Animate in the series
*/
animate: function (init) {
var series = this,
chart = series.chart,
@@ -14086,10 +13311,11 @@
symbol,
isImage,
graphic,
options = series.options,
seriesMarkerOptions = options.marker,
+ seriesPointAttr = series.pointAttr[''],
pointMarkerOptions,
enabled,
isInside,
markerGroup = series.markerGroup;
@@ -14107,19 +13333,19 @@
// only draw the point if y is defined
if (enabled && plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) {
// shortcuts
- pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE];
+ pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] || seriesPointAttr;
radius = pointAttr.r;
symbol = pick(pointMarkerOptions.symbol, series.symbol);
isImage = symbol.indexOf('url') === 0;
if (graphic) { // update
graphic
.attr({ // Since the marker group isn't clipped, each individual marker must be toggled
- visibility: isInside ? (hasSVG ? 'inherit' : VISIBLE) : HIDDEN
+ visibility: isInside ? 'inherit' : HIDDEN
})
.animate(extend({
x: plotX - radius,
y: plotY - radius
}, graphic.symbolName ? { // don't apply to image symbols #507
@@ -14194,14 +13420,15 @@
i,
point,
seriesPointAttr = [],
pointAttr,
pointAttrToOptions = series.pointAttrToOptions,
- hasPointSpecificOptions,
+ hasPointSpecificOptions = series.hasPointSpecificOptions,
negativeColor = seriesOptions.negativeColor,
defaultLineColor = normalOptions.lineColor,
defaultFillColor = normalOptions.fillColor,
+ turboThreshold = seriesOptions.turboThreshold,
attr,
key;
// series type specific modifications
if (seriesOptions.marker) { // line, spline, area, areaspline, scatter
@@ -14233,83 +13460,83 @@
// Generate the point-specific attribute collections if specific point
// options are given. If not, create a referance to the series wide point
// attributes
i = points.length;
- while (i--) {
- point = points[i];
- normalOptions = (point.options && point.options.marker) || point.options;
- if (normalOptions && normalOptions.enabled === false) {
- normalOptions.radius = 0;
- }
+ if (!turboThreshold || i < turboThreshold || hasPointSpecificOptions) {
+ while (i--) {
+ point = points[i];
+ normalOptions = (point.options && point.options.marker) || point.options;
+ if (normalOptions && normalOptions.enabled === false) {
+ normalOptions.radius = 0;
+ }
- if (point.negative && negativeColor) {
- point.color = point.fillColor = negativeColor;
- }
+ if (point.negative && negativeColor) {
+ point.color = point.fillColor = negativeColor;
+ }
- hasPointSpecificOptions = seriesOptions.colorByPoint || point.color; // #868
+ hasPointSpecificOptions = seriesOptions.colorByPoint || point.color; // #868
- // check if the point has specific visual options
- if (point.options) {
- for (key in pointAttrToOptions) {
- if (defined(normalOptions[pointAttrToOptions[key]])) {
- hasPointSpecificOptions = true;
+ // check if the point has specific visual options
+ if (point.options) {
+ for (key in pointAttrToOptions) {
+ if (defined(normalOptions[pointAttrToOptions[key]])) {
+ hasPointSpecificOptions = true;
+ }
}
}
- }
- // a specific marker config object is defined for the individual point:
- // create it's own attribute collection
- if (hasPointSpecificOptions) {
- normalOptions = normalOptions || {};
- pointAttr = [];
- stateOptions = normalOptions.states || {}; // reassign for individual point
- pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {};
+ // a specific marker config object is defined for the individual point:
+ // create it's own attribute collection
+ if (hasPointSpecificOptions) {
+ normalOptions = normalOptions || {};
+ pointAttr = [];
+ stateOptions = normalOptions.states || {}; // reassign for individual point
+ pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {};
- // Handle colors for column and pies
- if (!seriesOptions.marker) { // column, bar, point
- // if no hover color is given, brighten the normal color
- pointStateOptionsHover.color =
- Color(pointStateOptionsHover.color || point.color)
- .brighten(pointStateOptionsHover.brightness ||
- stateOptionsHover.brightness).get();
+ // Handle colors for column and pies
+ if (!seriesOptions.marker) { // column, bar, point
+ // If no hover color is given, brighten the normal color. #1619, #2579
+ pointStateOptionsHover.color = pointStateOptionsHover.color || (!point.options.color && stateOptionsHover.color) ||
+ Color(point.color)
+ .brighten(pointStateOptionsHover.brightness || stateOptionsHover.brightness)
+ .get();
+ }
- }
+ // normal point state inherits series wide normal state
+ attr = { color: point.color }; // #868
+ if (!defaultFillColor) { // Individual point color or negative color markers (#2219)
+ attr.fillColor = point.color;
+ }
+ if (!defaultLineColor) {
+ attr.lineColor = point.color; // Bubbles take point color, line markers use white
+ }
+ pointAttr[NORMAL_STATE] = series.convertAttribs(extend(attr, normalOptions), seriesPointAttr[NORMAL_STATE]);
- // normal point state inherits series wide normal state
- attr = { color: point.color }; // #868
- if (!defaultFillColor) { // Individual point color or negative color markers (#2219)
- attr.fillColor = point.color;
- }
- if (!defaultLineColor) {
- attr.lineColor = point.color; // Bubbles take point color, line markers use white
- }
- pointAttr[NORMAL_STATE] = series.convertAttribs(extend(attr, normalOptions), seriesPointAttr[NORMAL_STATE]);
+ // inherit from point normal and series hover
+ pointAttr[HOVER_STATE] = series.convertAttribs(
+ stateOptions[HOVER_STATE],
+ seriesPointAttr[HOVER_STATE],
+ pointAttr[NORMAL_STATE]
+ );
- // inherit from point normal and series hover
- pointAttr[HOVER_STATE] = series.convertAttribs(
- stateOptions[HOVER_STATE],
- seriesPointAttr[HOVER_STATE],
- pointAttr[NORMAL_STATE]
- );
+ // inherit from point normal and series hover
+ pointAttr[SELECT_STATE] = series.convertAttribs(
+ stateOptions[SELECT_STATE],
+ seriesPointAttr[SELECT_STATE],
+ pointAttr[NORMAL_STATE]
+ );
- // inherit from point normal and series hover
- pointAttr[SELECT_STATE] = series.convertAttribs(
- stateOptions[SELECT_STATE],
- seriesPointAttr[SELECT_STATE],
- pointAttr[NORMAL_STATE]
- );
+ // no marker config object is created: copy a reference to the series-wide
+ // attribute collection
+ } else {
+ pointAttr = seriesPointAttr;
+ }
- // no marker config object is created: copy a reference to the series-wide
- // attribute collection
- } else {
- pointAttr = seriesPointAttr;
+ point.pointAttr = pointAttr;
}
-
- point.pointAttr = pointAttr;
-
}
},
/**
* Clear DOM objects and free up memory
@@ -14502,10 +13729,11 @@
} else if (lineWidth && graphPath.length) { // #1487
attribs = {
stroke: prop[1],
'stroke-width': lineWidth,
+ fill: NONE,
zIndex: 1 // #1069
};
if (dashStyle) {
attribs.dashstyle = dashStyle;
} else if (roundCap) {
@@ -14741,11 +13969,11 @@
series.drawPoints();
}
// draw the mouse tracking area
- if (series.options.enableMouseTracking !== false) {
+ if (series.drawTracker && series.options.enableMouseTracking !== false) {
series.drawTracker();
}
// Handle inverted series and tracker groups
if (chart.inverted) {
@@ -14795,155 +14023,309 @@
});
}
series.translate();
series.setTooltipPoints(true);
-
series.render();
+
if (wasDirtyData) {
fireEvent(series, 'updatedData');
}
- },
+ }
+}; // end Series prototype
- /**
- * Set the state of the graph
- */
- setState: function (state) {
- var series = this,
- options = series.options,
- graph = series.graph,
- graphNeg = series.graphNeg,
- stateOptions = options.states,
- lineWidth = options.lineWidth,
- attribs;
+/**
+ * The class for stack items
+ */
+function StackItem(axis, options, isNegative, x, stackOption, stacking) {
+
+ var inverted = axis.chart.inverted;
- state = state || NORMAL_STATE;
+ this.axis = axis;
- if (series.state !== state) {
- series.state = state;
+ // Tells if the stack is negative
+ this.isNegative = isNegative;
- if (stateOptions[state] && stateOptions[state].enabled === false) {
- return;
- }
+ // Save the options to be able to style the label
+ this.options = options;
- if (state) {
- lineWidth = stateOptions[state].lineWidth || lineWidth + 1;
- }
+ // Save the x value to be able to position the label later
+ this.x = x;
- if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML
- attribs = {
- 'stroke-width': lineWidth
- };
- // use attr because animate will cause any other animation on the graph to stop
- graph.attr(attribs);
- if (graphNeg) {
- graphNeg.attr(attribs);
- }
- }
+ // Initialize total value
+ this.total = null;
+
+ // 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
+ // if the chart is inverted or not.
+ // First test the user supplied value, then use the dynamic.
+ this.alignOptions = {
+ align: options.align || (inverted ? (isNegative ? 'left' : 'right') : 'center'),
+ verticalAlign: options.verticalAlign || (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')),
+ y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)),
+ x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0)
+ };
+
+ this.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center');
+}
+
+StackItem.prototype = {
+ destroy: function () {
+ destroyObjectProperties(this, this.axis);
+ },
+
+ /**
+ * Renders the stack total label and adds it to the stack label group.
+ */
+ render: function (group) {
+ var options = this.options,
+ 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
+ if (this.label) {
+ this.label.attr({text: str, visibility: HIDDEN});
+ // Create new label
+ } else {
+ this.label =
+ this.axis.chart.renderer.text(str, 0, 0, options.useHTML) // dummy positions, actual position updated with setOffset method in columnseries
+ .css(options.style) // apply style
+ .attr({
+ align: this.textAlign, // fix the text-anchor
+ rotation: options.rotation, // rotation
+ visibility: HIDDEN // hidden until setOffset is called
+ })
+ .add(group); // add to the labels-group
}
},
/**
- * Set the visibility of the graph
- *
- * @param vis {Boolean} True to show the series, false to hide. If UNDEFINED,
- * the visibility is toggled.
+ * Sets the offset that the stack has from the x value and repositions the label.
*/
- setVisible: function (vis, redraw) {
- var series = this,
- chart = series.chart,
- legendItem = series.legendItem,
- showOrHide,
- ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries,
- oldVisibility = series.visible;
+ setOffset: function (xOffset, xWidth) {
+ var stackItem = this,
+ axis = stackItem.axis,
+ chart = axis.chart,
+ inverted = chart.inverted,
+ neg = this.isNegative, // special treatment is needed for negative stacks
+ y = axis.translate(this.percent ? 100 : this.total, 0, 0, 0, 1), // stack value translated mapped to chart coordinates
+ yZero = axis.translate(0), // stack origin
+ h = mathAbs(y - yZero), // stack height
+ x = chart.xAxis[0].translate(this.x) + xOffset, // stack x position
+ plotHeight = chart.plotHeight,
+ stackBox = { // this is the box for the complete stack
+ x: inverted ? (neg ? y : y - h) : x,
+ y: inverted ? plotHeight - x - xWidth : (neg ? (plotHeight - y - h) : plotHeight - y),
+ width: inverted ? h : xWidth,
+ height: inverted ? xWidth : h
+ },
+ label = this.label,
+ alignAttr;
+
+ if (label) {
+ label.align(this.alignOptions, null, stackBox); // align the label to the box
+
+ // Set visibility (#678)
+ alignAttr = label.alignAttr;
+ label[this.options.crop === false || chart.isInsidePlot(alignAttr.x, alignAttr.y) ? 'show' : 'hide'](true);
+ }
+ }
+};
- // if called without an argument, toggle visibility
- series.visible = vis = series.userOptions.visible = vis === UNDEFINED ? !oldVisibility : vis;
- showOrHide = vis ? 'show' : 'hide';
- // show or hide elements
- each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker'], function (key) {
- if (series[key]) {
- series[key][showOrHide]();
+// Stacking methods defined on the Axis prototype
+
+/**
+ * Build the stacks from top down
+ */
+Axis.prototype.buildStacks = function () {
+ var series = this.series,
+ reversedStacks = pick(this.options.reversedStacks, true),
+ i = series.length;
+ if (!this.isXAxis) {
+ this.usePercentage = false;
+ while (i--) {
+ series[reversedStacks ? i : series.length - i - 1].setStackedPoints();
+ }
+ // Loop up again to compute percent stack
+ if (this.usePercentage) {
+ for (i = 0; i < series.length; i++) {
+ series[i].setPercentStacks();
}
- });
+ }
+ }
+};
+Axis.prototype.renderStackTotals = function () {
+ var axis = this,
+ chart = axis.chart,
+ renderer = chart.renderer,
+ stacks = axis.stacks,
+ stackKey,
+ oneStack,
+ stackCategory,
+ stackTotalGroup = axis.stackTotalGroup;
- // hide tooltip (#1361)
- if (chart.hoverSeries === series) {
- series.onMouseOut();
- }
+ // Create a separate group for the stack total labels
+ if (!stackTotalGroup) {
+ axis.stackTotalGroup = stackTotalGroup =
+ renderer.g('stack-labels')
+ .attr({
+ visibility: VISIBLE,
+ zIndex: 6
+ })
+ .add();
+ }
+ // plotLeft/Top will change when y axis gets wider so we need to translate the
+ // stackTotalGroup at every render call. See bug #506 and #516
+ stackTotalGroup.translate(chart.plotLeft, chart.plotTop);
- if (legendItem) {
- chart.legend.colorizeItem(series, vis);
+ // Render each stack total
+ for (stackKey in stacks) {
+ oneStack = stacks[stackKey];
+ for (stackCategory in oneStack) {
+ oneStack[stackCategory].render(stackTotalGroup);
}
+ }
+};
- // rescale or adapt to resized chart
- series.isDirty = true;
- // in a stack, all other series are affected
- if (series.options.stacking) {
- each(chart.series, function (otherSeries) {
- if (otherSeries.options.stacking && otherSeries.visible) {
- otherSeries.isDirty = true;
- }
- });
- }
+// Stacking methods defnied for Series prototype
- // show or hide linked series
- each(series.linkedSeries, function (otherSeries) {
- otherSeries.setVisible(vis, false);
- });
+/**
+ * Adds series' points value to corresponding stack
+ */
+Series.prototype.setStackedPoints = function () {
+ if (!this.options.stacking || (this.visible !== true && this.chart.options.chart.ignoreHiddenSeries !== false)) {
+ return;
+ }
- if (ignoreHiddenSeries) {
- chart.isDirtyBox = true;
+ var series = this,
+ xData = series.processedXData,
+ yData = series.processedYData,
+ stackedYData = [],
+ yDataLength = yData.length,
+ seriesOptions = series.options,
+ threshold = seriesOptions.threshold,
+ stackOption = seriesOptions.stack,
+ stacking = seriesOptions.stacking,
+ stackKey = series.stackKey,
+ negKey = '-' + stackKey,
+ negStacks = series.negStacks,
+ yAxis = series.yAxis,
+ stacks = yAxis.stacks,
+ oldStacks = yAxis.oldStacks,
+ isNegative,
+ stack,
+ other,
+ 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 = negStacks && y < threshold;
+ key = isNegative ? negKey : stackKey;
+
+ // Create empty object for this stack if it doesn't exist yet
+ if (!stacks[key]) {
+ stacks[key] = {};
}
- if (redraw !== false) {
- chart.redraw();
+
+ // Initialize StackItem for this x
+ if (!stacks[key][x]) {
+ if (oldStacks[key] && oldStacks[key][x]) {
+ stacks[key][x] = oldStacks[key][x];
+ stacks[key][x].total = null;
+ } else {
+ stacks[key][x] = new StackItem(yAxis, yAxis.options.stackLabels, isNegative, x, stackOption, stacking);
+ }
}
- fireEvent(series, showOrHide);
- },
+ // If the StackItem doesn't exist, create it first
+ stack = stacks[key][x];
+ stack.points[series.index] = [stack.cum || 0];
- /**
- * Show the graph
- */
- show: function () {
- this.setVisible(true);
- },
+ // Add value to the stack total
+ if (stacking === 'percent') {
- /**
- * Hide the graph
- */
- hide: function () {
- this.setVisible(false);
- },
+ // Percent stacked column, totals are the same for the positive and negative stacks
+ other = isNegative ? stackKey : negKey;
+ if (negStacks && stacks[other] && stacks[other][x]) {
+ other = stacks[other][x];
+ stack.total = other.total = mathMax(other.total, stack.total) + mathAbs(y) || 0;
+ // Percent stacked areas
+ } else {
+ stack.total = correctFloat(stack.total + (mathAbs(y) || 0));
+ }
+ } else {
+ stack.total = correctFloat(stack.total + (y || 0));
+ }
- /**
- * Set the selected state of the graph
- *
- * @param selected {Boolean} True to select the series, false to unselect. If
- * UNDEFINED, the selection state is toggled.
- */
- select: function (selected) {
- var series = this;
- // if called without an argument, toggle
- series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected;
+ stack.cum = (stack.cum || 0) + (y || 0);
- if (series.checkbox) {
- series.checkbox.checked = selected;
- }
+ stack.points[series.index].push(stack.cum);
+ stackedYData[i] = stack.cum;
- fireEvent(series, selected ? 'select' : 'unselect');
- },
+ }
- drawTracker: TrackerMixin.drawTrackerGraph
+ if (stacking === 'percent') {
+ yAxis.usePercentage = true;
+ }
-}; // end Series prototype
+ this.stackedYData = stackedYData; // To be used in getExtremes
+ // Reset old stacks
+ yAxis.oldStacks = {};
+};
+
+/**
+ * Iterate over all stacks and compute the absolute values to percent
+ */
+Series.prototype.setPercentStacks = function () {
+ var series = this,
+ stackKey = series.stackKey,
+ stacks = series.yAxis.stacks,
+ processedXData = series.processedXData;
+
+ each([stackKey, '-' + stackKey], function (key) {
+ var i = processedXData.length,
+ x,
+ stack,
+ pointExtremes,
+ totalFactor;
+
+ while (i--) {
+ x = processedXData[i];
+ stack = stacks[key] && stacks[key][x];
+ pointExtremes = stack && stack.points[series.index];
+ if (pointExtremes) {
+ totalFactor = stack.total ? 100 / stack.total : 0;
+ pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor); // Y bottom value
+ pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor); // Y value
+ series.stackedYData[i] = pointExtremes[1];
+ }
+ }
+ });
+};
+
// Extend the Chart prototype for dynamic methods
extend(Chart.prototype, {
/**
* Add a series dynamically after time
@@ -15376,16 +14758,20 @@
/**
* Remove the axis from the chart
*/
remove: function (redraw) {
var chart = this.chart,
- key = this.coll; // xAxis or yAxis
+ key = this.coll, // xAxis or yAxis
+ axisSeries = this.series,
+ i = axisSeries.length;
- // Remove associated series
- each(this.series, function (series) {
- series.remove(false);
- });
+ // Remove associated series (#2687)
+ while (i--) {
+ if (axisSeries[i]) {
+ axisSeries[i].remove(false);
+ }
+ }
// Remove the axis
erase(chart.axes, this);
erase(chart[key], this);
chart.options[key].splice(this.options.index, 1);
@@ -15881,11 +15267,11 @@
}
});
}
var categoryWidth = mathMin(
- mathAbs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || 1),
+ mathAbs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || xAxis.tickInterval || 1), // #2610
xAxis.len // #1535
),
groupPadding = categoryWidth * options.groupPadding,
groupWidth = categoryWidth - 2 * groupPadding,
pointOffsetWidth = groupWidth / columnCount,
@@ -15959,11 +15345,10 @@
// Cache for access in polar
point.barX = barX;
point.pointWidth = pointWidth;
-
// Round off to obtain crisp edges
fromLeft = mathAbs(barX) < 0.5;
right = mathRound(barX + barW) + xCrisp;
barX = mathRound(barX) + xCrisp;
barW = right - barX;
@@ -16016,11 +15401,11 @@
drawPoints: function () {
var series = this,
chart = this.chart,
options = series.options,
renderer = chart.renderer,
- animationLimit = chart.options.animationLimit || 250,
+ animationLimit = options.animationLimit || 250,
shapeArgs;
// draw the columns
each(series.points, function (point) {
var plotY = point.plotY,
@@ -16029,11 +15414,11 @@
if (plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) {
shapeArgs = point.shapeArgs;
if (graphic) { // update
stop(graphic);
- graphic[chart.pointCount < animationLimit ? 'animate' : 'attr'](merge(shapeArgs));
+ graphic[series.points.length < animationLimit ? 'animate' : 'attr'](merge(shapeArgs));
} else {
point.graphic = graphic = renderer[point.shapeType](shapeArgs)
.attr(point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE])
.add(series.group)
@@ -16045,16 +15430,10 @@
}
});
},
/**
- * Add tracking event listener to the series group, so the point graphics
- * themselves act as trackers
- */
- drawTracker: TrackerMixin.drawTrackerPoint,
-
- /**
* Animate the column heights one by one from zero
* @param {Boolean} init Whether to initialize the animation or run it
*/
animate: function (init) {
var series = this,
@@ -16142,17 +15521,16 @@
sorted: false,
requireSorting: false,
noSharedTooltip: true,
trackerGroups: ['markerGroup'],
takeOrdinalPosition: false, // #2342
- drawTracker: TrackerMixin.drawTrackerPoint,
+ singularTooltips: true,
drawGraph: function () {
if (this.options.lineWidth) {
Series.prototype.drawGraph.call(this);
}
- },
- setTooltipPoints: noop
+ }
});
seriesTypes.scatter = ScatterSeries;
/**
@@ -16237,23 +15615,20 @@
* visibility is toggled
*/
setVisible: function (vis) {
var point = this,
series = point.series,
- chart = series.chart,
- method;
+ chart = series.chart;
// if called without an argument, toggle visibility
point.visible = point.options.visible = vis = vis === UNDEFINED ? !point.visible : vis;
series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data
-
- method = vis ? 'show' : 'hide';
// Show and hide associated elements
each(['graphic', 'dataLabel', 'connector', 'shadowGroup'], function (key) {
if (point[key]) {
- point[key][method]();
+ point[key][vis ? 'show' : 'hide'](true);
}
});
if (point.legendItem) {
chart.legend.colorizeItem(point, vis);
@@ -16314,10 +15689,11 @@
pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
stroke: 'borderColor',
'stroke-width': 'borderWidth',
fill: 'color'
},
+ singularTooltips: true,
/**
* Pies have one color each point
*/
getColor: noop,
@@ -16359,16 +15735,16 @@
/**
* Extend the basic setData method by running processData and generatePoints immediately,
* in order to access the points from the legend.
*/
- setData: function (data, redraw) {
- Series.prototype.setData.call(this, data, false);
+ setData: function (data, redraw, animation, updatePoints) {
+ Series.prototype.setData.call(this, data, false, animation, updatePoints);
this.processData();
this.generatePoints();
if (pick(redraw, true)) {
- this.chart.redraw();
+ this.chart.redraw(animation);
}
},
/**
* Extend the generatePoints method by adding total and percentage properties to each point
@@ -16418,11 +15794,11 @@
start,
end,
angle,
startAngle = options.startAngle || 0,
startAngleRad = series.startAngleRad = mathPI / 180 * (startAngle - 90),
- endAngleRad = series.endAngleRad = mathPI / 180 * ((options.endAngle || (startAngle + 360)) - 90),
+ endAngleRad = series.endAngleRad = mathPI / 180 * ((pick(options.endAngle, startAngle + 360)) - 90),
circ = endAngleRad - startAngleRad, //2 * mathPI,
points = series.points,
radiusX, // the x component of the radius vector for a given point
radiusY,
labelDistance = options.dataLabels.distance,
@@ -16439,11 +15815,11 @@
}
// utility for getting the x value from a given y, used for anticollision logic in data labels
series.getX = function (y, left) {
- angle = math.asin((y - positions[1]) / (positions[2] / 2 + labelDistance));
+ angle = math.asin(mathMin((y - positions[1]) / (positions[2] / 2 + labelDistance), 1));
return positions[0] +
(left ? -1 : 1) *
(mathCos(angle) * (positions[2] / 2 + labelDistance));
};
@@ -16469,15 +15845,19 @@
innerR: positions[3] / 2,
start: mathRound(start * precision) / precision,
end: mathRound(end * precision) / precision
};
- // center for the sliced out slice
+ // The angle must stay within -90 and 270 (#2645)
angle = (end + start) / 2;
- if (angle > 0.75 * circ) {
+ if (angle > 1.5 * mathPI) {
angle -= 2 * mathPI;
+ } else if (angle < -mathPI / 2) {
+ angle += 2 * mathPI;
}
+
+ // Center for the sliced out slice
point.slicedTranslation = {
translateX: mathRound(mathCos(angle) * slicedOffset),
translateY: mathRound(mathSin(angle) * slicedOffset)
};
@@ -16507,12 +15887,11 @@
angle // center angle
];
}
},
-
- setTooltipPoints: noop,
+
drawGraph: null,
/**
* Draw the data points
*/
@@ -16558,16 +15937,19 @@
// draw the slice
if (graphic) {
graphic.animate(extend(shapeArgs, groupTranslation));
} else {
- point.graphic = graphic = renderer.arc(shapeArgs)
+ point.graphic = graphic = renderer[point.shapeType](shapeArgs)
.setRadialReference(series.center)
.attr(
point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]
)
- .attr({ 'stroke-linejoin': 'round' })
+ .attr({
+ 'stroke-linejoin': 'round'
+ //zIndex: 1 // #2722 (reversed)
+ })
.attr(groupTranslation)
.add(series.group)
.shadow(shadow, shadowGroup);
}
@@ -16588,15 +15970,10 @@
return a.angle !== undefined && (b.angle - a.angle) * sign;
});
},
/**
- * Draw point specific tracker objects. Inherit directly from column series.
- */
- drawTracker: TrackerMixin.drawTrackerPoint,
-
- /**
* Use a simple symbol from LegendSymbolMixin
*/
drawLegendSymbol: LegendSymbolMixin.drawRectangle,
/**
@@ -16753,11 +16130,13 @@
var chart = this.chart,
inverted = chart.inverted,
plotX = pick(point.plotX, -999),
plotY = pick(point.plotY, -999),
bBox = dataLabel.getBBox(),
- visible = this.visible && (point.series.forceDL || chart.isInsidePlot(point.plotX, point.plotY, inverted)),
+ // 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;
if (visible) {
// The alignment box is a singular point
@@ -17151,10 +16530,11 @@
} else {
point.connector = connector = series.chart.renderer.path(connectorPath).attr({
'stroke-width': connectorWidth,
stroke: options.connectorColor || point.color || '#606060',
visibility: visibility
+ //zIndex: 0 // #2722 (reversed)
})
.add(series.group);
}
} else if (connector) {
point.connector = connector.destroy();
@@ -17262,10 +16642,11 @@
inside = pick(options.inside, !!this.options.stacking); // draw it inside the box?
// Align to the column itself, or the top of it
if (dlBox) { // Area range uses this method but not alignTo
alignTo = merge(dlBox);
+
if (inverted) {
alignTo = {
x: chart.plotWidth - alignTo.y - alignTo.height,
y: chart.plotHeight - alignTo.x - alignTo.width,
width: alignTo.height,
@@ -17283,10 +16664,11 @@
alignTo.height = 0;
}
}
}
+
// When alignment is undefined (typically columns and bars), display the individual
// point below or above the point depending on the threshold
options.align = pick(
options.align,
!inverted || inside ? 'center' : below ? 'right' : 'left'
@@ -17301,20 +16683,870 @@
};
}
+/**
+ * TrackerMixin for points and graphs
+ */
+var TrackerMixin = Highcharts.TrackerMixin = {
+
+ drawTrackerPoint: function () {
+ var series = this,
+ chart = series.chart,
+ pointer = chart.pointer,
+ cursor = series.options.cursor,
+ css = cursor && { cursor: cursor },
+ onMouseOver = function (e) {
+ var target = e.target,
+ point;
+
+ if (chart.hoverSeries !== series) {
+ series.onMouseOver();
+ }
+
+ while (target && !point) {
+ point = target.point;
+ target = target.parentNode;
+ }
+
+ if (point !== UNDEFINED && point !== chart.hoverPoint) { // undefined on graph in scatterchart
+ point.onMouseOver(e);
+ }
+ };
+
+ // Add reference to the point
+ each(series.points, function (point) {
+ if (point.graphic) {
+ point.graphic.element.point = point;
+ }
+ if (point.dataLabel) {
+ point.dataLabel.element.point = point;
+ }
+ });
+
+ // Add the event listeners, we need to do this only once
+ if (!series._hasTracking) {
+ each(series.trackerGroups, function (key) {
+ if (series[key]) { // we don't always have dataLabelsGroup
+ series[key]
+ .addClass(PREFIX + 'tracker')
+ .on('mouseover', onMouseOver)
+ .on('mouseout', function (e) { pointer.onTrackerMouseOut(e); })
+ .css(css);
+ if (hasTouch) {
+ series[key].on('touchstart', onMouseOver);
+ }
+ }
+ });
+ series._hasTracking = true;
+ }
+ },
+
+ /**
+ * Draw the tracker object that sits above all data labels and markers to
+ * track mouse events on the graph or points. For the line type charts
+ * the tracker uses the same graphPath, but with a greater stroke width
+ * for better control.
+ */
+ drawTrackerGraph: function () {
+ var series = this,
+ options = series.options,
+ trackByArea = options.trackByArea,
+ trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath),
+ trackerPathLength = trackerPath.length,
+ chart = series.chart,
+ pointer = chart.pointer,
+ 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();
+ }
+ },
+ /*
+ * Empirical lowest possible opacities for TRACKER_FILL for an element to stay invisible but clickable
+ * IE6: 0.002
+ * IE7: 0.002
+ * IE8: 0.002
+ * IE9: 0.00000000001 (unlimited)
+ * IE10: 0.0001 (exporting only)
+ * FF: 0.00000000001 (unlimited)
+ * Chrome: 0.000001
+ * Safari: 0.000001
+ * Opera: 0.00000000001 (unlimited)
+ */
+ TRACKER_FILL = 'rgba(192,192,192,' + (hasSVG ? 0.0001 : 0.002) + ')';
+
+ // Extend end points. A better way would be to use round linecaps,
+ // but those are not clickable in VML.
+ if (trackerPathLength && !trackByArea) {
+ i = trackerPathLength + 1;
+ while (i--) {
+ if (trackerPath[i] === M) { // extend left side
+ trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L);
+ }
+ if ((i && trackerPath[i] === M) || i === trackerPathLength) { // extend right side
+ trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]);
+ }
+ }
+ }
+
+ // handle single points
+ 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
+
+ series.tracker = renderer.path(trackerPath)
+ .attr({
+ 'stroke-linejoin': 'round', // #1225
+ visibility: series.visible ? VISIBLE : HIDDEN,
+ stroke: TRACKER_FILL,
+ fill: trackByArea ? TRACKER_FILL : NONE,
+ 'stroke-width' : options.lineWidth + (trackByArea ? 0 : 2 * snap),
+ zIndex: 2
+ })
+ .add(series.group);
+
+ // The tracker is added to the series group, which is clipped, but is covered
+ // by the marker group. So the marker group also needs to capture events.
+ each([series.tracker, series.markerGroup], function (tracker) {
+ tracker.addClass(PREFIX + 'tracker')
+ .on('mouseover', onMouseOver)
+ .on('mouseout', function (e) { pointer.onTrackerMouseOut(e); })
+ .css(css);
+
+ if (hasTouch) {
+ tracker.on('touchstart', onMouseOver);
+ }
+ });
+ }
+ }
+};
+/* End TrackerMixin */
+
+
+/**
+ * Add tracking event listener to the series group, so the point graphics
+ * themselves act as trackers
+ */
+
+if (seriesTypes.column) {
+ ColumnSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
+}
+
+if (seriesTypes.pie) {
+ seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
+}
+
+if (seriesTypes.scatter) {
+ ScatterSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
+}
+
+/*
+ * Extend Legend for item events
+ */
+extend(Legend.prototype, {
+
+ setItemEvents: function (item, legendItem, useHTML, itemStyle, itemHiddenStyle) {
+ var legend = this;
+ // Set the events on the item group, or in case of useHTML, the item itself (#1249)
+ (useHTML ? legendItem : item.legendGroup).on('mouseover', function () {
+ item.setState(HOVER_STATE);
+ legendItem.css(legend.options.itemHoverStyle);
+ })
+ .on('mouseout', function () {
+ legendItem.css(item.visible ? itemStyle : itemHiddenStyle);
+ item.setState();
+ })
+ .on('click', function (event) {
+ var strLegendItemClick = 'legendItemClick',
+ fnLegendItemClick = function () {
+ item.setVisible();
+ };
+
+ // Pass over the click/touch event. #4.
+ event = {
+ browserEvent: event
+ };
+
+ // click the name or symbol
+ if (item.firePointEvent) { // point
+ item.firePointEvent(strLegendItemClick, event, fnLegendItemClick);
+ } else {
+ fireEvent(item, strLegendItemClick, event, fnLegendItemClick);
+ }
+ });
+ },
+
+ createCheckboxForItem: function (item) {
+ var legend = this;
+
+ item.checkbox = createElement('input', {
+ type: 'checkbox',
+ checked: item.selected,
+ defaultChecked: item.selected // required by IE7
+ }, legend.options.itemCheckboxStyle, legend.chart.container);
+
+ addEvent(item.checkbox, 'click', function (event) {
+ var target = event.target;
+ fireEvent(item, 'checkboxClick', {
+ checked: target.checked
+ },
+ function () {
+ item.select();
+ }
+ );
+ });
+ }
+});
+
+/*
+ * Add pointer cursor to legend itemstyle in defaultOptions
+ */
+defaultOptions.legend.itemStyle.cursor = 'pointer';
+
+
+/*
+ * Extend the Chart object with interaction
+ */
+
+extend(Chart.prototype, {
+ /**
+ * Display the zoom button
+ */
+ showResetZoom: function () {
+ var chart = this,
+ lang = defaultOptions.lang,
+ btnOptions = chart.options.chart.resetZoomButton,
+ theme = btnOptions.theme,
+ states = theme.states,
+ alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox';
+
+ this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, function () { chart.zoomOut(); }, theme, states && states.hover)
+ .attr({
+ align: btnOptions.position.align,
+ title: lang.resetZoomTitle
+ })
+ .add()
+ .align(btnOptions.position, false, alignTo);
+
+ },
+
+ /**
+ * Zoom out to 1:1
+ */
+ zoomOut: function () {
+ var chart = this;
+ fireEvent(chart, 'selection', { resetSelection: true }, function () {
+ chart.zoom();
+ });
+ },
+
+ /**
+ * Zoom into a given portion of the chart given by axis coordinates
+ * @param {Object} event
+ */
+ zoom: function (event) {
+ var chart = this,
+ hasZoomed,
+ pointer = chart.pointer,
+ displayButton = false,
+ resetZoomButton;
+
+ // If zoom is called with no arguments, reset the axes
+ if (!event || event.resetSelection) {
+ each(chart.axes, function (axis) {
+ hasZoomed = axis.zoom();
+ });
+ } else { // else, zoom in on all axes
+ each(event.xAxis.concat(event.yAxis), function (axisData) {
+ var axis = axisData.axis,
+ isXAxis = axis.isXAxis;
+
+ // don't zoom more than minRange
+ if (pointer[isXAxis ? 'zoomX' : 'zoomY'] || pointer[isXAxis ? 'pinchX' : 'pinchY']) {
+ hasZoomed = axis.zoom(axisData.min, axisData.max);
+ if (axis.displayBtn) {
+ displayButton = true;
+ }
+ }
+ });
+ }
+
+ // Show or hide the Reset zoom button
+ resetZoomButton = chart.resetZoomButton;
+ if (displayButton && !resetZoomButton) {
+ chart.showResetZoom();
+ } else if (!displayButton && isObject(resetZoomButton)) {
+ chart.resetZoomButton = resetZoomButton.destroy();
+ }
+
+
+ // Redraw
+ if (hasZoomed) {
+ chart.redraw(
+ pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation
+ );
+ }
+ },
+
+ /**
+ * Pan the chart by dragging the mouse across the pane. This function is called
+ * on mouse move, and the distance to pan is computed from chartX compared to
+ * the first chartX position in the dragging operation.
+ */
+ pan: function (e, panning) {
+
+ var chart = this,
+ hoverPoints = chart.hoverPoints,
+ doRedraw;
+
+ // remove active points for shared tooltip
+ if (hoverPoints) {
+ each(hoverPoints, function (point) {
+ 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'],
+ 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;
+
+ if (axis.series.length && newMin > mathMin(extremes.dataMin, extremes.min) && 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
+ });
+
+ if (doRedraw) {
+ chart.redraw(false);
+ }
+ css(chart.container, { cursor: 'move' });
+ }
+});
+
+/*
+ * Extend the Point object with interaction
+ */
+extend(Point.prototype, {
+ /**
+ * Toggle the selection status of a point
+ * @param {Boolean} selected Whether to select or unselect the point.
+ * @param {Boolean} accumulate Whether to add to the previous selection. By default,
+ * this happens if the control key (Cmd on Mac) was pressed during clicking.
+ */
+ select: function (selected, accumulate) {
+ var point = this,
+ series = point.series,
+ chart = series.chart;
+
+ selected = pick(selected, !point.selected);
+
+ // fire the event with the defalut handler
+ point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () {
+ point.selected = point.options.selected = selected;
+ series.options.data[inArray(point, series.data)] = point.options;
+
+ point.setState(selected && SELECT_STATE);
+
+ // unselect all other points unless Ctrl or Cmd + click
+ if (!accumulate) {
+ each(chart.getSelectedPoints(), function (loopPoint) {
+ if (loopPoint.selected && loopPoint !== point) {
+ loopPoint.selected = loopPoint.options.selected = false;
+ series.options.data[inArray(loopPoint, series.data)] = loopPoint.options;
+ loopPoint.setState(NORMAL_STATE);
+ loopPoint.firePointEvent('unselect');
+ }
+ });
+ }
+ });
+ },
+
+ /**
+ * Runs on mouse over the point
+ */
+ onMouseOver: function (e) {
+ var point = this,
+ series = point.series,
+ chart = series.chart,
+ tooltip = chart.tooltip,
+ hoverPoint = chart.hoverPoint;
+
+ // set normal state to previous series
+ if (hoverPoint && hoverPoint !== point) {
+ hoverPoint.onMouseOut();
+ }
+
+ // trigger the event
+ point.firePointEvent('mouseOver');
+
+ // update the tooltip
+ if (tooltip && (!tooltip.shared || series.noSharedTooltip)) {
+ tooltip.refresh(point, e);
+ }
+
+ // hover this
+ point.setState(HOVER_STATE);
+ chart.hoverPoint = point;
+ },
+
+ /**
+ * Runs on mouse out from the point
+ */
+ onMouseOut: function () {
+ var chart = this.series.chart,
+ hoverPoints = chart.hoverPoints;
+
+ if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887
+ this.firePointEvent('mouseOut');
+
+ this.setState();
+ chart.hoverPoint = null;
+ }
+ },
+
+ /**
+ * Fire an event on the Point object. Must not be renamed to fireEvent, as this
+ * causes a name clash in MooTools
+ * @param {String} eventType
+ * @param {Object} eventArgs Additional event arguments
+ * @param {Function} defaultFunction Default event handler
+ */
+ firePointEvent: function (eventType, eventArgs, defaultFunction) {
+ var point = this,
+ series = this.series,
+ seriesOptions = series.options;
+
+ // load event handlers on demand to save time on mouseover/out
+ if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) {
+ this.importEvents();
+ }
+
+ // add default handler if in selection mode
+ if (eventType === 'click' && seriesOptions.allowPointSelect) {
+ defaultFunction = function (event) {
+ // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera
+ point.select(null, event.ctrlKey || event.metaKey || event.shiftKey);
+ };
+ }
+
+ fireEvent(this, eventType, eventArgs, defaultFunction);
+ },
+ /**
+ * Import events from the series' and point's options. Only do it on
+ * demand, to save processing time on hovering.
+ */
+ importEvents: function () {
+ if (!this.hasImportedEvents) {
+ var point = this,
+ options = merge(point.series.options.point, point.options),
+ events = options.events,
+ eventType;
+
+ point.events = events;
+
+ for (eventType in events) {
+ addEvent(point, eventType, events[eventType]);
+ }
+ this.hasImportedEvents = true;
+
+ }
+ },
+
+ /**
+ * Set the point's state
+ * @param {String} state
+ */
+ setState: function (state, move) {
+ var point = this,
+ plotX = point.plotX,
+ plotY = point.plotY,
+ series = point.series,
+ stateOptions = series.options.states,
+ markerOptions = defaultPlotOptions[series.type].marker && series.options.marker,
+ normalDisabled = markerOptions && !markerOptions.enabled,
+ markerStateOptions = markerOptions && markerOptions.states[state],
+ stateDisabled = markerStateOptions && markerStateOptions.enabled === false,
+ stateMarkerGraphic = series.stateMarkerGraphic,
+ pointMarker = point.marker || {},
+ chart = series.chart,
+ radius,
+ newSymbol,
+ pointAttr = point.pointAttr;
+
+ state = state || NORMAL_STATE; // empty string
+ move = move && stateMarkerGraphic;
+
+ if (
+ // already has this state
+ (state === point.state && !move) ||
+ // selected points don't respond to hover
+ (point.selected && state !== SELECT_STATE) ||
+ // series' state options is disabled
+ (stateOptions[state] && stateOptions[state].enabled === false) ||
+ // general point marker's state options is disabled
+ (state && (stateDisabled || (normalDisabled && !markerStateOptions.enabled))) ||
+ // individual point marker's state options is disabled
+ (state && pointMarker.states && pointMarker.states[state] && pointMarker.states[state].enabled === false) // #1610
+
+ ) {
+ return;
+ }
+
+
+ // apply hover styles to the existing point
+ if (point.graphic) {
+ radius = markerOptions && point.graphic.symbolName && pointAttr[state].r;
+ point.graphic.attr(merge(
+ pointAttr[state],
+ radius ? { // new symbol attributes (#507, #612)
+ x: plotX - radius,
+ y: plotY - radius,
+ width: 2 * radius,
+ height: 2 * radius
+ } : {}
+ ));
+ } else {
+ // if a graphic is not applied to each point in the normal state, create a shared
+ // graphic for the hover state
+ if (state && markerStateOptions) {
+ radius = markerStateOptions.radius;
+ newSymbol = pointMarker.symbol || series.symbol;
+
+ // If the point has another symbol than the previous one, throw away the
+ // state marker graphic and force a new one (#1459)
+ if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) {
+ stateMarkerGraphic = stateMarkerGraphic.destroy();
+ }
+
+ // Add a new state marker graphic
+ if (!stateMarkerGraphic) {
+ series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol(
+ newSymbol,
+ plotX - radius,
+ plotY - radius,
+ 2 * radius,
+ 2 * radius
+ )
+ .attr(pointAttr[state])
+ .add(series.markerGroup);
+ stateMarkerGraphic.currentSymbol = newSymbol;
+
+ // Move the existing graphic
+ } else {
+ stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054
+ x: plotX - radius,
+ y: plotY - radius
+ });
+ }
+ }
+
+ if (stateMarkerGraphic) {
+ stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? 'show' : 'hide'](); // #2450
+ }
+ }
+
+ point.state = state;
+ }
+});
+
+/*
+ * Extend the Series object with interaction
+ */
+
+extend(Series.prototype, {
+ /**
+ * Series mouse over handler
+ */
+ onMouseOver: function () {
+ var series = this,
+ chart = series.chart,
+ hoverSeries = chart.hoverSeries;
+
+ // set normal state to previous series
+ if (hoverSeries && hoverSeries !== series) {
+ hoverSeries.onMouseOut();
+ }
+
+ // trigger the event, but to save processing time,
+ // only if defined
+ if (series.options.events.mouseOver) {
+ fireEvent(series, 'mouseOver');
+ }
+
+ // hover this
+ series.setState(HOVER_STATE);
+ chart.hoverSeries = series;
+ },
+
+ /**
+ * Series mouse out handler
+ */
+ onMouseOut: function () {
+ // trigger the event only if listeners exist
+ var series = this,
+ options = series.options,
+ chart = series.chart,
+ tooltip = chart.tooltip,
+ hoverPoint = chart.hoverPoint;
+
+ // trigger mouse out on the point, which must be in this series
+ if (hoverPoint) {
+ hoverPoint.onMouseOut();
+ }
+
+ // fire the mouse out event
+ if (series && options.events.mouseOut) {
+ fireEvent(series, 'mouseOut');
+ }
+
+
+ // hide the tooltip
+ if (tooltip && !options.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) {
+ tooltip.hide();
+ }
+
+ // set normal state
+ series.setState();
+ chart.hoverSeries = null;
+ },
+
+ /**
+ * Set the state of the graph
+ */
+ setState: function (state) {
+ var series = this,
+ options = series.options,
+ graph = series.graph,
+ graphNeg = series.graphNeg,
+ stateOptions = options.states,
+ lineWidth = options.lineWidth,
+ attribs;
+
+ state = state || NORMAL_STATE;
+
+ if (series.state !== state) {
+ series.state = state;
+
+ if (stateOptions[state] && stateOptions[state].enabled === false) {
+ return;
+ }
+
+ if (state) {
+ lineWidth = stateOptions[state].lineWidth || lineWidth + 1;
+ }
+
+ if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML
+ attribs = {
+ 'stroke-width': lineWidth
+ };
+ // use attr because animate will cause any other animation on the graph to stop
+ graph.attr(attribs);
+ if (graphNeg) {
+ graphNeg.attr(attribs);
+ }
+ }
+ }
+ },
+
+ /**
+ * Set the visibility of the graph
+ *
+ * @param vis {Boolean} True to show the series, false to hide. If UNDEFINED,
+ * the visibility is toggled.
+ */
+ setVisible: function (vis, redraw) {
+ var series = this,
+ chart = series.chart,
+ legendItem = series.legendItem,
+ showOrHide,
+ ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries,
+ oldVisibility = series.visible;
+
+ // if called without an argument, toggle visibility
+ series.visible = vis = series.userOptions.visible = vis === UNDEFINED ? !oldVisibility : vis;
+ showOrHide = vis ? 'show' : 'hide';
+
+ // show or hide elements
+ each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker'], function (key) {
+ if (series[key]) {
+ series[key][showOrHide]();
+ }
+ });
+
+
+ // hide tooltip (#1361)
+ if (chart.hoverSeries === series) {
+ series.onMouseOut();
+ }
+
+
+ if (legendItem) {
+ chart.legend.colorizeItem(series, vis);
+ }
+
+
+ // rescale or adapt to resized chart
+ series.isDirty = true;
+ // in a stack, all other series are affected
+ if (series.options.stacking) {
+ each(chart.series, function (otherSeries) {
+ if (otherSeries.options.stacking && otherSeries.visible) {
+ otherSeries.isDirty = true;
+ }
+ });
+ }
+
+ // show or hide linked series
+ each(series.linkedSeries, function (otherSeries) {
+ otherSeries.setVisible(vis, false);
+ });
+
+ if (ignoreHiddenSeries) {
+ chart.isDirtyBox = true;
+ }
+ if (redraw !== false) {
+ chart.redraw();
+ }
+
+ fireEvent(series, showOrHide);
+ },
+
+ /**
+ * Memorize tooltip texts and positions
+ */
+ setTooltipPoints: function (renew) {
+ var series = this,
+ points = [],
+ pointsLength,
+ low,
+ high,
+ xAxis = series.xAxis,
+ xExtremes = xAxis && xAxis.getExtremes(),
+ axisLength = xAxis ? (xAxis.tooltipLen || xAxis.len) : series.chart.plotSizeX, // tooltipLen and tooltipPosName used in polar
+ point,
+ pointX,
+ 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 || series.singularTooltips) {
+ return;
+ }
+
+ // renew
+ if (renew) {
+ series.tooltipPoints = null;
+ }
+
+ // concat segments to overcome null values
+ each(series.segments || series.points, function (segment) {
+ points = points.concat(segment);
+ });
+
+ // 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];
+ pointX = point.x;
+ if (pointX >= xExtremes.min && pointX <= xExtremes.max) { // #1149
+ nextPoint = points[i + 1];
+
+ // Set this range's low to the last range's high plus one
+ low = high === UNDEFINED ? 0 : high + 1;
+ // Now find the new high
+ high = points[i + 1] ?
+ mathMin(mathMax(0, mathFloor( // #2070
+ (point.clientX + (nextPoint ? (nextPoint.wrappedClientX || nextPoint.clientX) : axisLength)) / 2
+ )), axisLength) :
+ axisLength;
+
+ while (low >= 0 && low <= high) {
+ tooltipPoints[low++] = point;
+ }
+ }
+ }
+ series.tooltipPoints = tooltipPoints;
+ },
+
+ /**
+ * Show the graph
+ */
+ show: function () {
+ this.setVisible(true);
+ },
+
+ /**
+ * Hide the graph
+ */
+ hide: function () {
+ this.setVisible(false);
+ },
+
+
+ /**
+ * Set the selected state of the graph
+ *
+ * @param selected {Boolean} True to select the series, false to unselect. If
+ * UNDEFINED, the selection state is toggled.
+ */
+ select: function (selected) {
+ var series = this;
+ // if called without an argument, toggle
+ series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected;
+
+ if (series.checkbox) {
+ series.checkbox.checked = selected;
+ }
+
+ fireEvent(series, selected ? 'select' : 'unselect');
+ },
+
+ drawTracker: TrackerMixin.drawTrackerGraph
+});
// global variables
extend(Highcharts, {
// Constructors
Axis: Axis,
Chart: Chart,
Color: Color,
Point: Point,
- Tick: Tick,
- Tooltip: Tooltip,
+ Tick: Tick,
Renderer: Renderer,
Series: Series,
SVGElement: SVGElement,
SVGRenderer: SVGRenderer,