使用D3更新SVG元素的Z-Index

96

如何使用D3库将SVG元素有效地置于z-order的顶部?

我的具体情况是,饼图在鼠标悬停在给定片段上时通过添加strokepath来突出显示。下面是生成我的图表的代码块:

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);
    });

我尝试了几个选项,但是到目前为止都没有运气。尝试使用style("z-index")并调用classed都没有起作用。

"top"类在我的CSS中定义如下:

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

fill语句用来确保我知道它是否正确地开启/关闭。它是正确的。

我听说使用sort是一种选择,但我不清楚如何将“选定”的元素置于顶部。

更新:

我通过以下代码解决了我的特定情况,在mouseover事件上添加了一个新的弧形到SVG中以显示高亮。

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();
    });
11个回答

97

如其他答案所解释的,SVG没有z-index概念。相反地,文档中元素的顺序决定了绘制的顺序。

除了手动重新排序元素外,还有另一种适用于某些情况的方法:

在使用D3时,经常会遇到某些类型的元素应始终位于其他类型元素的上方的情况。

例如,在布局图表时,链接应始终放置在节点下方。更一般地说,某些背景元素通常需要放置在所有其他元素下方,而某些突出显示和覆盖层则应放置在其上方。

如果你遇到这种情况,我发现创建父级群组元素是最好的选择。在SVG中,可以使用g元素来实现。例如,如果您希望链接始终位于节点下方,请执行以下操作:

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

现在,当你为链接和节点着色时,请按照以下方式选择(以#开头的选择器引用元素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

现在,所有的链接都将始终以结构方式附加在所有节点元素之前。因此,无论您添加或删除元素的次数和顺序如何,SVG都会显示所有链接位于所有节点之下。当然,同一类型的所有元素(即在同一个容器内)仍然受到它们添加的顺序的影响。


1
我曾经遇到过这个问题,当时我想着可以将一个项目向上移动,但我的真正问题是将元素组织成组。如果你的“真正”问题是移动单个元素,那么前面答案中描述的排序方法是一个不错的选择。 - John David Five
太棒了!谢谢你,notan3xit,你救了我的一天!只是为其他人添加一个想法 - 如果您需要按不同于所需可见性顺序的顺序添加元素,则可以按适当的可见性顺序创建组(甚至在向它们添加任何元素之前),然后以任何顺序将元素附加到这些组中。 - Olga Gnatenko
1
这是正确的方法。一旦你想到它,就显而易见了。 - Dave
对我来说,svg.select('#links')...必须被替换为d3.select('#links')...才能正常工作。也许这会帮助其他人。 - exchange

61

开发者提供的方案之一是:使用D3的排序操作符重新排序元素。(见https://github.com/mbostock/d3/issues/252

因此,可以通过比较它们的数据或位置(如果它们是无数据元素)来对元素进行排序:

.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
  });
})

这不仅适用于Z顺序,而且不会干扰刚开始拖动被提起的元素。 - Oliver Bock
3
我也遇到了同样的问题。a和b似乎只是数据,而不是d3选择元素或DOM元素。 - nkint
确实,就像对于@nkint而言,在比较a.idd.id时它并没有起作用。解决方案是直接比较ad - Peace Makes Plenty
这种方式无法适应大量子元素的情况。最好在随后的<g>中重新呈现被提起的元素。 - tribalvibes
1
另外, svg:use 元素可以动态指向所引用的元素。请参见 https://dev59.com/MHRB5IYBdhLWcg3w4bAo#6289809 获取解决方案。 - tribalvibes
@trialvibes 这将会在IE中禁用任何mouseout事件。 - 111

27

由于SVG没有Z-index而是使用DOM元素的顺序,您可以通过以下方式将其置于前面:

this.parentNode.appendChild(this);

您随后可以使用insertBefore,将其放回mouseout。但是这需要您能够定位要在其之前插入元素的兄弟节点。

演示:请查看此JSFiddle


如果Firefox无法渲染任何:hover样式,那么这将是一个很好的解决方案。我也不认为在这种情况下需要使用末尾的insert函数。 - John Slegers
@swenedo,你的解决方案对我非常有效,并将元素带到了前面。关于如何将其放到后面,我有点卡住了,不知道该如何正确编写代码。你能否请提供一个示例,说明如何将对象置于后面。谢谢。 - Mike
1
@MikeB。好的,我刚刚更新了我的答案。我意识到d3的insert函数可能不是最好的选择,因为它会创建一个__新__元素。我们只想移动一个现有的元素,因此在这个例子中我使用insertBefore - swenedo
我尝试使用这种方法,但不幸的是,在IE11中它不起作用。你有这个浏览器的解决方法吗? - Antonio Giovanni Schiavone

15

13

SVG不支持z-index。在其容器中,SVG DOM元素的顺序决定了它们的Z顺序。

据我所知(我以前尝试过几次),D3没有提供分离和重新连接单个元素的方法,以便将其置于最前面或其他位置。

有一个.order()方法,它重新排列节点以匹配它们在选择中出现的顺序。在您的情况下,您需要将单个元素置于最前面。因此,从技术上讲,您可以使用所需元素在前面(或在最后,记不清哪个是最顶部)重新排序选择,然后调用order()

或者,您可以跳过D3,在这个任务中使用纯JS(或jQuery)来重新插入那个单个DOM元素。


5
请注意,在某些浏览器中,在mouseover处理程序中删除/插入元素可能会导致mouseout事件无法正确发送,因此建议在所有浏览器中尝试此操作,以确保它按照您的期望工作。 - Erik Dahlström
调整顺序不会改变我的饼图布局吗?我也在研究jQuery如何重新插入元素。 - Nicholas Pappas
我已经更新了我的问题,并提供了我选择的解决方案,但实际上并没有利用原始问题的核心。如果您对这个解决方案有任何不好的地方,请提出评论!感谢您对原始问题的见解。 - Nicholas Pappas
似乎是一个合理的方法,主要因为覆盖形状没有填充。如果有填充,我希望它在出现的那一刻就“窃取”了鼠标事件。而且,我认为@ErikDahlström的观点仍然存在:这仍然可能是某些浏览器(主要是IE9)中的问题。为了更加“安全”,您可以在覆盖路径上添加.style('pointer-events', 'none')。不幸的是,这个属性在IE中什么都没做,但还是... - meetamit
@EvilClosetMonkey 你好... 这个问题得到了很多关注,我相信下面futurend的回答比我的更有帮助。因此,也许考虑将已接受的答案更改为futurend的答案,这样它就会出现在更高的位置。 - meetamit

4

我在我的代码中实现了futurend的解决方案,它可以正常工作,但是当我使用大量元素时,速度非常慢。以下是使用jQuery的替代方法,对于我特定的可视化效果工作得更快。它依赖于您想要置于顶部的svg具有共同的类(在我的示例中,该类在我的数据集中被记为d.key)。在我的代码中,有一个class为“locations”的<g>,其中包含我正在重新组织的所有SVG。

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

因此,当您悬停在特定数据点上时,代码会查找具有该特定类的所有其他数据点的SVG DOM元素。然后,它会分离并重新插入与这些数据点相关联的SVG DOM元素。


4
我希望扩展@notan3xit的回答,而不是写一个全新的答案(但我没有足够的声誉)。
另一种解决元素顺序问题的方法是在绘制时使用“insert”而不是“append”。这样,路径将始终在其他svg元素之前放置(假设您的代码已经对链接进行了enter(),然后再进行其他svg元素的enter())。
d3插入API:https://github.com/mbostock/d3/wiki/Selections#insert

1
我花费了很长时间才找到如何调整现有SVG的Z顺序。在d3.brush和工具提示行为的上下文中,我真的需要它们两个良好地协同工作(http://wrobstory.github.io/2013/11/D3-brush-and-tooltip.html)。为了使这两个功能良好地协同工作,你需要将d3.brush放在Z顺序的第一位(在画布上首先绘制,然后被其余的SVG元素覆盖),并且它将捕获所有鼠标事件,无论在它之上有什么(使用更高的Z索引)。
大多数论坛评论都说,你应该先添加d3.brush,然后再添加SVG "drawing"代码。但对我来说不可能,因为我加载了一个外部SVG文件。你可以随时轻松添加brush,并稍后通过以下方式更改Z顺序:
d3.select("svg").insert("g", ":first-child");

在d3.brush设置的上下文中,它看起来会像这样:
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()函数API: https://github.com/mbostock/d3/wiki/Selections#insert

希望这能帮助到您!


1
你可以这样做:鼠标悬停时,可以将其拉到顶部。
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();
});

如果你想将其推到后面

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

0

我使用了raise函数解决了它。

const raise = (d) => {
  d3.select(d).raise()
}

在需要在悬停时触发(以及其所有子元素)的组件中,只需放置此代码。

.on("mouseover", (d) => raise(d.srcElement.parentNode))

根据你的结构,也许不需要parentNode。在这个例子中,他们使用了"this",但在React中无法工作。https://codepen.io/_cselig/pen/KKgOppo


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接