Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to throttle function call on mouse event with D3.js

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.

like image 736
GuitarExtended Avatar asked Apr 14 '17 08:04

GuitarExtended


2 Answers

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.

like image 84
Gerardo Furtado Avatar answered Sep 28 '22 07:09

Gerardo Furtado


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.

like image 41
GuitarExtended Avatar answered Sep 28 '22 05:09

GuitarExtended