Cross-posted from github.com/bgschiller/d3-event-issue
Our story begins with some code that boils down to this:
const grid = document.querySelector("#grid svg");
const highlightCoordinates = function () {
const coords = d3.mouse(grid);
// do something with those coordinates.
};
grid.addEventListener("mousemove", highlightCoordinates);
The issues appeared when we decided to throttle that function (using lodash, but underscore would likely be the same):
grid.addEventListener("mousemove", _.throttle(highlightCoordinates, 100));
Lodash’s throttle has trailing
true by default (and we wanted to keep it on). The trailing call to highlightCoordinates
, by definition, occurs after all of the mousemove events are done. Using d3.mouse()
meant that we were depending on the value of d3.event
, and it had already been cleaned up at that point.
Following the advice on this stack overflow post, I set about modifying lodash.throttle to capture the value of d3.event
, and then put it back in place before invoking the function. Here’s the interesting part of the code (or check out the whole thing if you like):
function debounced() {
var time = _.now(),
isInvoking = shouldInvoke(time);
lastArgs = arguments;
lastThis = this;
lastCallTime = time;
// same as we're saving off the arguments and the context,
// store the d3 event at the time of call.
lastD3Event = d3.event;
//... and now the actual work of checking if
// we should invoke the function or do stuff with
// the timer, etc....
}
function invokeFunc(time) {
var args = lastArgs,
thisArg = lastThis;
// save the current value of d3.event, to put back after
// we're done faking the value.
var saveD3Event = d3.event;
// put the d3 event at time of last call in place
d3.event = lastD3Event;
lastD3Event = lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
// put back whatever was there before we messed with the value.
d3.event = saveD3Event;
return result;
}
New code, new error. Now, running this code gave the following error message:
It seems that d3.event
can only be read from, not written to. I wasn’t able to find where that is set up in d3’s code, but I did find this test, which suggested that I could write to d3Selection.event
, and it would change the value of d3.event
. Inspecting the property in node confirmed that:
$ node
> const d3 = require('d3')
undefined
> Object.getOwnPropertyDescriptor(d3, 'event').get.toString()
'function () { return d3Selection.event; }'
Okay! Now we’re getting somewhere! I changed my throttle code to set d3Selection.event
instead, and this… worked! But, it seems, not on every machine. My teammate, Katie, was seeing the same error as before:
Uncaught TypeError: Cannot read property 'sourceEvent' of null
Debugging on her machine showed that somehow, setting d3Selection.event wasn’t impacting the value of d3.event
:
The story continues (solved it. see below)
I haven’t solved this yet. I don’t know why it works on my machine but not Katie’s. I put together this repo to make it easier to investigate. Check out
- setting d3.event directly (and the source)
- setting d3Selection.event (and the source)
- setting d3.event directly, but without bundling d3 with webpack
Got an idea?
You can run this code locally
npm install
# install webpack, babel, d3, etcnpm run build
# produces the files in dist.
Another clue!
Oh! I almost forgot. Potentially another clue is that Katie’s machine doesn’t seem to be loading d3-selection-multi
correctly (or I set it up correctly). I’m loading it via
import "d3-selection-multi";
but she gets the following error:
Uncaught TypeError: measure.select(...).attr(...).attr(...).attr(...).attrs is not a function
Potentially Relevant Links
A Solution! but not a satisfying one…
Deleting the node_modules/
directory, and doing a fresh npm install
fixed things. My guess is that when I did require('d3-selection')
, my coworker’s machine was grabbing a different version than the require('d3')
version was using internally.
Maybe 'd3-selection'
was once listed in package.json as a direct dependency, rather than a transitive dependency from d3? So when we set d3Selection.event = ...
, it was assigning to a totally different copy of d3Selection
than d3.event
was reading from.