]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Configurable Tooltip Position Modes (#3453)
authorEvert Timberg <evert.timberg+github@gmail.com>
Fri, 14 Oct 2016 21:36:49 +0000 (16:36 -0500)
committerGitHub <noreply@github.com>
Fri, 14 Oct 2016 21:36:49 +0000 (16:36 -0500)
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

docs/01-Chart-Configuration.md
src/core/core.controller.js
src/core/core.legend.js
src/core/core.tooltip.js

index ef0a5b57f46459a18d2abda890fc66d6fb7b9583..c08e23d44f60b5dde779e64b173cf899f1a288b4 100644 (file)
@@ -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
index 6302dd11cef00970e688eaa497aada137d06151c..5e3632e4fde947ac8348bdcc265ffd082dcafbd5 100644 (file)
@@ -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;
@@ -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);
+                       }
                },
 
                /**
@@ -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) {
@@ -668,21 +657,47 @@ 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
@@ -690,10 +705,6 @@ module.exports = function(Chart) {
                                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);
@@ -710,34 +721,12 @@ module.exports = function(Chart) {
                                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;
                }
        });
 };
index 3afbabcf3e6d3e6af9caaa8fcf96773f34882485..043b78aac6a7f2c1d5bcdf710d608d203998f75d 100644 (file)
@@ -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) {
@@ -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;
                }
        });
 
index dcb9e89d31e1141d60bac97d851411d929761622..1e02fee7780428cdedea5ac992de99b1035ea911 100755 (executable)
@@ -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',
@@ -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
@@ -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) {
@@ -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
+                       };
+               }
+       };
 };