How / when event listeners connect to d3.js?

I am trying to create an SVG editor. In short, I need to attach mouse events to <g> elements at a specific depth within a given SVG. For various reasons, I cannot know the ID in advance. SVG is huge and will have hundreds if not thousands of elements.

 d3.selectAll("svg > g > g > g").select("g").on("mouseover", function() { console.log("mouseover"); }).on("mouseout", function() { console.log("mouseout"); }).on("click", function() { console.log("clicked"); }); 

This code works, but it takes a long time to run. Let's say I have ten such elements that will correspond to this particular choice. It seems that every second after loading the page, the other of 10 actually gets mouse events bound. I am wondering if I can receive a console event printed every time d3 attaches an event, or how I can determine if d3 is attached with everything that it needs to attach.

Basically, this JSFiddle should load mouse events much faster. If you wait a few seconds, you will see more and more boxes.

+5
source share
2 answers

This is a very interesting problem, I managed to get it to work, but I have no explanation why this works. I would appreciate it if someone with deep knowledge explained 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 padding to the elements before I attach event handlers to them .style("fill", "white").on("mouseover",

The script for the game - https://jsfiddle.net/v8e4hnff/1/

NOTE. They also tried to implement with native JS selectors and attaching an event handler on SVG elements, which was very slightly faster than D3. The behavior is the same for IE11 and Chrome.

As stated above, if someone can explain the behavior, do it!

+3
source

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() { // The difference between below log entries shows, that the event was // targeted at another element and bubbled up to this handler 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 the browser determines which element will be the target of a pointer event, it will do something called hit-testing:

16.5.1 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 /* Set to 2 to make it easier to hit */ } 

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.

+3
source

Source: https://habr.com/ru/post/1263106/


All Articles