diff --git a/samples/legend/callbacks.html b/samples/legend/callbacks.html new file mode 100644 index 00000000000..aa71857489e --- /dev/null +++ b/samples/legend/callbacks.html @@ -0,0 +1,124 @@ + + + + Legend Callbacks + + + + + + +
+
+ +
+ + + + diff --git a/samples/samples.js b/samples/samples.js index 29ed1ff8190..bb0463f7e6e 100644 --- a/samples/samples.js +++ b/samples/samples.js @@ -148,6 +148,9 @@ }, { title: 'Point style', path: 'legend/point-style.html' + }, { + title: 'Callbacks', + path: 'legend/callbacks.html' }] }, { title: 'Tooltip', diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 2e832f98afd..2c6870b279d 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -30,6 +30,7 @@ defaults._set('global', { }, onHover: null, + onLeave: null, labels: { boxWidth: 40, @@ -106,6 +107,11 @@ var Legend = Element.extend({ // Contains hit boxes for each dataset (in dataset order) this.legendHitBoxes = []; + /** + * @private + */ + this._hoveredItem = null; + // Are we in doughnut mode which has a different data type this.doughnutMode = false; }, @@ -458,20 +464,42 @@ var Legend = Element.extend({ } }, + /** + * @private + */ + _getLegendItemAt: function(x, y) { + var me = this; + var i, hitBox, lh; + + if (x >= me.left && x <= me.right && y >= me.top && y <= me.bottom) { + // See if we are touching one of the dataset boxes + lh = me.legendHitBoxes; + for (i = 0; i < lh.length; ++i) { + hitBox = lh[i]; + + if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) { + // Touching an element + return me.legendItems[i]; + } + } + } + + return null; + }, + /** * Handle an event * @private * @param {IEvent} event - The event to handle - * @return {boolean} true if a change occured */ handleEvent: function(e) { var me = this; var opts = me.options; var type = e.type === 'mouseup' ? 'click' : e.type; - var changed = false; + var hoveredItem; if (type === 'mousemove') { - if (!opts.onHover) { + if (!opts.onHover && !opts.onLeave) { return; } } else if (type === 'click') { @@ -483,33 +511,26 @@ var Legend = Element.extend({ } // Chart event already has relative position in it - var x = e.x; - var y = e.y; + hoveredItem = me._getLegendItemAt(e.x, e.y); - if (x >= me.left && x <= me.right && y >= me.top && y <= me.bottom) { - // See if we are touching one of the dataset boxes - var lh = me.legendHitBoxes; - for (var i = 0; i < lh.length; ++i) { - var hitBox = lh[i]; - - if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) { - // Touching an element - if (type === 'click') { - // use e.native for backwards compatibility - opts.onClick.call(me, e.native, me.legendItems[i]); - changed = true; - break; - } else if (type === 'mousemove') { - // use e.native for backwards compatibility - opts.onHover.call(me, e.native, me.legendItems[i]); - changed = true; - break; - } + if (type === 'click') { + if (hoveredItem && opts.onClick) { + // use e.native for backwards compatibility + opts.onClick.call(me, e.native, hoveredItem); + } + } else { + if (opts.onLeave && hoveredItem !== me._hoveredItem) { + if (me._hoveredItem) { + opts.onLeave.call(me, e.native, me._hoveredItem); } + me._hoveredItem = hoveredItem; } - } - return changed; + if (opts.onHover && hoveredItem) { + // use e.native for backwards compatibility + opts.onHover.call(me, e.native, hoveredItem); + } + } } }); diff --git a/test/specs/plugin.legend.tests.js b/test/specs/plugin.legend.tests.js index fee715bdb13..e970c596a82 100644 --- a/test/specs/plugin.legend.tests.js +++ b/test/specs/plugin.legend.tests.js @@ -11,6 +11,7 @@ describe('Legend block tests', function() { // a callback that will handle onClick: jasmine.any(Function), onHover: null, + onLeave: null, labels: { boxWidth: 40, @@ -653,4 +654,53 @@ describe('Legend block tests', function() { expect(chart.legend.options).toEqual(jasmine.objectContaining(Chart.defaults.global.legend)); }); }); + + describe('callbacks', function() { + it('should call onClick, onHover and onLeave at the correct times', function() { + var clickItem = null; + var hoverItem = null; + var leaveItem = null; + + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + legend: { + onClick: function(_, item) { + clickItem = item; + }, + onHover: function(_, item) { + hoverItem = item; + }, + onLeave: function(_, item) { + leaveItem = item; + } + } + } + }); + + var hb = chart.legend.legendHitBoxes[0]; + var el = { + x: hb.left + (hb.width / 2), + y: hb.top + (hb.height / 2) + }; + + jasmine.triggerMouseEvent(chart, 'click', el); + + expect(clickItem).toBe(chart.legend.legendItems[0]); + + jasmine.triggerMouseEvent(chart, 'mousemove', el); + + expect(hoverItem).toBe(chart.legend.legendItems[0]); + + jasmine.triggerMouseEvent(chart, 'mousemove', chart.getDatasetMeta(0).data[0]); + + expect(leaveItem).toBe(chart.legend.legendItems[0]); + }); + }); });