TL; DR
As it turns out, this is a complex variation of the notorious pointer-events vs. fill . Event handlers are actually bound to the <g> elements immediately. However, they are not executed for some time, because events will not go through these elements most of the time. Setting up pointer-events: all easily fix this problem.
Beyond technical issues, this is a great example of why you should provide a minimal example where things are shared to a minimum. The extraordinary amount of code made it unnecessarily difficult to attack. The following snippet contains enough code to demonstrate the problem:
d3.select("g").on("mouseover", function() {
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 the browser determines which element will be the target of a pointer event, it will do something called hit-testing:
Determining whether a pointer event leads to a positive test depends on the position of the pointer, the size and shape of the graphic element, and the calculated value of the 'pointer-event property in the element.
The above sentence contains two issues that are important to you:
Only graphic elements can become direct targets of pointer events, while simple <g> elements alone cannot be objects of these events. However, events can bubble and ultimately reach this group. Inside event handlers, you can register the actual purpose of the event, as specified in d3.event.target , as well as this , which points to an 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 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 , they will always be different. This is relevant for your scenario as you register handler functions in groups. Thus, handlers will only be executed if the child of the chart in the group becomes the target of the pointer object with an event bubbling up to the group itself. This in itself is not a problem, but in connection with the next paragraph it explains why your setup does not work.
The pointer-events property determines whether the element can be a mouse event object. Since this property is never set in all code, the default value starts, and visiblePainted is defined as follows (my selection):
An element can only be a mouse object when the visibility attribute is set to visible, and when the mouse cursor is over the internal (ie, "filling") element and the fill attribute is set to a value other than none , or when the mouse cursor is over the perimeter ( that is, the "stroke") of the element, and the stroke attribute is set to a value other than the value.
As other commentators noted, in the corresponding <path> elements inside your group there is a class st8 that defines fill: none , so they cannot become a target for the event when you hover their inside, i.e. fill. When these paths cannot be targeted by pointer events, there is no event that can go to your group, which makes event listeners useless.
If the listener was executed for the first time on an element (why this can happen, this is explained below, so bear with me at the moment), this problem solves itself by setting the fill property on the path, a legitimate target for pointer events. This is why handlers will continue to function when they first come to life.
Side note . This effect is so powerful that it will even affect how Dev tools work with these elements in Chrome and Firefox. When you try to check an element that has a padding of none by right-clicking on it, the dev tools will open a link to the root <svg> element instead of the element you clicked on because the latter was not the target of the event. Try this, on the contrary , with the element in which the event handler works, so to speak, and works, and it will open the dev tools for this element itself.
Decision
An easy solution to this is to allow pointer events on the inside, that is, fill the paths by explicitly setting the all property:
An element can only be a mouse object when the pointer is above the inner (i.e., padding) or around the perimeter (i.e., stroke) of the element. The fill , stroke and visibility values do not affect event handling.
This is best done right before registering 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 delays?
The above example provides a proper analysis and working solution, but if you give it some time to dive into it, the question remains: why on earth the handlers seem to be registered or, at least, should be activated by such delays. After thinking even more about this, it turns out that all the information for understanding this is already contained in my explanation.
As I said above, the <path> elements will actually be the goals of the event, not the groups. If the pointer-events property is visiblePainted by default, they are not completely inaccessible to pointer events, as you can see by reading the above specification:
[& hellip;] or when the mouse cursor is over the perimeter (ie, "move"), and the stroke attribute has a value other than the value.
While the notorious st8 class sets stroke: ff0000 (which is clearly nothing), it indicates stroke-width:0.24 , which is a pretty thin line. In addition, being broken, it turns out to be difficult to get in line. However, if you really click on it, this will cause the path to become the target for the event when the event riots into the group, eventually executing the event handler. This effect can be demonstrated by setting stroke-width to a larger value, which makes 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 }
Take a look at the JSFiddle for a working demo.
Even without setting pointer-events: all this will work, because the lines are now wide enough for the pointer to hit. Because bold lines are ugly and disrupt the subtle layout, but this is more a demonstration than a real solution.