From: Evert Timberg Date: Mon, 3 Oct 2016 20:05:21 +0000 (-0400) Subject: Improve Tooltip and Hover Interaction (#3400) X-Git-Tag: v2.4.0~1^2~37 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=03735563f44e1ae108f83b9c7ee946f8818c92c3;p=thirdparty%2FChart.js.git Improve Tooltip and Hover Interaction (#3400) Refactored interaction modes to use lookup functions in Chart.Interaction.modes and added new modes for 'point', 'index', 'nearest', 'x', and 'y' --- diff --git a/docs/01-Chart-Configuration.md b/docs/01-Chart-Configuration.md index dd16e6d2c..5ed712a97 100644 --- a/docs/01-Chart-Configuration.md +++ b/docs/01-Chart-Configuration.md @@ -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'`.
 
`single` highlights the closest element.
 
`label` highlights elements in all datasets at the same `X` value.
 
`'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'`.
 
`single` highlights the closest element.
 
`label` highlights elements in all datasets at the same `X` value.
 
`'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.
 
`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`. diff --git a/samples/AnimationCallbacks/progress-bar.html b/samples/AnimationCallbacks/progress-bar.html index 7d79d6c1d..fad7bd489 100644 --- a/samples/AnimationCallbacks/progress-bar.html +++ b/samples/AnimationCallbacks/progress-bar.html @@ -75,7 +75,7 @@ } }, tooltips: { - mode: 'label', + mode: 'index', }, scales: { xAxes: [{ diff --git a/samples/bar-multi-axis.html b/samples/bar-multi-axis.html index 9c8ec3642..ee027aaf8 100644 --- a/samples/bar-multi-axis.html +++ b/samples/bar-multi-axis.html @@ -56,7 +56,7 @@ data: barChartData, options: { responsive: true, - hoverMode: 'label', + hoverMode: 'index', hoverAnimationDuration: 400, stacked: false, title:{ diff --git a/samples/bar-stacked.html b/samples/bar-stacked.html index e85a72b26..685af83db 100644 --- a/samples/bar-stacked.html +++ b/samples/bar-stacked.html @@ -55,7 +55,7 @@ text:"Chart.js Bar Chart - Stacked" }, tooltips: { - mode: 'label' + mode: 'index' }, responsive: true, scales: { diff --git a/samples/different-point-sizes.html b/samples/different-point-sizes.html index 926eecfba..0fb231f52 100644 --- a/samples/different-point-sizes.html +++ b/samples/different-point-sizes.html @@ -70,7 +70,7 @@ position: 'bottom', }, hover: { - mode: 'label' + mode: 'index' }, scales: { xAxes: [{ diff --git a/samples/line-cubicInterpolationMode.html b/samples/line-cubicInterpolationMode.html index 97dac46ca..2d85c95c9 100644 --- a/samples/line-cubicInterpolationMode.html +++ b/samples/line-cubicInterpolationMode.html @@ -68,7 +68,7 @@ text:'Chart.js Line Chart - Cubic interpolation mode' }, tooltips: { - mode: 'label' + mode: 'index' }, hover: { mode: 'dataset' diff --git a/samples/line-legend.html b/samples/line-legend.html index 92e5e5b5a..201d4c07e 100644 --- a/samples/line-legend.html +++ b/samples/line-legend.html @@ -68,7 +68,7 @@ position: 'bottom', }, hover: { - mode: 'label' + mode: 'index' }, scales: { xAxes: [{ diff --git a/samples/line-multi-axis.html b/samples/line-multi-axis.html index 03da24bda..0b296ff69 100644 --- a/samples/line-multi-axis.html +++ b/samples/line-multi-axis.html @@ -54,7 +54,7 @@ data: lineChartData, options: { responsive: true, - hoverMode: 'label', + hoverMode: 'index', stacked: false, title:{ display:true, diff --git a/samples/line-multiline-labels.html b/samples/line-multiline-labels.html index 3fd0de5e1..b9d08aee7 100644 --- a/samples/line-multiline-labels.html +++ b/samples/line-multiline-labels.html @@ -65,7 +65,7 @@ text:'Chart.js Line Chart' }, tooltips: { - mode: 'label', + mode: 'index', callbacks: { // beforeTitle: function() { // return '...beforeTitle'; diff --git a/samples/line-skip-points.html b/samples/line-skip-points.html index 2d760d2e1..3ef275587 100644 --- a/samples/line-skip-points.html +++ b/samples/line-skip-points.html @@ -64,10 +64,10 @@ text:'Chart.js Line Chart - Skip Points' }, tooltips: { - mode: 'label', + mode: 'index', }, hover: { - mode: 'label' + mode: 'index' }, scales: { xAxes: [{ diff --git a/samples/line-stacked-area.html b/samples/line-stacked-area.html index 88f14e2bd..26616f5de 100644 --- a/samples/line-stacked-area.html +++ b/samples/line-stacked-area.html @@ -63,10 +63,10 @@ text:"Chart.js Line Chart - Stacked Area" }, tooltips: { - mode: 'label', + mode: 'index', }, hover: { - mode: 'label' + mode: 'index' }, scales: { xAxes: [{ diff --git a/samples/line-stepped.html b/samples/line-stepped.html index e618a698c..f6cebe262 100644 --- a/samples/line-stepped.html +++ b/samples/line-stepped.html @@ -68,7 +68,7 @@ text:'Chart.js Line Chart' }, tooltips: { - mode: 'label', + mode: 'index', callbacks: { // beforeTitle: function() { // return '...beforeTitle'; diff --git a/samples/line.html b/samples/line.html index ffca9df3b..464072ba1 100644 --- a/samples/line.html +++ b/samples/line.html @@ -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: [{ diff --git a/samples/scatter-multi-axis.html b/samples/scatter-multi-axis.html index 43d27e502..842f8891a 100644 --- a/samples/scatter-multi-axis.html +++ b/samples/scatter-multi-axis.html @@ -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' diff --git a/samples/tooltip-hooks.html b/samples/tooltip-hooks.html index 88a660515..08055ece8 100644 --- a/samples/tooltip-hooks.html +++ b/samples/tooltip-hooks.html @@ -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: [{ diff --git a/src/chart.js b/src/chart.js index aa0d46c45..2c5e62826 100644 --- a/src/chart.js +++ b/src/chart.js @@ -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); diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index d9fa7bfeb..f828eb79e 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -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; } }); diff --git a/src/core/core.controller.js b/src/core/core.controller.js index b2928c6b8..9712d8fb6 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -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 diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 6d63284a7..d3fd72741 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -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 index 000000000..571f3c3c0 --- /dev/null +++ b/src/core/core.interaction.js @@ -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; + } + } + }; +}; diff --git a/src/core/core.js b/src/core/core.js index d28997838..319219f5d 100755 --- a/src/core/core.js +++ b/src/core/core.js @@ -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, diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index d0068ee67..9210ed529 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -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, diff --git a/src/elements/element.arc.js b/src/elements/element.arc.js index b114e6f04..a61d19a3a 100644 --- a/src/elements/element.arc.js +++ b/src/elements/element.arc.js @@ -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; diff --git a/src/elements/element.point.js b/src/elements/element.point.js index 474ef252f..fe3a6696b 100644 --- a/src/elements/element.point.js +++ b/src/elements/element.point.js @@ -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; diff --git a/src/elements/element.rectangle.js b/src/elements/element.rectangle.js index 6752228c8..3f4ea10f9 100644 --- a/src/elements/element.rectangle.js +++ b/src/elements/element.rectangle.js @@ -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 index 000000000..312ac8fe6 --- /dev/null +++ b/test/core.interaction.tests.js @@ -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]]); + }); + }); +}); diff --git a/test/element.arc.tests.js b/test/element.arc.tests.js index 4ba854575..7ce70517d 100644 --- a/test/element.arc.tests.js +++ b/test/element.arc.tests.js @@ -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({ diff --git a/test/element.point.tests.js b/test/element.point.tests.js index c257f7375..9cbac8ea5 100644 --- a/test/element.point.tests.js +++ b/test/element.point.tests.js @@ -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({ diff --git a/test/element.rectangle.tests.js b/test/element.rectangle.tests.js index 8c28970b1..bd32ff3ac 100644 --- a/test/element.rectangle.tests.js +++ b/test/element.rectangle.tests.js @@ -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({