如何与来自重叠形状的 d3.js 鼠标事件交互?

How to interact with d3.js mouse events from overlapping shapes?

我正在使用 d3.js 根据列表中的数据在 SVG 容器上绘制一些绿色圆圈 myList

这是该圆圈的示例:

现在我想实现以下行为:

  1. 当用户的鼠标经过圆圈时,应该会出现一个矩形。
  2. 矩形的左上角应该是圆心。
  3. 矩形应该消失当且仅当鼠标在圆的边界之外矩形。

下面是我为解决这个问题而编写的代码(在@Cyril 的帮助下,谢谢!)。但它不能正常工作。当鼠标指针悬停在圆上时,矩形是可见的。但是,当鼠标指针向东南方向移动到矩形中时(甚至是与圆的象限重叠的矩形部分),圆的 mouseout 事件将触发并且矩形消失 - 甚至在矩形的 mouseover 事件尚未触发。从技术上讲,我认为这仍然在圈子里。但显然 d3.js 没有。

鉴于这些鼠标事件的复杂性以及伴随它们的微小差异(和竞争条件),我该如何实现此功能?

var myList = [
  {"centerX": 200, "centerY": 300, "mouseIn": {"circle":false, "rectangle":false}}, 
  {"centerX": 400, "centerY": 500, "mouseIn": {"circle":false, "rectangle":false}}, 
];

var myCircle = self.svgContainer.selectAll(".dots")
  .data(myList).enter().append("circle")
  .attr("class", "dots")
  .attr("cx", function(d, i) {return d.centerX})
  .attr("cy", function(d, i) {return d.centerY})
  .attr("r", 15)
  .attr("stroke-width", 0)
  .attr("fill", function(d, i) {return "Green"})
  .style("display", "block");

myCircle.on({
    "mouseover": function(d) {
      console.log('\n\nCircle MouseOver ******************************************');
      var wasCursorIn = d.mouseIn.circle || d.mouseIn.rectangle;
      console.log('wasCursorIn = ', JSON.stringify(wasCursorIn));
      d.mouseIn.circle = true;
      console.log('d.mouseIn = ', JSON.stringify(d.mouseIn));
      var isCursorIn = d.mouseIn.circle || d.mouseIn.rectangle;
      console.log('isCursorIn = ', isCursorIn);
      if ((!wasCursorIn) && isCursorIn) {
        if (typeof d.rectangle === 'undefined' || d.rectangle === null)
          d.rectangle = self.svgContainer.append("rect")
            .attr("x", d.centerX)
            .attr("y", d.centerY)
            .attr("width", 100)
            .attr("height", 50)
            .attr("stroke-width", 2)
            .attr("fill", "DimGray")
            .attr("stroke", "DarkKhaki")
            .on("mouseover", function(e) {
                console.log('\n\nRectangle MouseOver ***************************************');
                console.log("d = ", JSON.stringify(d));
                d.mouseIn.rectangle = true;
                console.log("d = ", JSON.stringify(d));
              }
            )
            .on("mouseout", function(e) {
                console.log('\n\nRectangle MouseOut ****************************************');
                console.log("d = ", JSON.stringify(d));
                var wasCursorOut2 = (!d.mouseIn.circle) && (!d.mouseIn.rectangle);
                console.log('wasCursorOut2 = ', wasCursorOut2);
                d.mouseIn.rectangle = false;
                console.log('d.mouseIn = ', JSON.stringify(d.mouseIn));
                var isCursorOut2 = (!d.mouseIn.circle) && (!d.mouseIn.rectangle);
                console.log('isCursorOut2 = ', isCursorOut2);
                if ((!wasCursorOut2) && isCursorOut2) {
                  d3.select(this).style("cursor", "default");
                  d.rectangle.remove();
                  d.rectangle = null;
                }
              }
            )
            .style("display", "block");
        else
          d.rectangle.style("display", "block");
      }
    },
    "mouseout": function(d) {
      console.log('\n\nCircle MouseOut *******************************************');
      var wasCursorOut = (!d.mouseIn.circle) && (!d.mouseIn.rectangle);
      console.log('wasCursorOut = ', wasCursorOut);
      d.mouseIn.circle = false;
      console.log('d.mouseIn = ', JSON.stringify(d.mouseIn));
      var isCursorOut = (!d.mouseIn.circle) && (!d.mouseIn.rectangle);
      console.log('isCursorOut = ', isCursorOut);
      if ((!wasCursorOut) && isCursorOut) {
        if (!(typeof d.rectangle === 'undefined' || d.rectangle === null))
          d.rectangle.style("display", "none");
      }
    }
  }
);

当 SVG 元素重叠时,鼠标事件会触发最顶部的元素。当鼠标从一个元素移动到另一个元素时,事件的顺序是 mouseout 事件(对于鼠标离开的元素),然后是 mouseover 事件(对于鼠标进入的元素)。由于您只想在鼠标离开圆形和矩形元素时删除矩形元素,因此您需要在圆形和矩形元素上监听 mouseout 事件,并且仅当鼠标位置在两者之外时才删除矩形元素元素。

以下是确定鼠标位置是否在元素内的一种可能解决方案。使用 svg 的 getScreenCTM().inverse() 矩阵将鼠标事件的客户端坐标转换为 svg 坐标。使用该点构造一个 1x1 矩阵。使用 svg 的 checkIntersection() 来确定矩形是否与元素相交。

以下代码片段以 javascript(即没有 D3.js)的形式演示了此解决方案。

var svgNS = "http://www.w3.org/2000/svg";

var svg = document.getElementById("mySvg");
var circle = document.getElementById("myCircle");
var rect = null;        

circle.addEventListener("mouseover", circle_mouseover);
circle.addEventListener("mouseout", circle_mouseout);

function circle_mouseover(e) {
    if (!rect) {
        rect = document.createElementNS(svgNS, "rect");
        rect.setAttribute("x", circle.getAttribute("cx"));
        rect.setAttribute("y", circle.getAttribute("cy"));
        rect.setAttribute("width", 100);
        rect.setAttribute("height", 50);
        rect.setAttribute("style", "fill: gray;");
        rect.addEventListener("mouseout", rect_mouseout);
        svg.appendChild(rect);
    }    
}

function circle_mouseout(e) {
    console.log("circle_mouseout");
    if (rect) {
        var p = svg.createSVGPoint();
        p.x = e.clientX;
        p.y = e.clientY;
        p = p.matrixTransform(svg.getScreenCTM().inverse());
        var r = svg.createSVGRect();
        r.x = p.x;
        r.y = p.y;
        r.width = 1;
        r.height = 1;
        if(!svg.checkIntersection(rect, r)) {
            rect.removeEventListener("mouseout", rect_mouseout);
            svg.removeChild(rect);
            rect = null;
        }
    }
}

function rect_mouseout(e) {
    var p = svg.createSVGPoint();
    p.x = e.clientX;
    p.y = e.clientY;
    p = p.matrixTransform(svg.getScreenCTM().inverse());
    var r = svg.createSVGRect();
    r.x = p.x;
    r.y = p.y;
    r.width = 1;
    r.height = 1;
    if(!svg.checkIntersection(circle, r)) {
        rect.removeEventListener("mouseout", rect_mouseout);
        svg.removeChild(rect);
        rect = null;
    }
}
<svg id="mySvg" width="150" height="150">
    <circle id="myCircle" cx="50" cy="50" r="25" style="fill: green;"/>
</svg>

注意:我认为FireFox 还没有实现checkIntersection() 函数。如果您需要支持 FireFox,那么您将需要一种不同的方法来检查点和元素的交集。如果您只处理圆形和矩形,那么很容易编写自己的函数来检查交集。