Reputation: 827
I'm calling a function on the "mousemove" event of a DOM element, using D3.js 's .on()
, like so :
d3.select("#myelement").on("mousemove", myfunc);
function myfunc(){
// Just to show that I need to get the mouse coordinates here
console.log(d3.mouse(this));
}
I need the function I'm calling to be aware of the event, i.e. the mouse coordinates.
Since the rest of my code is quite computationally expensive, I'd like to throttle the calls to myfunc
, say every 200 ms.
How can I do that while preserving the value of this
in myfunc
(so that d3.mouse(this)
still works) ?
I've tried this debounce function : https://davidwalsh.name/javascript-debounce-function
And also this : https://remysharp.com/2010/07/21/throttling-function-calls
But I'm unable to get those to work the way I want.
Upvotes: 5
Views: 4779
Reputation: 5322
You do not need a big segment of code or an oversized library like D3js for a decent throttle function. The purpose of a throttle function is to reduce browser resources, not to apply so much overhead that you are using even more. Also, my different uses for throttle functions require many different circumstances for them. Here is my list of things that a 'good' throttle function needs that this one has.
And, I believe that the following throttle function satisfies all of those.
var cachedThrottleFuncs = [],
minimumInterval = 200; // minimum interval between throttled function calls
function throttle(func, obj, evt) {
var timeouttype = 0,
curFunc;
function lowerTimeoutType(f){
timeouttype=0;
if (curFunc !== undefined){
curFunc();
curFunc = undefined;
}
};
return cachedThrottleFuncs[ ~(
~cachedThrottleFuncs.indexOf(func) ||
~(
cachedThrottleFuncs.push(function(Evt) {
switch (timeouttype){
case 0: // Execute immediatly
++timeouttype;
func.call(Evt.target, Evt);
setTimeout(lowerTimeoutType, minimumInterval);
break;
case 1: // Delayed execute
curFunc = func.bind(Evt.target, Evt);
Evt.preventDefault();
}
}) - 1
)
)];
};
function listen(obj, evt, func){
obj.addEventListener(evt, throttle(func, obj, evt));
};
function mute(obj, evt, func){
obj.removeEventListener(evt, throttle(func, obj, evt));
}
Example usage:
listen(document.body, 'scroll', function whenbodyscrolls(){
if (document.body.scrollTop > 400)
mute(document.body, 'scroll', whenbodyscrolls();
else
console.log('Body scrolled!')
});
Alternatively, if you only need to add event listeners, and you do not need to remove event listeners, then you can use the following even simpler version.
var minimumInterval = 200; // minimum interval between throttled function calls
function throttle(func, obj, evt) {
var timeouttype = 0,
curEvt = null;
function lowerTimeoutType(f){
timeouttype=0;
if (curEvt !== null){
func(curEvt);
curEvt = null;
}
};
return function(Evt) {
switch (timeouttype){
case 0: // Execute immediately
++timeouttype; // increase the timeouttype
func(Evt);
// Now, make it so that the timeouttype resets later
setTimeout(lowerTimeoutType, minimumInterval);
break;
case 1: // Delayed execute
// make it so that when timeouttype expires, your function
// is called with the freshest event
curEvt = Evt;
Evt.preventDefault();
}
};
};
By default, this throttles the function to at most one call every 200ms. To change the interval to a different number of milliseconds, then simply change the value of minimumInterval
.
Upvotes: 0
Reputation: 102174
The problem is not passing this
to the debounce function, which is quite easy, as you can see in this JSFiddle (I'm linking a JSFiddle because the Stack snippet freezes when logging this
or a D3 selection).
The real problem is passing the D3 event: since d3.event
is null after the event has finished, you have to keep a reference to it. Otherwise, you'll have a Cannot read property 'sourceEvent' of null
error when trying to use d3.mouse()
.
So, using the function of your second link, we can modify it to keep a reference to the D3 event:
function debounce(fn, delay) {
var timer = null;
return function() {
var context = this,
args = arguments,
evt = d3.event;
//we get the D3 event here
clearTimeout(timer);
timer = setTimeout(function() {
d3.event = evt;
//and use the reference here
fn.apply(context, args);
}, delay);
};
}
Here is the demo, hover over the big circle, slowly moving your mouse:
var circle = d3.select("circle");
circle.on("mousemove", debounce(function() {
console.log(d3.mouse(this));
}, 250));
function debounce(fn, delay) {
var timer = null;
return function() {
var context = this,
args = arguments,
evt = d3.event;
clearTimeout(timer);
timer = setTimeout(function() {
d3.event = evt;
fn.apply(context, args);
}, delay);
};
}
.as-console-wrapper { max-height: 30% !important;}
<script src="https://d3js.org/d3.v4.js"></script>
<svg>
<circle cx="120" cy="80" r="50" fill="teal"></circle>
</svg>
PS: In both the JSFiddle and in the Stack snippet the function is called only when you stop moving the mouse, which is not the desired behaviour for a mousemove
. I'll keep working on it.
Upvotes: 5
Reputation: 827
Thanks to Gerardo Furtado's answer, I managed to solve my problem by adapting the throttle function from this page like so :
function throttle(fn, threshhold, scope) {
threshhold || (threshhold = 250);
var last,
deferTimer;
return function () {
var context = scope || this;
var now = +new Date,
args = arguments,
event = d3.event;
if (last && now < last + threshhold) {
// hold on to it
clearTimeout(deferTimer);
deferTimer = setTimeout(function () {
last = now;
d3.event = event;
fn.apply(context, args);
}, threshhold);
} else {
last = now;
d3.event = event;
fn.apply(context, args);
}
};
}
});
Now the callback is aware of the d3.event and d3.mouse(this) can be used normally inside the function.
Upvotes: 2
Reputation: 6436
Edit: I am not sure if it would preserve the this
keyword in your case though, you can give it try.
Instead of coding your own throttle
or debounce
, you could simply use a helper library like lodash and pass your function to the API to get a throttled version of your function:
Example with jQuery, but should also work with d3:
// Avoid excessively updating the position while scrolling.
jQuery(window).on('scroll', _.throttle(updatePosition, 100));
https://lodash.com/docs/#throttle
Upvotes: 0