Adds new tooltip position option that allows configuring where a tooltip is displayed on the graph in relation to the elements that appear in it
custom | Function | null | See [section](#advanced-usage-external-tooltips) below
mode | String | 'nearest' | Sets which elements appear in the tooltip. See [Interaction Modes](#interaction-modes) for details
intersect | Boolean | true | if true, the tooltip mode applies only when the mouse position intersects with an element. If false, the mode will be applied at all times.
+position | String | 'average' | The mode for positioning the tooltip. 'average' mode will place the tooltip at the average position of the items displayed in the tooltip. 'nearest' will place the tooltip at the position of the element closest to the event position. New modes can be defined by adding functions to the Chart.Tooltip.positioners map.
itemSort | Function | undefined | Allows sorting of [tooltip items](#chart-configuration-tooltip-item-interface). Must implement at minimum a function that can be passed to [Array.prototype.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort). This function can also accept a third parameter that is the data object passed to the chart.
backgroundColor | Color | 'rgba(0,0,0,0.8)' | Background color of the tooltip
titleFontFamily | String | "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" | Font family for tooltip title inherited from global font family
me.chart = instance;
me.config = config;
me.options = config.options;
+ me._bufferedRender = false;
// Add the chart instance to the global namespace
Chart.instances[me.id] = me;
// Do this before render so that any plugins that need final scale updates can use it
Chart.plugins.notify('afterUpdate', [me]);
- me.render(animationDuration, lazy);
+ if (!me._bufferedRender) {
+ me.render(animationDuration, lazy);
+ }
},
/**
var method = enabled? 'setHoverStyle' : 'removeHoverStyle';
var element, i, ilen;
- switch (mode) {
- case 'single':
- elements = [elements[0]];
- break;
- case 'label':
- case 'dataset':
- case 'x-axis':
- // elements = elements;
- break;
- default:
- // unsupported mode
- return;
- }
-
for (i=0, ilen=elements.length; i<ilen; ++i) {
element = elements[i];
if (element) {
eventHandler: function(e) {
var me = this;
- var tooltip = me.tooltip;
+ var hoverOptions = me.options.hover;
+
+ // Buffer any update calls so that renders do not occur
+ me._bufferedRender = true;
+
+ var changed = me.handleEvent(e);
+ changed |= me.legend.handleEvent(e);
+ changed |= me.tooltip.handleEvent(e);
+
+ if (changed && !me.animating) {
+ // If entering, leaving, or changing elements, animate the change via pivot
+ me.stop();
+
+ // We only need to render at this point. Updating will cause scales to be
+ // recomputed generating flicker & using more memory than necessary.
+ me.render(hoverOptions.animationDuration, true);
+ }
+
+ me._bufferedRender = false;
+ return me;
+ },
+
+ /**
+ * Handle an event
+ * @private
+ * param e {Event} the event to handle
+ * @return {Boolean} true if the chart needs to re-render
+ */
+ handleEvent: function(e) {
+ var me = this;
var options = me.options || {};
var hoverOptions = options.hover;
- var tooltipsOptions = options.tooltips;
+ var changed = false;
me.lastActive = me.lastActive || [];
- me.lastTooltipActive = me.lastTooltipActive || [];
// Find Active Elements for hover and tooltips
if (e.type === 'mouseout') {
me.active = [];
- me.tooltipActive = [];
} else {
me.active = me.getElementsAtEventForMode(e, hoverOptions.mode, hoverOptions);
- me.tooltipActive = me.getElementsAtEventForMode(e, tooltipsOptions.mode, tooltipsOptions);
}
// On Hover hook
hoverOptions.onHover.call(me, me.active);
}
- if (me.legend && me.legend.handleEvent) {
- me.legend.handleEvent(e);
- }
-
if (e.type === 'mouseup' || e.type === 'click') {
if (options.onClick) {
options.onClick.call(me, e, me.active);
me.updateHoverStyle(me.active, hoverOptions.mode, true);
}
- // Built in Tooltips
- if (tooltipsOptions.enabled || tooltipsOptions.custom) {
- tooltip._active = me.tooltipActive;
- }
-
- // Hover animations
- if (!me.animating) {
- // If entering, leaving, or changing elements, animate the change via pivot
- if (!helpers.arrayEquals(me.active, me.lastActive) ||
- !helpers.arrayEquals(me.tooltipActive, me.lastTooltipActive)) {
-
- me.stop();
-
- if (tooltipsOptions.enabled || tooltipsOptions.custom) {
- tooltip.update(true);
- tooltip.pivot();
- }
-
- // We only need to render at this point. Updating will cause scales to be
- // recomputed generating flicker & using more memory than necessary.
- me.render(hoverOptions.animationDuration, true);
- }
- }
+ changed = !helpers.arrayEquals(me.active, me.lastActive);
// Remember Last Actives
me.lastActive = me.active;
- me.lastTooltipActive = me.tooltipActive;
- return me;
+
+ return changed;
}
});
};
}
},
- // Handle an event
+ /**
+ * Handle an event
+ * @private
+ * @param e {Event} the event to handle
+ * @return {Boolean} true if a change occured
+ */
handleEvent: function(e) {
var me = this;
var opts = me.options;
var type = e.type === 'mouseup' ? 'click' : e.type;
+ var changed = false;
if (type === 'mousemove') {
if (!opts.onHover) {
// Touching an element
if (type === 'click') {
opts.onClick.call(me, e, me.legendItems[i]);
+ changed = true;
break;
} else if (type === 'mousemove') {
opts.onHover.call(me, e, me.legendItems[i]);
+ changed = true;
break;
}
}
}
}
+
+ return changed;
}
});
enabled: true,
custom: null,
mode: 'nearest',
+ position: 'average',
intersect: true,
backgroundColor: 'rgba(0,0,0,0.8)',
titleFontStyle: 'bold',
return base;
}
- function getAveragePosition(elements) {
- if (!elements.length) {
- return false;
- }
-
- var i, len;
- var xPositions = [];
- var yPositions = [];
-
- for (i = 0, len = elements.length; i < len; ++i) {
- var el = elements[i];
- if (el && el.hasValue()) {
- var pos = el.tooltipPosition();
- xPositions.push(pos.x);
- yPositions.push(pos.y);
- }
- }
-
- var x = 0,
- y = 0;
- for (i = 0; i < xPositions.length; ++i) {
- if (xPositions[i]) {
- x += xPositions[i];
- y += yPositions[i];
- }
- }
-
- return {
- x: Math.round(x / xPositions.length),
- y: Math.round(y / xPositions.length)
- };
- }
-
// Private helper to create a tooltip iteam model
// @param element : the chart element (point, arc, bar) to create the tooltip item for
// @return : new tooltip item
model.opacity = 1;
var labelColors = [],
- tooltipPosition = getAveragePosition(active);
+ tooltipPosition = Chart.Tooltip.positioners[opts.position](active, me._eventPosition);
var tooltipItems = [];
for (i = 0, len = active.length; i < len; ++i) {
// Footer
this.drawFooter(pt, vm, ctx, opacity);
}
+ },
+
+ /**
+ * Handle an event
+ * @private
+ * @param e {Event} the event to handle
+ * @returns {Boolean} true if the tooltip changed
+ */
+ handleEvent: function(e) {
+ var me = this;
+ var options = me._options;
+ var changed = false;
+
+ me._lastActive = me._lastActive || [];
+
+ // Find Active Elements for tooltips
+ if (e.type === 'mouseout') {
+ me._active = [];
+ } else {
+ me._active = me._chartInstance.getElementsAtEventForMode(e, options.mode, options);
+ }
+
+ // Remember Last Actives
+ changed = !helpers.arrayEquals(me._active, me._lastActive);
+ me._lastActive = me._active;
+
+ if (options.enabled || options.custom) {
+ me._eventPosition = helpers.getRelativePosition(e, me._chart);
+
+ var model = me._model;
+ me.update(true);
+ me.pivot();
+
+ // See if our tooltip position changed
+ changed |= (model.x !== me._model.x) || (model.y !== me._model.y);
+ }
+
+ return changed;
}
});
+
+ /**
+ * @namespace Chart.Tooltip.positioners
+ */
+ Chart.Tooltip.positioners = {
+ /**
+ * Average mode places the tooltip at the average position of the elements shown
+ * @function Chart.Tooltip.positioners.average
+ * @param elements {ChartElement[]} the elements being displayed in the tooltip
+ * @returns {Point} tooltip position
+ */
+ average: function(elements) {
+ if (!elements.length) {
+ return false;
+ }
+
+ var i, len;
+ var x = 0;
+ var y = 0;
+ var count = 0;
+
+ for (i = 0, len = elements.length; i < len; ++i) {
+ var el = elements[i];
+ if (el && el.hasValue()) {
+ var pos = el.tooltipPosition();
+ x += pos.x;
+ y += pos.y;
+ ++count;
+ }
+ }
+
+ return {
+ x: Math.round(x / count),
+ y: Math.round(y / count)
+ };
+ },
+
+ /**
+ * Gets the tooltip position nearest of the item nearest to the event position
+ * @function Chart.Tooltip.positioners.nearest
+ * @param elements {Chart.Element[]} the tooltip elements
+ * @param eventPosition {Point} the position of the event in canvas coordinates
+ * @returns {Point} the tooltip position
+ */
+ nearest: function(elements, eventPosition) {
+ var x = eventPosition.x;
+ var y = eventPosition.y;
+
+ var nearestElement;
+ var minDistance = Number.POSITIVE_INFINITY;
+ var i, len;
+ for (i = 0, len = elements.length; i < len; ++i) {
+ var el = elements[i];
+ if (el && el.hasValue()) {
+ var center = el.getCenterPoint();
+ var d = helpers.distanceBetweenPoints(eventPosition, center);
+
+ if (d < minDistance) {
+ minDistance = d;
+ nearestElement = el;
+ }
+ }
+ }
+
+ if (nearestElement) {
+ var tp = nearestElement.tooltipPosition();
+ x = tp.x;
+ y = tp.y;
+ }
+
+ return {
+ x: x,
+ y: y
+ };
+ }
+ };
};