How/when Do Event Listeners Get Attached In D3.js?
Solution 1:
tl;dr
As it turns out, this is an intricate variation of the infamous pointer-events
vs. fill
hassle. The event handlers are in fact attached to the <g>
elements right away. They are, however, not executed for some time, because the events will not get through to these elements most of the time. Setting pointer-events: all
does easily fix this issue.
Apart from the technical issues this is a perfect example of why you should provide a minimal example, where things are stripped down to the bare minimum. The sheer amount of code made it unnecessarily hard to attack. The following snippet contains just enough code to demonstrate the issue:
d3.select("g").on("mouseover", function() {
// The difference between below log entries shows, that the event was
// targeted at another element and bubbled up to this handler's element.
console.dir(d3.event.target.tagName); // <rect>: actual target for this event
console.dir(this.tagName); // <g>: element this handler is attached to
d3.select(this).select("rect")
.style("fill", "orange");
});
rect {
stroke: red;
stroke-width: 0.2;
stroke-dasharray: 1.5 1.5;
fill:none;
}
<script src="https://d3js.org/d3.v4.js"></script>
<svg width="300" height="300">
<g>
<rect x="20" y="20" width="200" height="200"/>
</g>
</svg>
Analysis
When a browser determines which element will become a target of a pointer event, it will do something called hit-testing:
16.5.1 Hit-testing
Determining whether a pointer event results in a positive hit-test depends upon the position of the pointer, the size and shape of the graphics element, and the computed value of the ‘pointer-events’ property on the element.
The above sentence contains two pieces of vital information for your issue:
Only graphic elements can become direct targets of pointer events, whereas mere
<g>
elements alone cannot by itself be targets of these events. The events may, however, bubble and eventually reach that group. From within your event handlers you can log the actual target of the event as referenced ind3.event.target
as well asthis
, which points to the element, this handler was attached to:.on("mouseover", function() { // The difference between below log entries shows, that the event was // targeted at another element and bubbled up to this handler's element. console.log(d3.event.target); // <path>: actual target for this event console.log(this); // <g>: element this handler is attached to d3.select(this).select("path") .style("fill", "orange"); })
As you can see in this JSFiddle, these will always differ. This is relevant for your scenario, because you register the handler functions on the groups. This way, the handlers will only get executed if a graphics child element of the group becomes a pointer event's target with the event bubbling up to the group itself. This, on its own, is not much of a problem, but, in conjunction with the next point, this explains, why your set-up is not working.
The
pointer-events
property determines, "whether or when an element may be the target of a mouse event". Because this property is never set throughout your code, the default kicks in, which isvisiblePainted
defined as follows (emphasis mine):The element can only be the target of a mouse event when the visibility attribute is set to visible and when the mouse cursor is over the interior (i.e., 'fill') of the element and the fill attribute is set to a value other than none, or when the mouse cursor is over the perimeter (i.e., 'stroke') of the element and the stroke attribute is set to a value other than none.
As others have noted in the comments the relevant
<path>
elements within your group all feature the classst8
which definesfill: none
, whereby preventing them from becoming an event target when hovering their interior, i.e., fill. When these paths cannot become target for the pointer events, there is no event, that could bubble up to your group, which renders the event listeners useless.If a listener was executed the first time on an element (why this can happen is explained below, so bear with me for the moment), this problem resolves itself by setting the
fill
property on the path, whereby making it a legitimate target for pointer events. This is why the handlers will continue functioning when they have first come to life.Side note: This effect is so powerful, that it will even influence the way the dev tools deal with these elements in Chrome and Firefox. When you try to inspect an element, that has fill set to none, by right clicking on it, the dev tools will open up referencing the root
<svg>
element instead of the element you clicked on, because the latter was not the event's target. Try this, in contrast, with an element where the event handler is already working, so to speak, and it will open the dev tools for exactly this very element.
Solution
The easy solution to this is to allow for pointer events to occur on the interior, i.e. fill, of the paths by explicitly setting the property to all
:
The element can only be the target of a mouse event when the pointer is over the interior (i.e., fill) or the perimeter (i.e., stroke) of the element. The values of the
fill
,stroke
andvisibility
attribute do not affect event processing.
This can best be done right before registering the event handlers as in my updated JSFiddle:
d3.selectAll("svg > g > g").select("g").select("g")
.attr("pointer-events", "all")
.on("mouseover", function() {
//...
}
Why does it work sometimes and why the delays?
The above provides a proper analysis and a working solution, but, if you give it some time to sink in, there still remains the question, why on earth the handlers appear to be registered or, at least, to be activated with such delays. Pondering even more on this, it turns out all information to understand the issue is already contained in my explanation.
As I said above, the <path>
elements will actually be the event targets, not the groups. With the pointer-events
property defaulting to visiblePainted
they are not completely unreachable for pointer events as can be seen re-reading above mentioned specification:
[…] or when the mouse cursor is over the perimeter (i.e., 'stroke') of the element and the stroke attribute is set to a value other than none.
Altough the infamous class st8
sets stroke: ff0000
(which obviously is other than none), it specifies stroke-width:0.24
which is a pretty thin line. Additionally being dashed, it turns out, it is hard to hit the line at all. If you actually do hit it, though, it will cause the path to become an event target with the event bubbling to the group, eventually executing the event handler. This effect can be demonstrated by setting the stroke-width
to a larger value making it easier to hit the path:
.st8 {
fill:none;
stroke:#ff0000;
stroke-dasharray:1.68,1.2;
stroke-linecap:round;
stroke-linejoin:round;
stroke-width:2 /* Set to 2 to make it easier to hit */
}
Have a look at this JSFiddle for a working demo.
Even without setting pointer-events: all
this will work, because the lines are now wide enough to be hit by the pointer. Because the fat lines are ugly and will break the fine layout, this is more of a demonstration than a real solution, though.
Solution 2:
This is a very interesting problem, I managed to make it work, but I have no explanation to why this works. Would appreciate if someone with in-depth knowledge would explain this.
Slow:
var targetElements = d3.selectAll("svg > g > g").select("g").select("g").select("path");
targetElements.on("mouseover", function() {
d3.select(this)
.style("fill", "orange");
}).on("mouseout", function() {
d3.select(this)
.style("fill", "BLUE");
}).on("click", function() {
d3.select(this)
.style("fill", "green");
});
Fast:
var targetElements = d3.selectAll("svg > g > g").select("g").select("g").select("path");
targetElements.style('fill', 'white'); // Black magic - comment this out and the event handler attachment is delayed alot
targetElements.on("mouseover", function() {
d3.select(this)
.style("fill", "orange");
}).on("mouseout", function() {
d3.select(this)
.style("fill", "BLUE");
}).on("click", function() {
d3.select(this)
.style("fill", "green");
});
The difference is only in applying fill to the elements before I attach event handlers to them - .style("fill", "white").on("mouseover",
The Fiddle to play around - https://jsfiddle.net/v8e4hnff/1/
NOTE: Also tried to implement with JS native selectors and event handler attachment on the SVG elements, that was very little faster than D3. Behavior is the same on IE11 and Chrome.
As said above, if someone can explain the behavior, please do!
Post a Comment for "How/when Do Event Listeners Get Attached In D3.js?"