如何与来自重叠形状的 d3.js 鼠标事件交互?
How to interact with d3.js mouse events from overlapping shapes?
我正在使用 d3.js 根据列表中的数据在 SVG 容器上绘制一些绿色圆圈 myList
。
这是该圆圈的示例:
现在我想实现以下行为:
- 当用户的鼠标经过圆圈时,应该会出现一个矩形。
- 矩形的左上角应该是圆心。
- 矩形应该消失当且仅当鼠标在圆的边界之外和矩形。
下面是我为解决这个问题而编写的代码(在@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,那么您将需要一种不同的方法来检查点和元素的交集。如果您只处理圆形和矩形,那么很容易编写自己的函数来检查交集。
我正在使用 d3.js 根据列表中的数据在 SVG 容器上绘制一些绿色圆圈 myList
。
这是该圆圈的示例:
现在我想实现以下行为:
- 当用户的鼠标经过圆圈时,应该会出现一个矩形。
- 矩形的左上角应该是圆心。
- 矩形应该消失当且仅当鼠标在圆的边界之外和矩形。
下面是我为解决这个问题而编写的代码(在@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,那么您将需要一种不同的方法来检查点和元素的交集。如果您只处理圆形和矩形,那么很容易编写自己的函数来检查交集。