Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support hover events #44

Merged
merged 1 commit into from
Dec 2, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,8 @@ To configure the annotations plugin, you can simply add new config options to yo
drawTime: 'afterDraw', // (default)

// Mouse events to enable on each annotation.
// Similar to the `events` common chart configuration option
// http://www.chartjs.org/docs/#chart-configuration-common-chart-configuration
// Should be an array of: mousemove, mousedown, mouseup, click, dblclick, contextmenu, wheel
// Should be an array of one or more browser-supported mouse events
// See https://developer.mozilla.org/en-US/docs/Web/Events
events: ['click']
}
}
Expand Down Expand Up @@ -124,6 +123,12 @@ Vertical or horizontal lines are supported.

// Mouse event handlers - be sure to enable the corresponding events in the
// annotation events array or the event handler will not be called.
// See https://developer.mozilla.org/en-US/docs/Web/Events for a list of
// supported mouse events.
onMouseenter: function(e) {},
Copy link
Member

Choose a reason for hiding this comment

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

Would it be a good idea to support touch events as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Eventually, yes, but it will need some care to do it right in a way that works cross-browser and also works for the user. Probably need some hit radius logic added, and integration with Hammer.js. That's more than I can get into right now.

onMouseover: function(e) {},
onMouseleave: function(e) {},
onMouseout: function(e) {},
onMousemove: function(e) {},
onMousedown: function(e) {},
onMouseup: function(e) {},
Expand Down Expand Up @@ -172,6 +177,12 @@ The 4 coordinates, xMin, xMax, yMin, yMax are optional. If not specified, the bo

// Mouse event handlers - be sure to enable the corresponding events in the
// annotation events array or the event handler will not be called.
// See https://developer.mozilla.org/en-US/docs/Web/Events for a list of
// supported mouse events.
onMouseenter: function(e) {},
onMouseover: function(e) {},
onMouseleave: function(e) {},
onMouseout: function(e) {},
onMousemove: function(e) {},
onMousedown: function(e) {},
onMouseup: function(e) {},
Expand Down
35 changes: 27 additions & 8 deletions samples/horizontal-line.html
Original file line number Diff line number Diff line change
Expand Up @@ -127,22 +127,41 @@
}]
},
annotation: {
events: ['click'],
events: ['click', 'dblclick', 'mouseover', 'mouseout'],
annotations: [{
type: 'line',
mode: 'horizontal',
scaleID: 'y-axis-1',
value: 120,
borderColor: 'black',
borderWidth: 5,
borderColor: 'rgba(255, 0, 0, 0.5)',
borderWidth: 2,
onClick: function(e) {
// The annotation is is bound to the `this` variable
alert('Annotation ' + e.type);
this._model.borderColor = 'rgba(255, 0, 0, 1.0)';
this.chartInstance.render(0);
setTimeout((function() {
this._model.borderColor = 'rgba(255, 0, 0, 0.5)';
this.chartInstance.render(0);
}).bind(this), 500);
},
onDblclick: function(e) {
this._model.borderColor = 'rgba(0, 255, 0, 1.0)';
this.chartInstance.render(0);
setTimeout((function() {
this._model.borderColor = 'rgba(255, 0, 0, 0.5)';
this.chartInstance.render(0);
}).bind(this), 500);
},
onMouseover: function(e) {
this._model.borderWidth = 7;
this.chartInstance.render(0);
this.chartInstance.chart.canvas.style.cursor = 'pointer';
},
onMouseout: function(e) {
this._model.borderWidth = 5;
this.chartInstance.render(0);
this.chartInstance.chart.canvas.style.cursor = 'initial';
}
}]
},
onClick: function(e, annotation) {
alert('Chart ' + e.type);
}
}
});
Expand Down
4 changes: 4 additions & 0 deletions src/element.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
module.exports = function(Chart) {
var chartHelpers = Chart.helpers;

var AnnotationElement = Chart.Element.extend({
initialize: function() {
this.hidden = false;
this.hovering = false;
this._model = chartHelpers.clone(this._model) || {};
this.setDataLimits();
},
destroy: function() {},
Expand Down
148 changes: 148 additions & 0 deletions src/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
var helpers = require('./helpers.js');

module.exports = function(Chart) {
var chartHelpers = Chart.helpers;

var dblClickSpeed = Chart.Annotation.dblClickSpeed || 350; // ms

function getEventHandlerName(eventName) {
return 'on' + eventName[0].toUpperCase() + eventName.substring(1);
}

function createMouseEvent(type, previousEvent) {
try {
return new MouseEvent(type, previousEvent);
} catch (exception) {
try {
var m = document.createEvent('MouseEvent');
m.initMouseEvent(
type,
previousEvent.canBubble,
previousEvent.cancelable,
previousEvent.view,
previousEvent.detail,
previousEvent.screenX,
previousEvent.screenY,
previousEvent.clientX,
previousEvent.clientY,
previousEvent.ctrlKey,
previousEvent.altKey,
previousEvent.shiftKey,
previousEvent.metaKey,
previousEvent.button,
previousEvent.relatedTarget
);
return m;
} catch (exception2) {
var e = document.createEvent('Event');
e.initEvent(
type,
previousEvent.canBubble,
previousEvent.cancelable
);
return e;
}
}
}

function collapseHoverEvents(events) {
var hover = false;
var filteredEvents = events.filter(function(eventName) {
switch (eventName) {
case 'mouseenter':
case 'mouseover':
case 'mouseout':
case 'mouseleave':
hover = true;
return false;

default:
return true;
}
});
if (hover && filteredEvents.indexOf('mousemove') === -1) {
filteredEvents.push('mousemove');
}
return filteredEvents;
}

function dispatcher(e) {
var position = chartHelpers.getRelativePosition(e, this.chart);
var element = helpers.getNearestItems(this.annotations, position);
var events = collapseHoverEvents(this.options.annotation.events);
var eventHandlers = [];
var eventHandlerName = getEventHandlerName(e.type);
var options = (element || {}).options;

// Detect hover events
if (e.type === 'mousemove') {
if (element && !element.hovering) {
// hover started
['mouseenter', 'mouseover'].forEach(function(eventName) {
var eventHandlerName = getEventHandlerName(eventName);
var hoverEvent = createMouseEvent(eventName, e); // recreate the event to match the handler
element.hovering = true;
if (typeof options[eventHandlerName] === 'function') {
eventHandlers.push([ options[eventHandlerName], hoverEvent, element ]);
}
});
} else if (!element) {
// hover ended
this.annotations.forEach(function(element) {
if (element.hovering) {
element.hovering = false;
var options = element.options;
['mouseout', 'mouseleave'].forEach(function(eventName) {
var eventHandlerName = getEventHandlerName(eventName);
var hoverEvent = createMouseEvent(eventName, e); // recreate the event to match the handler
if (typeof options[eventHandlerName] === 'function') {
eventHandlers.push([ options[eventHandlerName], hoverEvent, element ]);
}
});
}
});
}
}

// Suppress duplicate click events during a double click
Copy link
Member

Choose a reason for hiding this comment

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

I like this comment. Very detailed :)

// 1. click -> 2. click -> 3. dblclick
//
// 1: wait dblClickSpeed ms, then fire click
// 2: cancel (1) if it is waiting then wait dblClickSpeed ms then fire click, else fire click immediately
// 3: cancel (1) or (2) if waiting, then fire dblclick
if (element && events.indexOf('dblclick') > -1 && typeof options.onDblclick === 'function') {
if (e.type === 'click' && typeof options.onClick === 'function') {
clearTimeout(element.clickTimeout);
element.clickTimeout = setTimeout(function() {
delete element.clickTimeout;
options.onClick.call(element, e);
}, dblClickSpeed);
e.stopImmediatePropagation();
e.preventDefault();
return;
} else if (e.type === 'dblclick' && element.clickTimeout) {
clearTimeout(element.clickTimeout);
delete element.clickTimeout;
}
}

// Dispatch the event to the usual handler, but only if we haven't substituted it
if (element && typeof options[eventHandlerName] === 'function' && eventHandlers.length === 0) {
eventHandlers.push([ options[eventHandlerName], e, element ]);
}

if (eventHandlers.length > 0) {
e.stopImmediatePropagation();
e.preventDefault();
eventHandlers.forEach(function(eventHandler) {
// [handler, event, element]
eventHandler[0].call(eventHandler[2], eventHandler[1]);
});
}
}

return {
dispatcher: dispatcher,
collapseHoverEvents: collapseHoverEvents
};
};
5 changes: 0 additions & 5 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ function decorate(obj, prop, func) {
}
}

function getEventHandlerName(eventName) {
return 'on' + eventName[0].toUpperCase() + eventName.substring(1);
}

function getScaleLimits(scaleId, annotations, scaleMin, scaleMax) {
var ranges = annotations.filter(function(annotation) {
return !!annotation._model.ranges[scaleId];
Expand Down Expand Up @@ -78,7 +74,6 @@ function getNearestItems(annotations, position) {
module.exports = {
isValid: isValid,
decorate: decorate,
getEventHandlerName: getEventHandlerName,
getScaleLimits: getScaleLimits,
getNearestItems: getNearestItems
};
32 changes: 12 additions & 20 deletions src/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ var helpers = require('./helpers.js');
// Configure plugin namespace
Chart.Annotation = Chart.Annotation || {};

Chart.Annotation.dblClickSpeed = 350; // ms

var events = require('./events.js')(Chart);

var DRAW_AFTER = 'afterDraw';
var DRAW_AFTER_DATASETS = 'afterDatasetsDraw';
var DRAW_BEFORE_DATASETS = 'beforeDatasetsDraw';
Expand Down Expand Up @@ -87,18 +91,6 @@ function build(configs, chartInstance) {
});
}

function eventDispatcher(e) {
var position = chartHelpers.getRelativePosition(e, this.chart);
var element = helpers.getNearestItems(this.annotations, position);
var eventHandlerName = helpers.getEventHandlerName(e.type);
var options = (element || {}).options;
if (element && options[eventHandlerName]) {
e.stopImmediatePropagation();
e.preventDefault();
options[eventHandlerName].call(element, e);
}
}

var annotationPlugin = {
beforeInit: function(chartInstance) {
chartInstance.annotations = [];
Expand Down Expand Up @@ -131,20 +123,20 @@ var annotationPlugin = {

// Detect and intercept events that happen on an annotation element
var config = chartInstance.options.annotation || {};
if (config.events) {
chartInstance._annotationEventHandler = eventDispatcher.bind(chartInstance);
config.events.forEach(function(eventName) {
var watchFor = config.events || [];
if (watchFor) {
chartInstance._annotationEventHandler = events.dispatcher.bind(chartInstance);
events.collapseHoverEvents(watchFor).forEach(function(eventName) {
chartHelpers.addEvent(chartInstance.chart.canvas, eventName, chartInstance._annotationEventHandler);
});
}
},
destroy: function(chartInstance) {
var config = chartInstance.annotations._config;
if (config.events.length > 0) {
config.events.forEach(function(eventName) {
chartHelpers.removeEvent(chartInstance.chart.canvas, eventName, chartInstance._annotationEventHandler);
});
}
var events = config.events || [];
events.forEach(function(eventName) {
chartHelpers.removeEvent(chartInstance.chart.canvas, eventName, chartInstance._annotationEventHandler);
});
},
beforeUpdate: function(chartInstance) {
// Build the configuration with all the defaults set
Expand Down
26 changes: 15 additions & 11 deletions src/types/box.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ var helpers = require('../helpers.js');
module.exports = function(Chart) {
var BoxAnnotation = Chart.Annotation.Element.extend({
setDataLimits: function() {
var model = this._model = this._model || {};
var model = this._model;
var options = this.options;
var chartInstance = this.chartInstance;

Expand Down Expand Up @@ -36,7 +36,7 @@ module.exports = function(Chart) {
}
},
configure: function() {
var model = this._model = this._model || {};
var model = this._model;
var options = this.options;
var chartInstance = this.chartInstance;

Expand Down Expand Up @@ -85,23 +85,27 @@ module.exports = function(Chart) {
model.backgroundColor = options.backgroundColor;
},
inRange: function(mouseX, mouseY) {
return this._view &&
mouseX >= this._view.left &&
mouseX <= this._view.right &&
mouseY >= this._view.top &&
mouseY <= this._view.bottom;
var model = this._model;
return model &&
mouseX >= model.left &&
mouseX <= model.right &&
mouseY >= model.top &&
mouseY <= model.bottom;
},
getCenterPoint: function() {
var model = this._model;
return {
x: (this._view.right + this._view.left) / 2,
y: (this._view.bottom + this._view.top) / 2
x: (model.right + model.left) / 2,
y: (model.bottom + model.top) / 2
};
},
getWidth: function() {
return Math.abs(this._view.right - this._view.left);
var model = this._model;
return Math.abs(model.right - model.left);
},
getHeight: function() {
return Math.abs(this._view.bottom - this._view.top);
var model = this._model;
return Math.abs(model.bottom - model.top);
},
getArea: function() {
return this.getWidth() * this.getHeight();
Expand Down
Loading