Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/01-Chart-Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ enabled | Boolean | true | Are tooltips enabled
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
Expand Down
87 changes: 38 additions & 49 deletions src/core/core.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ module.exports = function(Chart) {
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;
Expand Down Expand Up @@ -403,7 +404,9 @@ module.exports = function(Chart) {
// 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);
}
},

/**
Expand Down Expand Up @@ -644,20 +647,6 @@ module.exports = function(Chart) {
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) {
Expand All @@ -668,32 +657,54 @@ module.exports = function(Chart) {

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
if (hoverOptions.onHover) {
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);
Expand All @@ -710,34 +721,12 @@ module.exports = function(Chart) {
me.updateHoverStyle(me.active, hoverOptions.mode, true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that mean the chart is redrawn at every mouse move? If so, that's not good :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, yes. What do you think about detecting if there are changes in update and returning a Boolean. If there are changes, we redraw.

Alternatively, we factor the position code out to here and pass it to the tooltip before update.

}

// 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;
}
});
};
12 changes: 11 additions & 1 deletion src/core/core.legend.js
Original file line number Diff line number Diff line change
Expand Up @@ -426,11 +426,17 @@ module.exports = function(Chart) {
}
},

// 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) {
Expand Down Expand Up @@ -458,14 +464,18 @@ module.exports = function(Chart) {
// 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;
}
});

Expand Down
150 changes: 116 additions & 34 deletions src/core/core.tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module.exports = function(Chart) {
enabled: true,
custom: null,
mode: 'nearest',
position: 'average',
intersect: true,
backgroundColor: 'rgba(0,0,0,0.8)',
titleFontStyle: 'bold',
Expand Down Expand Up @@ -104,39 +105,6 @@ module.exports = function(Chart) {
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
Expand Down Expand Up @@ -504,7 +472,7 @@ module.exports = function(Chart) {
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) {
Expand Down Expand Up @@ -770,6 +738,120 @@ module.exports = function(Chart) {
// 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
};
}
};
};