ChatGPT解决这个技术问题 Extra ChatGPT

Updating SVG Element Z-Index With D3

What is an effective way to bring an SVG element to the top of the z-order, using the D3 library?

My specific scenario is a pie chart which highlights (by adding a stroke to the path) when the mouse is over a given piece. The code block for generating my chart is below:

svg.selectAll("path")
    .data(d)
  .enter().append("path")
    .attr("d", arc)
    .attr("class", "arc")
    .attr("fill", function(d) { return color(d.name); })
    .attr("stroke", "#fff")
    .attr("stroke-width", 0)
    .on("mouseover", function(d) {
        d3.select(this)
            .attr("stroke-width", 2)
            .classed("top", true);
            //.style("z-index", 1);
    })
    .on("mouseout", function(d) {
        d3.select(this)
            .attr("stroke-width", 0)
            .classed("top", false);
            //.style("z-index", -1);
    });

I've tried a few options, but no luck so far. Using style("z-index") and calling classed both did not work.

The "top" class is defined as follows in my CSS:

.top {
    fill: red;
    z-index: 100;
}

The fill statement is there to make sure I knew it was turning on/off correctly. It is.

I've heard using sort is an option, but I'm unclear on how it would be implemented for bringing the "selected" element to the top.

UPDATE:

I fixed my particular situation with the following code, which adds a new arc to the SVG on the mouseover event to show a highlight.

svg.selectAll("path")
    .data(d)
  .enter().append("path")
    .attr("d", arc)
    .attr("class", "arc")
    .style("fill", function(d) { return color(d.name); })
    .style("stroke", "#fff")
    .style("stroke-width", 0)
    .on("mouseover", function(d) {
        svg.append("path")
          .attr("d", d3.select(this).attr("d"))
          .attr("id", "arcSelection")
          .style("fill", "none")
          .style("stroke", "#fff")
          .style("stroke-width", 2);
    })
    .on("mouseout", function(d) {
        d3.select("#arcSelection").remove();
    });

n
notan3xit

As explained in the other answers, SVG does not have a notion of a z-index. Instead, the order of elements in the document determines the order in the drawing.

Apart from reordering the elements manually, there is another way for certain situations:

Working with D3 you often have certain types of elements that should always be drawn on top of other types of elements.

For example, when laying out graphs, links should always be placed below nodes. More generally, some background elements usually need to be placed below everything else, while some highlights and overlays should be placed above.

If you have this kind of situation, I found that creating parent group elements for those groups of elements is the best way to go. In SVG, you can use the g element for that. For example, if you have links that should be always placed below nodes, do the following:

svg.append("g").attr("id", "links")
svg.append("g").attr("id", "nodes")

Now, when you paint your links and nodes, select as follows (the selectors starting with # reference the element id):

svg.select("#links").selectAll(".link")
// add data, attach elements and so on

svg.select("#nodes").selectAll(".node")
// add data, attach elements and so on

Now, all links will always be appended structurally before all node elements. Thus, the SVG will show all links below all nodes, no matter how often and in what order you add or remove elements. Of course, all elements of the same type (i.e. within the same container) will still be subject to the order in which they were added.


I ran into this with the idea in my head that I could move one item up, when my real problem was organization of my elements into groups. If your "real" problem is moving individual elements then the sort method described in the previous answer is a good way to go.
Awesome! Thank you, notan3xit, you saved my day! Just to add an idea for others - if you need to add elements in the order different from the desired visibility order, you can just create groups in the proper visibility order (even before adding any elements to them), and then append elements to these groups in any order.
This is the correct approach. Obvious once you think of it.
For me, svg.select('#links') ... had to be replaced by d3.select('#links')... in order to work. Maybe this will help others.
E
Eliran Malka

One of the solutions presented by the developer is: "use D3's sort operator to reorder the elements." (see https://github.com/mbostock/d3/issues/252)

In this light, one might sort the elements by comparing their data, or positions if they were dataless elements:

.on("mouseover", function(d) {
    svg.selectAll("path").sort(function (a, b) { // select the parent and sort the path's
      if (a.id != d.id) return -1;               // a is not the hovered element, send "a" to the back
      else return 1;                             // a is the hovered element, bring "a" to the front
  });
})

This not only works on the Z order, it also doesn't upset any dragging that has just started with the element being raised.
it doesn't work for me too. a,b seems to be only the data, not the d3 selection element or DOM element
Indeed, like for @nkint it did not work for be when comparing a.id and d.id. The solution is to directly compare a and d.
This won't perform adequately for a large number of child elements. Better to re-render the raised element in a subsequent <g>.
Alternately, the svg:use element can point dynamically to the raised element. See stackoverflow.com/a/6289809/292085 for solution.
s
swenedo

Since SVG doesn't have Z-index but use the order of the DOM elements, you can bring it to front by:

this.parentNode.appendChild(this);

You can then e.g. make use of insertBefore to put it back on mouseout. This however requires you to be able to target the sibling-node your element should be inserted before.

DEMO: Take a look at this JSFiddle


This would be a great solution if it weren't for Firefox failing to render any :hover styles. I also don't think the ìnsert function at the end would be required for this use case.
@swenedo, your solution works great for me and brings the element to the front. Regarding to bring it to the back, I'm a little bit stacked and don't know how to write it in the correct way. Can you, please, post an example, how you can bring the object to the back. Thanks.
@MikeB. Sure, I have just updated my answer. I realised d3's insert function probably isn't the best way to go, because it creates a new element. We just want to move an existing element, therefore I use insertBefore in this example instead.
I tried to use this method, but unfortunately I does not work in IE11. Do you have a work-around for this browser?
n
nategood

SVG doesn't do z-index. Z-order is dictated by the order of the SVG DOM elements in their container.

As far as I could tell (and I've tried this a couple of times in the past), D3 doesn't provide methods for detaching and reattaching a single element in order to bring it to the front or whatnot.

There is an .order() method, which reshuffles the nodes to match the order they appear in the selection. In your case, you need to bring a single element to the front. So, technically, you could resort the selection with the desired element in front (or at the end, can't remember which is topmost), and then call order() on it.

Or, you could skip d3 for this task and use plain JS (or jQuery) to re-insert that single DOM element.


Do note that there are issues with removing/inserting elements inside a mouseover handler in some browsers, it can lead to mouseout events not being sent properly, so I'd recommend trying this in all browsers to make sure it works as you expect.
Wouldn't altering the order also change the layout of my pie chart? I'm also digging around jQuery to learn how to re-insert elements.
I've updated my question with my solution I picked out, which doesn't actually utilize the core of the original question. If you see anything really bad about this solution I welcome comments! Thank you for insight on the original question.
Seems like a reasonable approach, mainly thanks to the fact that the overlaid shape doesn't have a fill. If it did, I'd expect it to "steal" the mouse event the moment it appeared. And, I think @ErikDahlström's point still stands: this could still be an issue in some browsers (IE9 primarily). For extra "safety" you could add .style('pointer-events', 'none') to the overlaid path. Unfortunately, this property doesn't do anything in IE, but still....
@EvilClosetMonkey Hi... This question gets a lot of views, and I believe futurend's answer below is more helpful than mine. So, perhaps consider changing the accepted answer to futurend's, so it appears higher up.
C
Cem Schemel

The simple answer is to use d3 ordering methods. In addition to d3.select('g').order(), there is .lower() and .raise() in version 4. This changes how your elements appear. Please consult the docs for more information - https://github.com/d3/d3/blob/master/API.md#selections-d3-selection


l
lk135

I implemented futurend's solution in my code and it worked, but with the large number of elements I was using, it was very slow. Here's the alternative method using jQuery that worked faster for my particular visualization. It relies on the svgs you want on top having a class in common (in my example the class is noted in my data set as d.key). In my code there is a <g> with the class "locations" that contains all of the SVGs I'm re-organizing.

.on("mouseover", function(d) {
    var pts = $("." + d.key).detach();
    $(".locations").append(pts);
 });

So when you hover on a particular data point, the code finds all the other data points with SVG DOM elements with that particular class. Then it detaches and re-inserts the SVG DOM elements associated with those data points.


A
Ali

Wanted to expand on what @notan3xit answered rather than write out an entire new answer (but I don't have enough reputation).

Another way to solve the element order problem is to use 'insert' rather than 'append' when drawing . That way the paths will always be placed together before the other svg elements(this assumes your code already does the enter() for links before the enter() for the other svg elements).

d3 insert api: https://github.com/mbostock/d3/wiki/Selections#insert


m
mskr182

It took me ages to find how to tweak the Z-order in an existing SVG. I really needed it in the context of d3.brush with tooltip behavior. In order to have the two features work nicely together (http://wrobstory.github.io/2013/11/D3-brush-and-tooltip.html), you need the d3.brush to be the first in Z-order (1st to be drawn on the canvas, then covered by the rest of the SVG elements) and it will capture all mouse events, no matter what is on top of it (with higher Z indices).

Most forum comments say that you should add the d3.brush first in your code, then your SVG "drawing" code. But for me it was not possible as I loaded an external SVG file. You can easily add the brush at any time and alter the Z-order later on with:

d3.select("svg").insert("g", ":first-child");

In the context of a d3.brush setup it will look like:

brush = d3.svg.brush()
    .x(d3.scale.identity().domain([1, width-1]))
    .y(d3.scale.identity().domain([1, height-1]))
    .clamp([true,true])
    .on("brush", function() {
      var extent = d3.event.target.extent();
      ...
    });
d3.select("svg").insert("g", ":first-child");
  .attr("class", "brush")
  .call(brush);

d3.js insert() function API: https://github.com/mbostock/d3/wiki/Selections#insert

Hope this helps!


R
RamiReddy P

You can Do like this On Mouse Over You can Pull it to top.

d3.selection.prototype.bringElementAsTopLayer = function() {
       return this.each(function(){
       this.parentNode.appendChild(this);
   });
};

d3.selection.prototype.pushElementAsBackLayer = function() { 
return this.each(function() { 
    var firstChild = this.parentNode.firstChild; 
    if (firstChild) { 
        this.parentNode.insertBefore(this, firstChild); 
    } 
}); 

};

nodes.on("mouseover",function(){
  d3.select(this).bringElementAsTopLayer();
});

If You want To Push To Back

nodes.on("mouseout",function(){
   d3.select(this).pushElementAsBackLayer();
});

C
Community

Version 1

In theory, the following should work fine.

The CSS code :

path:hover {
    stroke: #fff;
    stroke-width : 2;
}

This CSS code will add a stroke to the selected path.

The JS code :

svg.selectAll("path").on("mouseover", function(d) {
    this.parentNode.appendChild(this);
});

This JS code first removes the path from the DOM tree and then adds it as the last child of its parent. This makes sure the path is drawn on top of all other children of the same parent.

In practice, this code works fine in Chrome but breaks in some other browsers. I tried it in Firefox 20 on my Linux Mint machine and couldn't get it to work. Somehow, Firefox fails to trigger the :hover styles and I haven't found a way to fix this.

Version 2

So I came up with an alternative. It may be a bit 'dirty', but at least it works and it doesn't require looping over all elements (as some of the other answers).

The CSS code :

path.hover {
    stroke: #fff;
    stroke-width : 2;
}

Instead of using the :hover pseudoselector, I use a .hover class

The JS code :

svg.selectAll(".path")
   .on("mouseover", function(d) {
       d3.select(this).classed('hover', true);
       this.parentNode.appendChild(this);
   })
   .on("mouseout", function(d) {
       d3.select(this).classed('hover', false);
   })

On mouseover, I add the .hover class to my path. On mouseout, I remove it. As in the first case, the code also removes the path from the DOM tree and then adds it as the last child of its parent.