]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Improve Tooltip and Hover Interaction (#3400)
authorEvert Timberg <evert.timberg+github@gmail.com>
Mon, 3 Oct 2016 20:05:21 +0000 (16:05 -0400)
committerGitHub <noreply@github.com>
Mon, 3 Oct 2016 20:05:21 +0000 (16:05 -0400)
Refactored interaction modes to use lookup functions in Chart.Interaction.modes and added new modes for 'point', 'index', 'nearest', 'x', and 'y'

29 files changed:
docs/01-Chart-Configuration.md
samples/AnimationCallbacks/progress-bar.html
samples/bar-multi-axis.html
samples/bar-stacked.html
samples/different-point-sizes.html
samples/line-cubicInterpolationMode.html
samples/line-legend.html
samples/line-multi-axis.html
samples/line-multiline-labels.html
samples/line-skip-points.html
samples/line-stacked-area.html
samples/line-stepped.html
samples/line.html
samples/scatter-multi-axis.html
samples/tooltip-hooks.html
src/chart.js
src/controllers/controller.bar.js
src/core/core.controller.js
src/core/core.helpers.js
src/core/core.interaction.js [new file with mode: 0644]
src/core/core.js
src/core/core.tooltip.js
src/elements/element.arc.js
src/elements/element.point.js
src/elements/element.rectangle.js
test/core.interaction.tests.js [new file with mode: 0644]
test/element.arc.tests.js
test/element.point.tests.js
test/element.rectangle.tests.js

index dd16e6d2c93241d52e57e1269671cad75f0fcf44..5ed712a97cdc8f5e6c289ff6caaf16d9212fd89e 100644 (file)
@@ -36,13 +36,13 @@ This concept was introduced in Chart.js 1.0 to keep configuration [DRY](https://
 
 Chart.js merges the options object passed to the chart with the global configuration using chart type defaults and scales defaults appropriately. This way you can be as specific as you would like in your individual chart configuration, while still changing the defaults for all chart types where applicable. The global general options are defined in `Chart.defaults.global`. The defaults for each chart type are discussed in the documentation for that chart type.
 
-The following example would set the hover mode to 'single' for all charts where this was not overridden by the chart type defaults or the options passed to the constructor on creation.
+The following example would set the hover mode to 'nearest' for all charts where this was not overridden by the chart type defaults or the options passed to the constructor on creation.
 
 ```javascript
-Chart.defaults.global.hover.mode = 'single';
+Chart.defaults.global.hover.mode = 'nearest';
 
-// Hover mode is set to single because it was not overridden here
-var chartInstanceHoverModeSingle = new Chart(ctx, {
+// Hover mode is set to nearest because it was not overridden here
+var chartInstanceHoverModeNearest = new Chart(ctx, {
     type: 'line',
     data: data,
 });
@@ -54,7 +54,7 @@ var chartInstanceDifferentHoverMode = new Chart(ctx, {
     options: {
         hover: {
             // Overrides the global setting
-            mode: 'label'
+            mode: 'index'
         }
     }
 })
@@ -200,7 +200,7 @@ var chartInstance = new Chart(ctx, {
                 fontColor: 'rgb(255, 99, 132)'
             }
         }
-    }
+}
 });
 ```
 
@@ -212,7 +212,8 @@ Name | Type | Default | Description
 --- | --- | --- | ---
 enabled | Boolean | true | Are tooltips enabled
 custom | Function | null | See [section](#advanced-usage-external-tooltips) below
-mode | String | 'single' | Sets which elements appear in the tooltip. Acceptable options are `'single'`, `'label'` or `'x-axis'`. <br>&nbsp;<br>`single` highlights the closest element. <br>&nbsp;<br>`label` highlights elements in all datasets at the same `X` value. <br>&nbsp;<br>`'x-axis'` also highlights elements in all datasets at the same `X` value, but activates when hovering anywhere within the vertical slice of the x-axis representing that `X` value.
+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.
 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
@@ -286,10 +287,28 @@ The hover configuration is passed into the `options.hover` namespace. The global
 
 Name | Type | Default | Description
 --- | --- | --- | ---
-mode | String | 'single' | Sets which elements hover. Acceptable options are `'single'`, `'label'`, `'x-axis'`, or `'dataset'`. <br>&nbsp;<br>`single` highlights the closest element. <br>&nbsp;<br>`label` highlights elements in all datasets at the same `X` value. <br>&nbsp;<br>`'x-axis'` also highlights elements in all datasets at the same `X` value, but activates when hovering anywhere within the vertical slice of the x-axis representing that `X` value.  <br>&nbsp;<br>`dataset` highlights the closest dataset.
+mode | String | 'naerest' | Sets which elements appear in the tooltip. See [Interaction Modes](#interaction-modes) for details
+intersect | Boolean | true | if true, the hover mode only applies when the mouse position intersects an item on the chart
 animationDuration | Number | 400 | Duration in milliseconds it takes to animate hover style changes
 onHover | Function | null | Called when any of the events fire. Called in the context of the chart and passed an array of active elements (bars, points, etc)
 
+### Interaction Modes
+When configuring interaction with the graph via hover or tooltips, a number of different modes are available.
+
+The following table details the modes and how they behave in conjunction with the `intersect` setting
+
+Mode | Behaviour 
+--- | --- 
+point | Finds all of the items that intersect the point
+nearest | Gets the item that is nearest to the point. The nearest item is determined based on the distance to the center of the chart item (point, bar). If 2 or more items are at the same distance, the one with the smallest area is used. If `intersect` is true, this is only triggered when the mouse position intersects an item in the graph. This is very useful for combo charts where points are hidden behind bars.
+single (deprecated) | Finds the first item that intersects the point and returns it. Behaves like 'nearest' mode with intersect = true.
+label (deprecated) | See `'index'` mode
+index | Finds item at the same index. If the `intersect` setting is true, the first intersecting item is used to determine the index in the data. If `intersect` false the nearest item is used to determine the index. 
+x-axis (deprecated) | Behaves like `'index'` mode with `intersect = true`
+dataset | Finds items in the same dataset. If the `intersect` setting is true, the first intersecting item is used to determine the index in the data. If `intersect` false the nearest item is used to determine the index.
+x | Returns all items that would intersect based on the `X` coordinate of the position only. Would be useful for a vertical cursor implementation. Note that this only applies to cartesian charts
+y | Returns all items that would intersect based on the `Y` coordinate of the position. This would be useful for a horizontal cursor implementation. Note that this only applies to cartesian charts.
+
 ### Animation Configuration
 
 The following animation options are available. The global options for are defined in `Chart.defaults.global.animation`.
index 7d79d6c1d8c4884e47bcd7aa9edd5ef3753237f5..fad7bd48934e9d57242ef6bdeb82eb09d32131f6 100644 (file)
@@ -75,7 +75,7 @@
                     }
                 },
                 tooltips: {
-                    mode: 'label',
+                    mode: 'index',
                 },
                 scales: {
                     xAxes: [{
index 9c8ec3642b7b2139ce537be4454229f7a8e779b6..ee027aaf8d3a254f2c2af9184cc829b28bc58a75 100644 (file)
@@ -56,7 +56,7 @@
             data: barChartData, 
             options: {
                 responsive: true,
-                hoverMode: 'label',
+                hoverMode: 'index',
                 hoverAnimationDuration: 400,
                 stacked: false,
                 title:{
index e85a72b26bc40563c1e0624674f7d038107bfede..685af83db787c505d0f7594876ea8d571e395a3c 100644 (file)
@@ -55,7 +55,7 @@
                         text:"Chart.js Bar Chart - Stacked"
                     },
                     tooltips: {
-                        mode: 'label'
+                        mode: 'index'
                     },
                     responsive: true,
                     scales: {
index 926eecfba8b1c670bdd9eb1a127e693539b6ff05..0fb231f5232c885d8c80e2f3e588433ee1a18c52 100644 (file)
@@ -70,7 +70,7 @@
                     position: 'bottom',
                 },
                 hover: {
-                    mode: 'label'
+                    mode: 'index'
                 },
                 scales: {
                     xAxes: [{
index 97dac46ca43ddf0794a003d81182014482ba6e33..2d85c95c930a03792003d6246292cd87050df670 100644 (file)
@@ -68,7 +68,7 @@
                     text:'Chart.js Line Chart - Cubic interpolation mode'
                 },
                 tooltips: {
-                    mode: 'label'
+                    mode: 'index'
                 },
                 hover: {
                     mode: 'dataset'
index 92e5e5b5ab412ea04554250b6d470a80757a9c80..201d4c07e511c59cb2fe43e22b83c36930479d09 100644 (file)
@@ -68,7 +68,7 @@
                     position: 'bottom',
                 },
                 hover: {
-                    mode: 'label'
+                    mode: 'index'
                 },
                 scales: {
                     xAxes: [{
index 03da24bdabba19a9270644ac91edf27f0774afd2..0b296ff69ed096e074edc2981c8445f769a5ea6f 100644 (file)
@@ -54,7 +54,7 @@
             data: lineChartData,
             options: {
                 responsive: true,
-                hoverMode: 'label',
+                hoverMode: 'index',
                 stacked: false,
                 title:{
                     display:true,
index 3fd0de5e14290d22604bc3253422c5d376dd8a68..b9d08aee7a37c9341116fda75a06e44b77680a31 100644 (file)
@@ -65,7 +65,7 @@
                     text:'Chart.js Line Chart'
                 },
                 tooltips: {
-                    mode: 'label',
+                    mode: 'index',
                     callbacks: {
                         // beforeTitle: function() {
                         //     return '...beforeTitle';
index 2d760d2e1f637fd6acd7562a6048287a5826074e..3ef27558755739f753abbb6102b87d4431ca0a55 100644 (file)
                     text:'Chart.js Line Chart - Skip Points'
                 },
                 tooltips: {
-                    mode: 'label',
+                    mode: 'index',
                 },
                 hover: {
-                    mode: 'label'
+                    mode: 'index'
                 },
                 scales: {
                     xAxes: [{
index 88f14e2bd818f0af82ace857d1f9528ae7eece21..26616f5dea801e67b95b9aca77a14b18bdbd4f18 100644 (file)
           text:"Chart.js Line Chart - Stacked Area"
         },
         tooltips: {
-          mode: 'label',
+          mode: 'index',
         },
         hover: {
-          mode: 'label'
+          mode: 'index'
         },
         scales: {
           xAxes: [{
index e618a698cfaee5542f4a8fe83349a3996d73c839..f6cebe262717b2f5ff36cba05834c35b4c286a90 100644 (file)
@@ -68,7 +68,7 @@
                     text:'Chart.js Line Chart'
                 },
                 tooltips: {
-                    mode: 'label',
+                    mode: 'index',
                     callbacks: {
                         // beforeTitle: function() {
                         //     return '...beforeTitle';
index ffca9df3b1e3f45ab62f9924c9b1e79f66a741fc..464072ba1db599e17d41b24c87b574770ed89106 100644 (file)
@@ -65,7 +65,8 @@
                     text:'Chart.js Line Chart'
                 },
                 tooltips: {
-                    mode: 'label',
+                    mode: 'index',
+                    intersect: false,
                     callbacks: {
                         // beforeTitle: function() {
                         //     return '...beforeTitle';
@@ -91,7 +92,8 @@
                     }
                 },
                 hover: {
-                    mode: 'dataset'
+                    mode: 'nearest',
+                    intersect: true
                 },
                 scales: {
                     xAxes: [{
index 43d27e502284243dd08f978cf86819f4ecf8170a..842f8891a4b9509b916c074b713b7dd7debbb66a 100644 (file)
@@ -99,7 +99,8 @@
                data: scatterChartData,
                options: {
                    responsive: true,
-                   hoverMode: 'single',
+                   hoverMode: 'nearest',
+                   intersect: true,
                title: {
                        display: true,
                        text: 'Chart.js Scatter Chart - Multi Axis'
index 88a660515fed5850e3c0e0ee2bcedd237ee262d3..08055ece8cac0ae36ca53330ae8dd01cbf428662 100644 (file)
@@ -59,7 +59,7 @@
                     text:"Chart.js Line Chart - Tooltip Hooks"
                 },
                 tooltips: {
-                    mode: 'label',
+                    mode: 'index',
                     callbacks: {
                         beforeTitle: function() {
                             return '...beforeTitle';
@@ -91,7 +91,7 @@
                     }
                 },
                 hover: {
-                    mode: 'label'
+                    mode: 'index'
                 },
                 scales: {
                     xAxes: [{
index aa0d46c45cef2bfb430f1ecce9bb690e4552d1c0..2c5e628264c5d7f0c76ab390c16bb70e3c8f9220 100644 (file)
@@ -16,6 +16,7 @@ require('./core/core.ticks.js')(Chart);
 require('./core/core.scale')(Chart);
 require('./core/core.title')(Chart);
 require('./core/core.legend')(Chart);
+require('./core/core.interaction')(Chart);
 require('./core/core.tooltip')(Chart);
 
 require('./elements/element.arc')(Chart);
index d9fa7bfeb05d705e6a7ea9f44129272e701b7ad4..f828eb79e912916ad9dc8d8df70c81b2a53ab3e3 100644 (file)
@@ -422,21 +422,6 @@ module.exports = function(Chart) {
                                        if (vm.borderWidth) {
                                                ctx.stroke();
                                        }
-                               },
-
-                               inRange: function(mouseX, mouseY) {
-                                       var vm = this._view;
-                                       var inRange = false;
-
-                                       if (vm) {
-                                               if (vm.x < vm.base) {
-                                                       inRange = (mouseY >= vm.y - vm.height / 2 && mouseY <= vm.y + vm.height / 2) && (mouseX >= vm.x && mouseX <= vm.base);
-                                               } else {
-                                                       inRange = (mouseY >= vm.y - vm.height / 2 && mouseY <= vm.y + vm.height / 2) && (mouseX >= vm.base && mouseX <= vm.x);
-                                               }
-                                       }
-
-                                       return inRange;
                                }
                        });
 
index b2928c6b863f5245ae8885bcf1c66c23216468e7..9712d8fb627857eed4ed913d50e8e0542ab3b326 100644 (file)
@@ -515,125 +515,28 @@ module.exports = function(Chart) {
                // Get the single element that was clicked on
                // @return : An object containing the dataset index and element index of the matching element. Also contains the rectangle that was draw
                getElementAtEvent: function(e) {
-                       var me = this;
-                       var eventPosition = helpers.getRelativePosition(e, me.chart);
-                       var elementsArray = [];
-
-                       helpers.each(me.data.datasets, function(dataset, datasetIndex) {
-                               if (me.isDatasetVisible(datasetIndex)) {
-                                       var meta = me.getDatasetMeta(datasetIndex);
-                                       helpers.each(meta.data, function(element) {
-                                               if (element.inRange(eventPosition.x, eventPosition.y)) {
-                                                       elementsArray.push(element);
-                                                       return elementsArray;
-                                               }
-                                       });
-                               }
-                       });
-
-                       return elementsArray.slice(0, 1);
+                       return Chart.Interaction.modes.single(this, e);
                },
 
                getElementsAtEvent: function(e) {
-                       var me = this;
-                       var eventPosition = helpers.getRelativePosition(e, me.chart);
-                       var elementsArray = [];
-
-                       var found = function() {
-                               if (me.data.datasets) {
-                                       for (var i = 0; i < me.data.datasets.length; i++) {
-                                               var meta = me.getDatasetMeta(i);
-                                               if (me.isDatasetVisible(i)) {
-                                                       for (var j = 0; j < meta.data.length; j++) {
-                                                               if (meta.data[j].inRange(eventPosition.x, eventPosition.y)) {
-                                                                       return meta.data[j];
-                                                               }
-                                                       }
-                                               }
-                                       }
-                               }
-                       }.call(me);
-
-                       if (!found) {
-                               return elementsArray;
-                       }
-
-                       helpers.each(me.data.datasets, function(dataset, datasetIndex) {
-                               if (me.isDatasetVisible(datasetIndex)) {
-                                       var meta = me.getDatasetMeta(datasetIndex),
-                                               element = meta.data[found._index];
-                                       if (element && !element._view.skip) {
-                                               elementsArray.push(element);
-                                       }
-                               }
-                       }, me);
-
-                       return elementsArray;
+                       return Chart.Interaction.modes.label(this, e, {intersect: true});
                },
 
                getElementsAtXAxis: function(e) {
-                       var me = this;
-                       var eventPosition = helpers.getRelativePosition(e, me.chart);
-                       var elementsArray = [];
-
-                       var found = function() {
-                               if (me.data.datasets) {
-                                       for (var i = 0; i < me.data.datasets.length; i++) {
-                                               var meta = me.getDatasetMeta(i);
-                                               if (me.isDatasetVisible(i)) {
-                                                       for (var j = 0; j < meta.data.length; j++) {
-                                                               if (meta.data[j].inLabelRange(eventPosition.x, eventPosition.y)) {
-                                                                       return meta.data[j];
-                                                               }
-                                                       }
-                                               }
-                                       }
-                               }
-                       }.call(me);
-
-                       if (!found) {
-                               return elementsArray;
-                       }
-
-                       helpers.each(me.data.datasets, function(dataset, datasetIndex) {
-                               if (me.isDatasetVisible(datasetIndex)) {
-                                       var meta = me.getDatasetMeta(datasetIndex);
-                                       var index = helpers.findIndex(meta.data, function(it) {
-                                               return found._model.x === it._model.x;
-                                       });
-                                       if (index !== -1 && !meta.data[index]._view.skip) {
-                                               elementsArray.push(meta.data[index]);
-                                       }
-                               }
-                       }, me);
-
-                       return elementsArray;
+                       return Chart.Interaction.modes['x-axis'](this, e, {intersect: true});
                },
 
-               getElementsAtEventForMode: function(e, mode) {
-                       var me = this;
-                       switch (mode) {
-                       case 'single':
-                               return me.getElementAtEvent(e);
-                       case 'label':
-                               return me.getElementsAtEvent(e);
-                       case 'dataset':
-                               return me.getDatasetAtEvent(e);
-                       case 'x-axis':
-                               return me.getElementsAtXAxis(e);
-                       default:
-                               return e;
+               getElementsAtEventForMode: function(e, mode, options) {
+                       var method = Chart.Interaction.modes[mode];
+                       if (typeof method === 'function') {
+                               return method(this, e, options);
                        }
+
+                       return [];
                },
 
                getDatasetAtEvent: function(e) {
-                       var elementsArray = this.getElementAtEvent(e);
-
-                       if (elementsArray.length > 0) {
-                               elementsArray = this.getDatasetMeta(elementsArray[0]._datasetIndex).data;
-                       }
-
-                       return elementsArray;
+                       return Chart.Interaction.modes.dataset(this, e);
                },
 
                getDatasetMeta: function(datasetIndex) {
@@ -777,8 +680,8 @@ module.exports = function(Chart) {
                                me.active = [];
                                me.tooltipActive = [];
                        } else {
-                               me.active = me.getElementsAtEventForMode(e, hoverOptions.mode);
-                               me.tooltipActive = me.getElementsAtEventForMode(e, tooltipsOptions.mode);
+                               me.active = me.getElementsAtEventForMode(e, hoverOptions.mode, hoverOptions);
+                               me.tooltipActive = me.getElementsAtEventForMode(e, tooltipsOptions.mode, tooltipsOptions);
                        }
 
                        // On Hover hook
index 6d63284a7099d31ccf57d74a553a84a7b7fee8ca..d3fd727419d004dadfb130006a41594bd43d0de7 100644 (file)
@@ -308,6 +308,9 @@ module.exports = function(Chart) {
                        distance: radialDistanceFromCenter
                };
        };
+       helpers.distanceBetweenPoints = function(pt1, pt2) {
+               return Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2));
+       };
        helpers.aliasPixel = function(pixelWidth) {
                return (pixelWidth % 2 === 0) ? 0 : 0.5;
        };
diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js
new file mode 100644 (file)
index 0000000..571f3c3
--- /dev/null
@@ -0,0 +1,263 @@
+'use strict';
+
+module.exports = function(Chart) {
+       var helpers = Chart.helpers;
+
+       /**
+        * Helper function to traverse all of the visible elements in the chart
+        * @param chart {chart} the chart
+        * @param handler {Function} the callback to execute for each visible item
+        */
+       function parseVisibleItems(chart, handler) {
+               var datasets = chart.data.datasets;
+               var meta, i, j, ilen, jlen;
+
+               for (i = 0, ilen = datasets.length; i < ilen; ++i) {
+                       if (!chart.isDatasetVisible(i)) {
+                               continue;
+                       }
+
+                       meta = chart.getDatasetMeta(i);
+                       for (j = 0, jlen = meta.data.length; j < jlen; ++j) {
+                               var element = meta.data[j];
+                               if (!element._view.skip) {
+                                       handler(element);
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Helper function to get the items that intersect the event position
+        * @param items {ChartElement[]} elements to filter
+        * @param position {Point} the point to be nearest to
+        * @return {ChartElement[]} the nearest items
+        */
+       function getIntersectItems(chart, position) {
+               var elements = [];
+
+               parseVisibleItems(chart, function(element) {
+                       if (element.inRange(position.x, position.y)) {
+                               elements.push(element);
+                       }
+               });
+
+               return elements;
+       }
+
+       /**
+        * Helper function to get the items nearest to the event position considering all visible items in teh chart
+        * @param chart {Chart} the chart to look at elements from
+        * @param position {Point} the point to be nearest to
+        * @param intersect {Boolean} if true, only consider items that intersect the position
+        * @return {ChartElement[]} the nearest items
+        */
+       function getNearestItems(chart, position, intersect) {
+               var minDistance = Number.POSITIVE_INFINITY;
+               var nearestItems = [];
+
+               parseVisibleItems(chart, function(element) {
+                       if (intersect && !element.inRange(position.x, position.y)) {
+                               return;
+                       }
+
+                       var center = element.getCenterPoint();
+                       var distance = Math.round(helpers.distanceBetweenPoints(position, center));
+
+                       if (distance < minDistance) {
+                               nearestItems = [element];
+                               minDistance = distance;
+                       } else if (distance === minDistance) {
+                               // Can have multiple items at the same distance in which case we sort by size
+                               nearestItems.push(element);
+                       }
+               });
+
+               return nearestItems;
+       }
+
+       function indexMode(chart, e, options) {
+               var position = helpers.getRelativePosition(e, chart.chart);
+               var items = options.intersect ? getIntersectItems(chart, position) : getNearestItems(chart, position, false);
+               var elements = [];
+
+               if (!items.length) {
+                       return [];
+               }
+
+               chart.data.datasets.forEach(function(dataset, datasetIndex) {
+                       if (chart.isDatasetVisible(datasetIndex)) {
+                               var meta = chart.getDatasetMeta(datasetIndex),
+                                       element = meta.data[items[0]._index];
+
+                               // don't count items that are skipped (null data)
+                               if (element && !element._view.skip) {
+                                       elements.push(element);
+                               }
+                       }
+               });
+
+               return elements;
+       }
+
+       /**
+        * @interface IInteractionOptions
+        */
+       /**
+        * If true, only consider items that intersect the point
+        * @name IInterfaceOptions#boolean
+        * @type Boolean
+        */
+
+       /**
+        * @namespace Chart.Interaction
+        * Contains interaction related functions
+        */
+       Chart.Interaction = {
+               // Helper function for different modes
+               modes: {
+                       single: function(chart, e) {
+                               var position = helpers.getRelativePosition(e, chart.chart);
+                               var elements = [];
+
+                               parseVisibleItems(chart, function(element) {
+                                       if (element.inRange(position.x, position.y)) {
+                                               elements.push(element);
+                                               return elements;
+                                       }
+                               });
+
+                               return elements.slice(0, 1);
+                       },
+
+                       /**
+                        * @function Chart.Interaction.modes.label
+                        * @deprecated since version 2.4.0
+                        */
+                       label: indexMode,
+
+                       /**
+                        * Returns items at the same index. If the options.intersect parameter is true, we only return items if we intersect something
+                        * If the options.intersect mode is false, we find the nearest item and return the items at the same index as that item
+                        * @function Chart.Interaction.modes.index
+                        * @since v2.4.0
+                        * @param chart {chart} the chart we are returning items from
+                        * @param e {Event} the event we are find things at
+                        * @param options {IInteractionOptions} options to use during interaction
+                        * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned
+                        */
+                       index: indexMode,
+
+                       /**
+                        * Returns items in the same dataset. If the options.intersect parameter is true, we only return items if we intersect something
+                        * If the options.intersect is false, we find the nearest item and return the items in that dataset
+                        * @function Chart.Interaction.modes.dataset
+                        * @param chart {chart} the chart we are returning items from
+                        * @param e {Event} the event we are find things at
+                        * @param options {IInteractionOptions} options to use during interaction
+                        * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned
+                        */
+                       dataset: function(chart, e, options) {
+                               var position = helpers.getRelativePosition(e, chart.chart);
+                               var items = options.intersect ? getIntersectItems(chart, position) : getNearestItems(chart, position, false);
+
+                               if (items.length > 0) {
+                                       items = chart.getDatasetMeta(items[0]._datasetIndex).data;
+                               }
+
+                               return items;
+                       },
+
+                       /**
+                        * @function Chart.Interaction.modes.x-axis
+                        * @deprecated since version 2.4.0. Use index mode and intersect == true
+                        */
+                       'x-axis': function(chart, e) {
+                               return indexMode(chart, e, true);
+                       },
+
+                       /**
+                        * Point mode returns all elements that hit test based on the event position
+                        * of the event
+                        * @function Chart.Interaction.modes.intersect
+                        * @param chart {chart} the chart we are returning items from
+                        * @param e {Event} the event we are find things at
+                        * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned
+                        */
+                       point: function(chart, e) {
+                               var position = helpers.getRelativePosition(e, chart.chart);
+                               return getIntersectItems(chart, position);
+                       },
+
+                       /**
+                        * nearest mode returns the element closest to the point
+                        * @function Chart.Interaction.modes.intersect
+                        * @param chart {chart} the chart we are returning items from
+                        * @param e {Event} the event we are find things at
+                        * @param options {IInteractionOptions} options to use
+                        * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned
+                        */
+                       nearest: function(chart, e, options) {
+                               var position = helpers.getRelativePosition(e, chart.chart);
+                               var nearestItems = getNearestItems(chart, position, options.intersect);
+
+                               // We have multiple items at the same distance from the event. Now sort by smallest
+                               if (nearestItems.length > 1) {
+                                       nearestItems.sort(function(a, b) {
+                                               var sizeA = a.getArea();
+                                               var sizeB = b.getArea();
+                                               var ret = sizeA - sizeB;
+
+                                               if (ret === 0) {
+                                                       // if equal sort by dataset index
+                                                       ret = a._datasetIndex - b._datasetIndex;
+                                               }
+
+                                               return ret;
+                                       });
+                               }
+
+                               // Return only 1 item
+                               return nearestItems.slice(0, 1);
+                       },
+
+                       /**
+                        * x mode returns the elements that hit-test at the current x coordinate
+                        * @function Chart.Interaction.modes.x
+                        * @param chart {chart} the chart we are returning items from
+                        * @param e {Event} the event we are find things at
+                        * @param options {IInteractionOptions} options to use
+                        * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned
+                        */
+                       x: function(chart, e) {
+                               var position = helpers.getRelativePosition(e, chart.chart);
+                               var items = [];
+                               parseVisibleItems(chart, function(element) {
+                                       if (element.inXRange(position.x)) {
+                                               items.push(element);
+                                       }
+                               });
+                               return items;
+                       },
+
+                       /**
+                        * y mode returns the elements that hit-test at the current y coordinate
+                        * @function Chart.Interaction.modes.y
+                        * @param chart {chart} the chart we are returning items from
+                        * @param e {Event} the event we are find things at
+                        * @param options {IInteractionOptions} options to use
+                        * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned
+                        */
+                       y: function(chart, e) {
+                               var position = helpers.getRelativePosition(e, chart.chart);
+                               var items = [];
+                               parseVisibleItems(chart, function(element) {
+                                       if (element.inYRange(position.x)) {
+                                               items.push(element);
+                                       }
+                               });
+                               return items;
+                       }
+               }
+       };
+};
index d2899783816605f00ed38b71bf14dc4f6ad84ce0..319219f5d20bf0f7ce322210f81520f2be082aac 100755 (executable)
@@ -30,7 +30,8 @@ module.exports = function() {
                        events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'],
                        hover: {
                                onHover: null,
-                               mode: 'single',
+                               mode: 'nearest',
+                               intersect: true,
                                animationDuration: 400
                        },
                        onClick: null,
index d0068ee674dfdfe56819900e443c1b1d40978b8d..9210ed529ac05ae21b47dff08fecd8ae4998d167 100755 (executable)
@@ -7,7 +7,8 @@ module.exports = function(Chart) {
        Chart.defaults.global.tooltips = {
                enabled: true,
                custom: null,
-               mode: 'single',
+               mode: 'nearest',
+               intersect: true,
                backgroundColor: 'rgba(0,0,0,0.8)',
                titleFontStyle: 'bold',
                titleSpacing: 2,
index b114e6f04b7ed3c002ab0ff0bc44307354924b8a..a61d19a3a5ee7066e6c44ec335769123817842f6 100644 (file)
@@ -52,6 +52,19 @@ module.exports = function(Chart) {
                        }
                        return false;
                },
+               getCenterPoint: function() {
+                       var vm = this._view;
+                       var halfAngle = (vm.startAngle + vm.endAngle) / 2;
+                       var halfRadius = (vm.innerRadius + vm.outerRadius) / 2;
+                       return {
+                               x: vm.x + Math.cos(halfAngle) * halfRadius,
+                               y: vm.y + Math.sin(halfAngle) * halfRadius
+                       };
+               },
+               getArea: function() {
+                       var vm = this._view;
+                       return Math.PI * ((vm.endAngle - vm.startAngle) / (2 * Math.PI)) * (Math.pow(vm.outerRadius, 2) - Math.pow(vm.innerRadius, 2));
+               },
                tooltipPosition: function() {
                        var vm = this._view;
 
index 474ef252f53cd76af282f1fa5fbd167b90001d11..fe3a6696bd1e09e8a8b206ae6c394c3fc0d5a566 100644 (file)
@@ -18,14 +18,35 @@ module.exports = function(Chart) {
                hoverBorderWidth: 1
        };
 
+       function xRange(mouseX) {
+               var vm = this._view;
+               return vm ? (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + vm.hitRadius, 2)) : false;
+       }
+
+       function yRange(mouseY) {
+               var vm = this._view;
+               return vm ? (Math.pow(mouseY - vm.y, 2) < Math.pow(vm.radius + vm.hitRadius, 2)) : false;
+       }
+
        Chart.elements.Point = Chart.Element.extend({
                inRange: function(mouseX, mouseY) {
                        var vm = this._view;
                        return vm ? ((Math.pow(mouseX - vm.x, 2) + Math.pow(mouseY - vm.y, 2)) < Math.pow(vm.hitRadius + vm.radius, 2)) : false;
                },
-               inLabelRange: function(mouseX) {
+
+               inLabelRange: xRange,
+               inXRange: xRange,
+               inYRange: yRange,
+
+               getCenterPoint: function() {
                        var vm = this._view;
-                       return vm ? (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + vm.hitRadius, 2)) : false;
+                       return {
+                               x: vm.x,
+                               y: vm.y
+                       };
+               },
+               getArea: function() {
+                       return Math.PI * Math.pow(this._view.radius, 2);
                },
                tooltipPosition: function() {
                        var vm = this._view;
index 6752228c8703c8a210231edd826766850d246eb3..3f4ea10f907e92816a83fc6e682ab416067b4718 100644 (file)
@@ -11,6 +11,44 @@ module.exports = function(Chart) {
                borderSkipped: 'bottom'
        };
 
+       function isVertical(bar) {
+               return bar._view.width !== undefined;
+       }
+
+       /**
+        * Helper function to get the bounds of the bar regardless of the orientation
+        * @private
+        * @param bar {Chart.Element.Rectangle} the bar
+        * @return {Bounds} bounds of the bar
+        */
+       function getBarBounds(bar) {
+               var vm = bar._view;
+               var x1, x2, y1, y2;
+
+               if (isVertical(bar)) {
+                       // vertical
+                       var halfWidth = vm.width / 2;
+                       x1 = vm.x - halfWidth;
+                       x2 = vm.x + halfWidth;
+                       y1 = Math.min(vm.y, vm.base);
+                       y2 = Math.max(vm.y, vm.base);
+               } else {
+                       // horizontal bar
+                       var halfHeight = vm.height / 2;
+                       x1 = Math.min(vm.x, vm.base);
+                       x2 = Math.max(vm.x, vm.base);
+                       y1 = vm.y - halfHeight;
+                       y2 = vm.y + halfHeight;
+               }
+
+               return {
+                       left: x1,
+                       top: y1,
+                       right: x2,
+                       bottom: y2
+               };
+       }
+
        Chart.elements.Rectangle = Chart.Element.extend({
                draw: function() {
                        var ctx = this._chart.ctx;
@@ -72,16 +110,56 @@ module.exports = function(Chart) {
                        return vm.base - vm.y;
                },
                inRange: function(mouseX, mouseY) {
+                       var inRange = false;
+
+                       if (this._view) {
+                               var bounds = getBarBounds(this);
+                               inRange = mouseX >= bounds.left && mouseX <= bounds.right && mouseY >= bounds.top && mouseY <= bounds.bottom;
+                       }
+
+                       return inRange;
+               },
+               inLabelRange: function(mouseX, mouseY) {
+                       var me = this;
+                       if (!me._view) {
+                               return false;
+                       }
+
+                       var inRange = false;
+                       var bounds = getBarBounds(me);
+
+                       if (isVertical(me)) {
+                               inRange = mouseX >= bounds.left && mouseX <= bounds.right;
+                       } else {
+                               inRange = mouseY >= bounds.top && mouseY <= bounds.bottom;
+                       }
+
+                       return inRange;
+               },
+               inXRange: function(mouseX) {
+                       var bounds = getBarBounds(this);
+                       return mouseX >= bounds.left && mouseX <= bounds.right;
+               },
+               inYRange: function(mouseY) {
+                       var bounds = getBarBounds(this);
+                       return mouseY >= bounds.top && mouseY <= bounds.bottom;
+               },
+               getCenterPoint: function() {
                        var vm = this._view;
-                       return vm ?
-                                       (vm.y < vm.base ?
-                                               (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.y && mouseY <= vm.base) :
-                                               (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.base && mouseY <= vm.y)) :
-                                       false;
+                       var x, y;
+                       if (isVertical(this)) {
+                               x = vm.x;
+                               y = (vm.y + vm.base) / 2;
+                       } else {
+                               x = (vm.x + vm.base) / 2;
+                               y = vm.y;
+                       }
+
+                       return {x: x, y: y};
                },
-               inLabelRange: function(mouseX) {
+               getArea: function() {
                        var vm = this._view;
-                       return vm ? (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) : false;
+                       return vm.width * Math.abs(vm.y - vm.base);
                },
                tooltipPosition: function() {
                        var vm = this._view;
diff --git a/test/core.interaction.tests.js b/test/core.interaction.tests.js
new file mode 100644 (file)
index 0000000..312ac8f
--- /dev/null
@@ -0,0 +1,562 @@
+// Tests of the interaction handlers in Core.Interaction
+
+// Test the rectangle element
+describe('Core.Interaction', function() {
+       describe('point mode', function() {
+               it ('should return all items under the point', function() {
+                       var chartInstance = window.acquireChart({
+                               type: 'line',
+                               data: {
+                                       datasets: [{
+                                               label: 'Dataset 1',
+                                               data: [10, 20, 30],
+                                               pointHoverBorderColor: 'rgb(255, 0, 0)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 0)'
+                                       }, {
+                                               label: 'Dataset 2',
+                                               data: [40, 20, 40],
+                                               pointHoverBorderColor: 'rgb(0, 0, 255)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 255)'
+                                       }],
+                                       labels: ['Point 1', 'Point 2', 'Point 3']
+                               }
+                       });
+
+                       // Trigger an event over top of the
+                       var meta0 = chartInstance.getDatasetMeta(0);
+                       var meta1 = chartInstance.getDatasetMeta(1);
+                       var point = meta0.data[1];
+
+                       var node = chartInstance.chart.canvas;
+                       var rect = node.getBoundingClientRect();
+
+                       var evt = {
+                               view: window,
+                               bubbles: true,
+                               cancelable: true,
+                               clientX: rect.left + point._model.x,
+                               clientY: rect.top + point._model.y,
+                               currentTarget: node
+                       };
+
+                       var elements = Chart.Interaction.modes.point(chartInstance, evt);
+                       expect(elements).toEqual([point, meta1.data[1]]);
+               });
+
+               it ('should return an empty array when no items are found', function() {
+                       var chartInstance = window.acquireChart({
+                               type: 'line',
+                               data: {
+                                       datasets: [{
+                                               label: 'Dataset 1',
+                                               data: [10, 20, 30],
+                                               pointHoverBorderColor: 'rgb(255, 0, 0)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 0)'
+                                       }, {
+                                               label: 'Dataset 2',
+                                               data: [40, 20, 40],
+                                               pointHoverBorderColor: 'rgb(0, 0, 255)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 255)'
+                                       }],
+                                       labels: ['Point 1', 'Point 2', 'Point 3']
+                               }
+                       });
+
+                       // Trigger an event at (0, 0)
+                       var node = chartInstance.chart.canvas;
+                       var evt = {
+                               view: window,
+                               bubbles: true,
+                               cancelable: true,
+                               clientX: 0,
+                               clientY: 0,
+                               currentTarget: node
+                       };
+
+                       var elements = Chart.Interaction.modes.point(chartInstance, evt);
+                       expect(elements).toEqual([]);
+               });
+       });
+
+       describe('index mode', function() {
+               it ('should return all items at the same index', function() {
+                       var chartInstance = window.acquireChart({
+                               type: 'line',
+                               data: {
+                                       datasets: [{
+                                               label: 'Dataset 1',
+                                               data: [10, 20, 30],
+                                               pointHoverBorderColor: 'rgb(255, 0, 0)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 0)'
+                                       }, {
+                                               label: 'Dataset 2',
+                                               data: [40, 40, 40],
+                                               pointHoverBorderColor: 'rgb(0, 0, 255)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 255)'
+                                       }],
+                                       labels: ['Point 1', 'Point 2', 'Point 3']
+                               }
+                       });
+
+                       // Trigger an event over top of the
+                       var meta0 = chartInstance.getDatasetMeta(0);
+                       var meta1 = chartInstance.getDatasetMeta(1);
+                       var point = meta0.data[1];
+
+                       var node = chartInstance.chart.canvas;
+                       var rect = node.getBoundingClientRect();
+
+                       var evt = {
+                               view: window,
+                               bubbles: true,
+                               cancelable: true,
+                               clientX: rect.left + point._model.x,
+                               clientY: rect.top + point._model.y,
+                               currentTarget: node
+                       };
+
+                       var elements = Chart.Interaction.modes.index(chartInstance, evt, { intersect: true });
+                       expect(elements).toEqual([point, meta1.data[1]]);
+               });
+
+               it ('should return all items at the same index when intersect is false', function() {
+                       var chartInstance = window.acquireChart({
+                               type: 'line',
+                               data: {
+                                       datasets: [{
+                                               label: 'Dataset 1',
+                                               data: [10, 20, 30],
+                                               pointHoverBorderColor: 'rgb(255, 0, 0)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 0)'
+                                       }, {
+                                               label: 'Dataset 2',
+                                               data: [40, 40, 40],
+                                               pointHoverBorderColor: 'rgb(0, 0, 255)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 255)'
+                                       }],
+                                       labels: ['Point 1', 'Point 2', 'Point 3']
+                               }
+                       });
+
+                       // Trigger an event over top of the
+                       var meta0 = chartInstance.getDatasetMeta(0);
+                       var meta1 = chartInstance.getDatasetMeta(1);
+
+                       var node = chartInstance.chart.canvas;
+                       var rect = node.getBoundingClientRect();
+
+                       var evt = {
+                               view: window,
+                               bubbles: true,
+                               cancelable: true,
+                               clientX: rect.left,
+                               clientY: rect.top,
+                               currentTarget: node
+                       };
+
+                       var elements = Chart.Interaction.modes.index(chartInstance, evt, { intersect: false });
+                       expect(elements).toEqual([meta0.data[0], meta1.data[0]]);
+               });
+       });
+
+       describe('dataset mode', function() {
+               it ('should return all items in the dataset of the first item found', function() {
+                       var chartInstance = window.acquireChart({
+                               type: 'line',
+                               data: {
+                                       datasets: [{
+                                               label: 'Dataset 1',
+                                               data: [10, 20, 30],
+                                               pointHoverBorderColor: 'rgb(255, 0, 0)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 0)'
+                                       }, {
+                                               label: 'Dataset 2',
+                                               data: [40, 40, 40],
+                                               pointHoverBorderColor: 'rgb(0, 0, 255)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 255)'
+                                       }],
+                                       labels: ['Point 1', 'Point 2', 'Point 3']
+                               }
+                       });
+
+                       // Trigger an event over top of the
+                       var meta = chartInstance.getDatasetMeta(0);
+                       var point = meta.data[1];
+
+                       var node = chartInstance.chart.canvas;
+                       var rect = node.getBoundingClientRect();
+
+                       var evt = {
+                               view: window,
+                               bubbles: true,
+                               cancelable: true,
+                               clientX: rect.left + point._model.x,
+                               clientY: rect.top + point._model.y,
+                               currentTarget: node
+                       };
+
+                       var elements = Chart.Interaction.modes.dataset(chartInstance, evt, { intersect: true });
+                       expect(elements).toEqual(meta.data);
+               });
+
+               it ('should return all items in the dataset of the first item found when intersect is false', function() {
+                       var chartInstance = window.acquireChart({
+                               type: 'line',
+                               data: {
+                                       datasets: [{
+                                               label: 'Dataset 1',
+                                               data: [10, 20, 30],
+                                               pointHoverBorderColor: 'rgb(255, 0, 0)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 0)'
+                                       }, {
+                                               label: 'Dataset 2',
+                                               data: [40, 40, 40],
+                                               pointHoverBorderColor: 'rgb(0, 0, 255)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 255)'
+                                       }],
+                                       labels: ['Point 1', 'Point 2', 'Point 3']
+                               }
+                       });
+
+                       var node = chartInstance.chart.canvas;
+                       var rect = node.getBoundingClientRect();
+
+                       var evt = {
+                               view: window,
+                               bubbles: true,
+                               cancelable: true,
+                               clientX: rect.left,
+                               clientY: rect.top,
+                               currentTarget: node
+                       };
+
+                       var elements = Chart.Interaction.modes.dataset(chartInstance, evt, { intersect: false });
+
+                       var meta = chartInstance.getDatasetMeta(1);
+                       expect(elements).toEqual(meta.data);
+               });
+       });
+
+       describe('nearest mode', function() {
+               it ('should return the nearest item', function() {
+                       var chartInstance = window.acquireChart({
+                               type: 'line',
+                               data: {
+                                       datasets: [{
+                                               label: 'Dataset 1',
+                                               data: [10, 20, 30],
+                                               pointHoverBorderColor: 'rgb(255, 0, 0)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 0)'
+                                       }, {
+                                               label: 'Dataset 2',
+                                               data: [40, 40, 40],
+                                               pointHoverBorderColor: 'rgb(0, 0, 255)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 255)'
+                                       }],
+                                       labels: ['Point 1', 'Point 2', 'Point 3']
+                               }
+                       });
+
+                       // Trigger an event over top of the
+                       var meta = chartInstance.getDatasetMeta(1);
+                       var node = chartInstance.chart.canvas;
+                       var rect = node.getBoundingClientRect();
+                       var evt = {
+                               view: window,
+                               bubbles: true,
+                               cancelable: true,
+                               clientX: 0,
+                               clientY: 0,
+                               currentTarget: node
+                       };
+
+                       // Nearest to 0,0 (top left) will be first point of dataset 2
+                       var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: false });
+                       expect(elements).toEqual([meta.data[0]]);
+               });
+
+               it ('should return the smallest item if more than 1 are at the same distance', function() {
+                       var chartInstance = window.acquireChart({
+                               type: 'line',
+                               data: {
+                                       datasets: [{
+                                               label: 'Dataset 1',
+                                               data: [10, 40, 30],
+                                               pointRadius: [5, 5, 5],
+                                               pointHoverBorderColor: 'rgb(255, 0, 0)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 0)'
+                                       }, {
+                                               label: 'Dataset 2',
+                                               data: [40, 40, 40],
+                                               pointRadius: [10, 10, 10],
+                                               pointHoverBorderColor: 'rgb(0, 0, 255)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 255)'
+                                       }],
+                                       labels: ['Point 1', 'Point 2', 'Point 3']
+                               }
+                       });
+
+                       // Trigger an event over top of the
+                       var meta0 = chartInstance.getDatasetMeta(0);
+                       var meta1 = chartInstance.getDatasetMeta(1);
+
+                       // Halfway between 2 mid points
+                       var pt = {
+                               x: meta0.data[1]._view.x,
+                               y: (meta0.data[1]._view.y + meta1.data[1]._view.y) / 2
+                       };
+
+                       var node = chartInstance.chart.canvas;
+                       var rect = node.getBoundingClientRect();
+                       var evt = {
+                               view: window,
+                               bubbles: true,
+                               cancelable: true,
+                               clientX: rect.left + pt.x,
+                               clientY: rect.top + pt.y,
+                               currentTarget: node
+                       };
+
+                       // Nearest to 0,0 (top left) will be first point of dataset 2
+                       var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: false });
+                       expect(elements).toEqual([meta0.data[1]]);
+               });
+
+               it ('should return the lowest dataset index if size and area are the same', function() {
+                       var chartInstance = window.acquireChart({
+                               type: 'line',
+                               data: {
+                                       datasets: [{
+                                               label: 'Dataset 1',
+                                               data: [10, 40, 30],
+                                               pointRadius: [5, 10, 5],
+                                               pointHoverBorderColor: 'rgb(255, 0, 0)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 0)'
+                                       }, {
+                                               label: 'Dataset 2',
+                                               data: [40, 40, 40],
+                                               pointRadius: [10, 10, 10],
+                                               pointHoverBorderColor: 'rgb(0, 0, 255)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 255)'
+                                       }],
+                                       labels: ['Point 1', 'Point 2', 'Point 3']
+                               }
+                       });
+
+                       // Trigger an event over top of the
+                       var meta0 = chartInstance.getDatasetMeta(0);
+                       var meta1 = chartInstance.getDatasetMeta(1);
+
+                       // Halfway between 2 mid points
+                       var pt = {
+                               x: meta0.data[1]._view.x,
+                               y: (meta0.data[1]._view.y + meta1.data[1]._view.y) / 2
+                       };
+
+                       var node = chartInstance.chart.canvas;
+                       var rect = node.getBoundingClientRect();
+                       var evt = {
+                               view: window,
+                               bubbles: true,
+                               cancelable: true,
+                               clientX: rect.left + pt.x,
+                               clientY: rect.top + pt.y,
+                               currentTarget: node
+                       };
+
+                       // Nearest to 0,0 (top left) will be first point of dataset 2
+                       var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: false });
+                       expect(elements).toEqual([meta0.data[1]]);
+               });
+       });
+
+       describe('nearest intersect mode', function() {
+               it ('should return the nearest item', function() {
+                       var chartInstance = window.acquireChart({
+                               type: 'line',
+                               data: {
+                                       datasets: [{
+                                               label: 'Dataset 1',
+                                               data: [10, 20, 30],
+                                               pointHoverBorderColor: 'rgb(255, 0, 0)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 0)'
+                                       }, {
+                                               label: 'Dataset 2',
+                                               data: [40, 40, 40],
+                                               pointHoverBorderColor: 'rgb(0, 0, 255)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 255)'
+                                       }],
+                                       labels: ['Point 1', 'Point 2', 'Point 3']
+                               }
+                       });
+
+                       // Trigger an event over top of the
+                       var meta = chartInstance.getDatasetMeta(1);
+                       var point = meta.data[1];
+
+                       var node = chartInstance.chart.canvas;
+                       var rect = node.getBoundingClientRect();
+                       var evt = {
+                               view: window,
+                               bubbles: true,
+                               cancelable: true,
+                               clientX: rect.left + point._view.x + 15,
+                               clientY: rect.top + point._view.y,
+                               currentTarget: node
+                       };
+
+                       // Nothing intersects so find nothing
+                       var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: true });
+                       expect(elements).toEqual([]);
+
+                       evt = {
+                               view: window,
+                               bubbles: true,
+                               cancelable: true,
+                               clientX: rect.left + point._view.x,
+                               clientY: rect.top + point._view.y,
+                               currentTarget: node
+                       };
+                       elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: true });
+                       expect(elements).toEqual([point]);
+               });
+
+               it ('should return the nearest item even if 2 intersect', function() {
+                       var chartInstance = window.acquireChart({
+                               type: 'line',
+                               data: {
+                                       datasets: [{
+                                               label: 'Dataset 1',
+                                               data: [10, 39, 30],
+                                               pointRadius: [5, 30, 5],
+                                               pointHoverBorderColor: 'rgb(255, 0, 0)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 0)'
+                                       }, {
+                                               label: 'Dataset 2',
+                                               data: [40, 40, 40],
+                                               pointRadius: [10, 10, 10],
+                                               pointHoverBorderColor: 'rgb(0, 0, 255)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 255)'
+                                       }],
+                                       labels: ['Point 1', 'Point 2', 'Point 3']
+                               }
+                       });
+
+                       // Trigger an event over top of the
+                       var meta0 = chartInstance.getDatasetMeta(0);
+
+                       // Halfway between 2 mid points
+                       var pt = {
+                               x: meta0.data[1]._view.x,
+                               y: meta0.data[1]._view.y
+                       };
+
+                       var node = chartInstance.chart.canvas;
+                       var rect = node.getBoundingClientRect();
+                       var evt = {
+                               view: window,
+                               bubbles: true,
+                               cancelable: true,
+                               clientX: rect.left + pt.x,
+                               clientY: rect.top + pt.y,
+                               currentTarget: node
+                       };
+
+                       // Nearest to 0,0 (top left) will be first point of dataset 2
+                       var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: true });
+                       expect(elements).toEqual([meta0.data[1]]);
+               });
+
+               it ('should return the smallest item if more than 1 are at the same distance', function() {
+                       var chartInstance = window.acquireChart({
+                               type: 'line',
+                               data: {
+                                       datasets: [{
+                                               label: 'Dataset 1',
+                                               data: [10, 40, 30],
+                                               pointRadius: [5, 5, 5],
+                                               pointHoverBorderColor: 'rgb(255, 0, 0)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 0)'
+                                       }, {
+                                               label: 'Dataset 2',
+                                               data: [40, 40, 40],
+                                               pointRadius: [10, 10, 10],
+                                               pointHoverBorderColor: 'rgb(0, 0, 255)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 255)'
+                                       }],
+                                       labels: ['Point 1', 'Point 2', 'Point 3']
+                               }
+                       });
+
+                       // Trigger an event over top of the
+                       var meta0 = chartInstance.getDatasetMeta(0);
+
+                       // Halfway between 2 mid points
+                       var pt = {
+                               x: meta0.data[1]._view.x,
+                               y: meta0.data[1]._view.y
+                       };
+
+                       var node = chartInstance.chart.canvas;
+                       var rect = node.getBoundingClientRect();
+                       var evt = {
+                               view: window,
+                               bubbles: true,
+                               cancelable: true,
+                               clientX: rect.left + pt.x,
+                               clientY: rect.top + pt.y,
+                               currentTarget: node
+                       };
+
+                       // Nearest to 0,0 (top left) will be first point of dataset 2
+                       var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: true });
+                       expect(elements).toEqual([meta0.data[1]]);
+               });
+
+               it ('should return the item at the lowest dataset index if distance and area are the same', function() {
+                       var chartInstance = window.acquireChart({
+                               type: 'line',
+                               data: {
+                                       datasets: [{
+                                               label: 'Dataset 1',
+                                               data: [10, 40, 30],
+                                               pointRadius: [5, 10, 5],
+                                               pointHoverBorderColor: 'rgb(255, 0, 0)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 0)'
+                                       }, {
+                                               label: 'Dataset 2',
+                                               data: [40, 40, 40],
+                                               pointRadius: [10, 10, 10],
+                                               pointHoverBorderColor: 'rgb(0, 0, 255)',
+                                               pointHoverBackgroundColor: 'rgb(0, 255, 255)'
+                                       }],
+                                       labels: ['Point 1', 'Point 2', 'Point 3']
+                               }
+                       });
+
+                       // Trigger an event over top of the
+                       var meta0 = chartInstance.getDatasetMeta(0);
+
+                       // Halfway between 2 mid points
+                       var pt = {
+                               x: meta0.data[1]._view.x,
+                               y: meta0.data[1]._view.y
+                       };
+
+                       var node = chartInstance.chart.canvas;
+                       var rect = node.getBoundingClientRect();
+                       var evt = {
+                               view: window,
+                               bubbles: true,
+                               cancelable: true,
+                               clientX: rect.left + pt.x,
+                               clientY: rect.top + pt.y,
+                               currentTarget: node
+                       };
+
+                       // Nearest to 0,0 (top left) will be first point of dataset 2
+                       var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: true });
+                       expect(elements).toEqual([meta0.data[1]]);
+               });
+       });
+});
index 4ba8545754cad845a0aad27f5d14a83653aeceab..7ce70517d2484ef27204f1c754ecd45417fc7d98 100644 (file)
@@ -60,6 +60,46 @@ describe('Arc element tests', function() {
                expect(pos.y).toBeCloseTo(0.5);
        });
 
+       it ('should get the area', function() {
+               var arc = new Chart.elements.Arc({
+                       _datasetIndex: 2,
+                       _index: 1
+               });
+
+               // Mock out the view as if the controller put it there
+               arc._view = {
+                       startAngle: 0,
+                       endAngle: Math.PI / 2,
+                       x: 0,
+                       y: 0,
+                       innerRadius: 0,
+                       outerRadius: Math.sqrt(2),
+               };
+
+               expect(arc.getArea()).toBeCloseTo(0.5 * Math.PI, 6);
+       });
+
+       it ('should get the center', function() {
+               var arc = new Chart.elements.Arc({
+                       _datasetIndex: 2,
+                       _index: 1
+               });
+
+               // Mock out the view as if the controller put it there
+               arc._view = {
+                       startAngle: 0,
+                       endAngle: Math.PI / 2,
+                       x: 0,
+                       y: 0,
+                       innerRadius: 0,
+                       outerRadius: Math.sqrt(2),
+               };
+
+               var center = arc.getCenterPoint();
+               expect(center.x).toBeCloseTo(0.5, 6);
+               expect(center.y).toBeCloseTo(0.5, 6);
+       });
+
        it ('should draw correctly with no border', function() {
                var mockContext = window.createMockContext();
                var arc = new Chart.elements.Arc({
index c257f7375ca5d7fd05a638ec151f3c6defcacf25..9cbac8ea5607a1c6d6a4894a160f467afb5035c0 100644 (file)
@@ -64,6 +64,36 @@ describe('Point element tests', function() {
                });
        });
 
+       it('should get the correct area', function() {
+               var point = new Chart.elements.Point({
+                       _datasetIndex: 2,
+                       _index: 1
+               });
+
+               // Attach a view object as if we were the controller
+               point._view = {
+                       radius: 2,
+               };
+
+               expect(point.getArea()).toEqual(Math.PI * 4);
+       });
+
+       it('should get the correct center point', function() {
+               var point = new Chart.elements.Point({
+                       _datasetIndex: 2,
+                       _index: 1
+               });
+
+               // Attach a view object as if we were the controller
+               point._view = {
+                       radius: 2,
+                       x: 10,
+                       y: 10
+               };
+
+               expect(point.getCenterPoint()).toEqual({ x: 10, y: 10 });
+       });
+
        it ('should draw correctly', function() {
                var mockContext = window.createMockContext();
                var point = new Chart.elements.Point({
index 8c28970b1c3008826317297e0039111b1483d2e9..bd32ff3ac233d4a26a3f8d1ac5bcea72b6046a35 100644 (file)
@@ -132,6 +132,40 @@ describe('Rectangle element tests', function() {
                });
        });
 
+       it ('should get the correct area', function() {
+               var rectangle = new Chart.elements.Rectangle({
+                       _datasetIndex: 2,
+                       _index: 1
+               });
+
+               // Attach a view object as if we were the controller
+               rectangle._view = {
+                       base: 0,
+                       width: 4,
+                       x: 10,
+                       y: 15
+               };
+
+               expect(rectangle.getArea()).toEqual(60);
+       });
+
+       it ('should get the center', function() {
+               var rectangle = new Chart.elements.Rectangle({
+                       _datasetIndex: 2,
+                       _index: 1
+               });
+
+               // Attach a view object as if we were the controller
+               rectangle._view = {
+                       base: 0,
+                       width: 4,
+                       x: 10,
+                       y: 15
+               };
+
+               expect(rectangle.getCenterPoint()).toEqual({ x: 10, y: 7.5 });
+       });
+
        it ('should draw correctly', function() {
                var mockContext = window.createMockContext();
                var rectangle = new Chart.elements.Rectangle({