最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to throttle function call on mouse event with D3.js - Stack Overflow

programmeradmin1浏览0评论

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 putationally 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 : /javascript-debounce-function And also this : But I'm unable to get those to work the way I want.

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 putationally 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./2010/07/21/throttling-function-calls But I'm unable to get those to work the way I want.

Share edited Apr 14, 2017 at 8:54 GuitarExtended asked Apr 14, 2017 at 8:28 GuitarExtendedGuitarExtended 8272 gold badges12 silver badges36 bronze badges 4
  • can you try d3.select("#myelement").mousemove(function(e){ console.log(e); }) – cbalakus Commented Apr 14, 2017 at 8:51
  • 2 @cbalakus There is no mousemove method in D3. – Gerardo Furtado Commented Apr 14, 2017 at 8:53
  • 1 ".mousemove()" is not a function on a d3 selection, if I'm not mistaken. – GuitarExtended Commented Apr 14, 2017 at 8:53
  • If you strictly want to limit the calls of your function, just count. Set a counter to 0 outside your function, then in the function increase the counter until it reaches a threshold, and then do your action and reset the counter to 0. – D.Ogranos Commented Apr 10, 2019 at 8:36
Add a ment  | 

4 Answers 4

Reset to default 5

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/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.

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.

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./docs/#throttle

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.

  • Minimal overhead.
  • Immediate function call if it has been more than interval MS since the last call.
  • Avoiding executing function for another interval MS.
  • Delaying excessive event firing instead of dropping the event altogether.
  • Updates the delayed event when need be so that it doesn't bee 'stale'.
  • Prevents the default action of the event when the throttled function is delayed.
  • Be able to remove the throttle event listener listener.

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.

发布评论

评论列表(0)

  1. 暂无评论