tl;博士
事实证明,这是臭名昭著的pointer-events 与fill 麻烦的复杂变体。事件处理程序实际上立即附加到<g> 元素。但是,它们在一段时间内不会执行,因为事件在大多数情况下都不会传递到这些元素。设置pointer-events: all 可以轻松解决此问题。
除了技术问题之外,这是一个完美的例子,说明了为什么您应该提供一个minimal示例,其中的东西被精简到最低限度。大量的代码使得攻击变得不必要地困难。以下 sn-p 包含足够的代码来演示该问题:
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>
分析
当浏览器确定哪个元素将成为指针事件的目标时,它会做一些称为命中测试的事情:
确定指针事件是否导致正面命中测试取决于指针的位置、graphics element 的大小和形状以及元素上‘pointer-events’ 属性的计算值。
上面的句子包含两条关于您的问题的重要信息:
-
只有图形元素可以成为指针事件的直接目标,而仅<g> 元素本身不能成为这些事件的目标。然而,这些事件可能会冒泡并最终到达该群体。在您的事件处理程序中,您可以记录在d3.event.target 和this 中引用的事件的实际目标,它指向该处理程序附加到的元素:
.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");
})
正如您在JSFiddle 中看到的,这些总是不同的。
这与您的方案相关,因为您在组上注册了处理程序函数。这样,只有当组的图形子元素成为指针事件的目标并且事件冒泡到组本身时,处理程序才会被执行。这本身并不是什么大问题,但是结合下一点,这解释了为什么您的设置不起作用。
-
pointer-events 属性确定,“元素是否或何时可能成为鼠标事件的目标”。因为这个属性从来没有在你的代码中设置,所以默认值是visiblePainted,定义如下(强调我的):
只有当可见性属性设置为可见并且鼠标光标位于元素的内部(即“填充”)并且设置了填充属性时,该元素才能成为鼠标事件的目标设置为 none 以外的值,或者当鼠标光标位于元素的周边(即“stroke”)上且 stroke 属性设置为 none 以外的值时。
正如其他人在 cmets 中指出的那样,您组中的相关 <path> 元素都具有定义 fill: none 的类 st8,从而防止它们在悬停其内部时成为事件目标,即填充。当这些路径不能成为指针事件的目标时,就没有事件可以冒泡到您的组,这会使事件侦听器无用。
如果第一次在一个元素上执行了监听器(为什么会发生这种情况,下面会解释,所以暂时请耐心等待),这个问题可以通过设置fill 属性自行解决在路径上,从而使其成为指针事件的合法目标。这就是为什么处理程序在它们刚开始运行时会继续运行的原因。
旁注:这种效果非常强大,甚至会影响开发工具在 Chrome 和 Firefox 中处理这些元素的方式。当您尝试检查已将填充设置为无的元素时,通过右键单击它,开发工具将打开引用根 <svg> 元素而不是您单击的元素,因为后者不是事件的目标.相比之下,使用事件处理程序已经在工作的元素试试这个,可以这么说,它会为这个元素打开开发工具。
解决方案
对此的简单解决方案是通过将属性显式设置为all,允许指针事件发生在路径的内部,即填充路径:
只有当指针位于内部(即填充)或周边(即,
行程)的元素。 fill、stroke 和 visibility 属性的值不影响事件处理。
这最好在我更新的JSFiddle 中注册事件处理程序之前完成:
d3.selectAll("svg > g > g").select("g").select("g")
.attr("pointer-events", "all")
.on("mouseover", function() {
//...
}
为什么它有时会起作用以及为什么会出现延迟?
上面提供了一个正确的分析和一个可行的解决方案,但是,如果你给它一些时间来深入了解,仍然存在一个问题,为什么处理程序似乎被注册了,或者,至少,在这样的延迟下被激活。对此进行了更多思考,结果发现我的解释中已经包含了理解该问题的所有信息。
正如我上面所说,<path> 元素实际上是事件目标,而不是组。 pointer-events 属性默认为 visiblePainted,它们对于指针事件并非完全不可访问,如重新阅读上述规范所示:
[...] 或者当鼠标光标位于元素的周界(即“stroke”)上并且 stroke 属性设置为 none 以外的值时。
尽管臭名昭著的类st8 设置了stroke: ff0000(显然不是没有),但它指定了stroke-width:0.24,这是一条非常细的线。事实证明,除了虚线之外,根本很难达到目标。但是,如果您真的点击它,它将导致路径成为事件目标,事件冒泡到组,最终执行事件处理程序。可以通过将stroke-width 设置为更大的值以更容易命中路径来演示此效果:
.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 */
}
看看这个JSFiddle 的工作演示。
即使没有设置pointer-events: all,这也会起作用,因为线条现在足够宽,可以被指针击中。因为粗线条很难看,并且会破坏精细的布局,但这更像是一个演示而不是真正的解决方案。