I got a D3v6 graph, which contains a context menu for the nodes. If clicked, its possible to add a link to another node. My problem is the zoom function, which I declared on the root SVG:
var svg = d3.select("svg")
.attr("class", "canvas")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function (event) {
svg.attr("transform", event.transform)
}))
//.on("mousedown", mousedownSVG)
.on("mousemove", mousemoveSVG)
.on("mouseup", mouseupSVG)
.append("g")
Case 1: do not zoom, use the Add Link function to connect node 2 with node 0. No issue at all
Case 2: Reload snippet, zoom in and afterwards use the Add Link function. The dragLine is not at the mouse position. Somehow the zoom function destroys the scale of my graph, which cause to return wrong positions.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3v6 Refactor</title>
<!-- call external d3.js framework -->
<script src="https://d3js.org/d3.v6.js"></script>
</head>
<style>
body {
overflow: hidden;
background-color: rgb(220, 220, 220);
margin: 0px;
}
.node {
stroke: white;
stroke-width: 2px;
cursor: pointer;
}
.node:hover {
stroke: red
}
.link {
fill: none;
cursor: default;
stroke: rgb(0, 0, 0);
stroke-width: 3px;
}
.dragline {
stroke-width: 2;
pointer-events: none;
}
.dragline.hidden {
stroke-width: 0;
}
#context-menu-node {
font-family: "Open Sans", sans-serif;
position: fixed;
z-index: 10000;
width: 190px;
background: whitesmoke;
border: 2px;
border-radius: 6px;
border-color: white;
border-style: solid;
transform: scale(0);
transform-origin: top left;
}
#context-menu-node.active {
transform: scale(1);
transition: transform 200ms ease-in-out;
}
#context-menu-node .item {
padding: 8px 10px;
font-size: 15px;
color: black;
}
#context-menu-node .item i {
display: inline-block;
margin-right: 5px;
}
#context-menu-node hr {
margin: 5px 0px;
border-color: whitesmoke;
}
#context-menu-node .item:hover {
background: lightblue;
}
</style>
<body>
<!-- right click context menu node -->
<div id="context-menu-node">
<div id="addLink" class="item">
<i class="fas fa-link"></i></i> Add Link
</div>
<hr>
<div id="addNode" class="item">
<i class="fas fa-link"></i></i> Add Node
</div>
</div>
<svg id="svg"> </svg>
<script>
var graph = {
"nodes": [
{
"id": 0,
},
{
"id": 1,
},
{
"id": 2,
}
],
"links": [
{
"source": 0,
"target": 1,
},
{
"source": 1,
"target": 2,
}
]
}
var width = window.innerWidth
var height = window.innerHeight
var svg = d3.select("svg")
.attr("class", "canvas")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function (event) {
svg.attr("transform", event.transform)
}))
//.on("mousedown", mousedownSVG)
.on("mousemove", mousemoveSVG)
.on("mouseup", mouseupSVG)
.append("g")
// remove zoom on dblclick listener
d3.select("svg").on("dblclick.zoom", null)
var linkContainer = svg.append("g").attr("class", "linkContainer")
var nodeContainer = svg.append("g").attr("class", "nodeContainer")
var mousedownNode = "";
var targetLink = "";
var addLinkClicked = false;
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) {
return d.id;
}).distance(100))
.force("charge", d3.forceManyBody().strength(-500))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(50))
var dragLine = svg.append("path")
.attr("class", "link dragline")
.attr("d", "M0,0 L0,0");
initialize()
function initialize() {
link = linkContainer.selectAll(".link")
.data(graph.links)
.join("line")
.attr("class", "link")
node = nodeContainer.selectAll(".node")
.data(graph.nodes, d => d.id)
.join("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
node
.data(graph.nodes, d => d.id)
.append("circle")
.attr("r", 30)
.style("fill", "whitesmoke")
.on("contextmenu", contextMenuNode)
.on("mouseenter", (event, d) => {
targetLink = d
})
.on("mousedown", (event, d) => {
mousedownNode = d;
})
.on("click", function (event, d) {
if (!addLinkClicked) return
// needed by FF
dragLine
.classed('hidden', true)
console.log(targetLink.id)
graph.links.push({ source: thisElement.id, target: targetLink.id });
addLinkClicked = false;
initialize();
simulation.alpha(0.3).restart()
});
node
.append("text")
.style("class", "icon")
.attr("font-family", "FontAwesome")
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.attr("font-size", 30)
.attr("fill", "black")
.attr("stroke-width", "0px")
.attr("pointer-events", "none")
.text((d) => {
return d.id
})
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation
.force("link")
.links(graph.links)
}
function contextMenuNode(event, d) {
thisElement = d
event.preventDefault()
document.getElementById("context-menu-node").classList.remove("active")
var contextMenu = document.getElementById("context-menu-node")
contextMenu.style.top = event.clientY + "px"
contextMenu.style.left = event.clientX + "px"
contextMenu.classList.add("active")
window.addEventListener("click", function () {
contextMenu.classList.remove("active")
})
document.getElementById("addNode").addEventListener("click", addNode)
document.getElementById("addLink").addEventListener("click", addLink)
}
function addLink() {
addLinkClicked = true
const point = d3.pointer(event)
dragLine
.classed("hidden", false)
.attr('d', `M${thisElement.x},${thisElement.y}L${thisElement.x},${thisElement.y}`);
}
function addNode() {
var newID = Math.floor(Math.random() * 1000)
graph.nodes.push({
"id": newID
})
graph.links.push({ source: newID, target: thisElement.id })
initialize()
simulation.alpha(0.3).restart()
}
/*
function mousedownSVG(event) {
var point = d3.pointer(event)
var newNode = { id: graph.nodes.length, x: point[0], y: point[1] }
graph.nodes.push(newNode)
initialize()
simulation.alpha(0.3).restart()
}
*/
function mousemoveSVG(event) {
if (!addLinkClicked) return
const point = d3.pointer(event)
// update drag line
dragLine.attr('d', `M${mousedownNode.x},${mousedownNode.y}L${point[0]},${point[1]}`);
}
function mouseupSVG(event) {
if (mousedownNode) {
//hide drag line
dragLine
.classed("hidden", true)
}
}
function ticked() {
// update link positions
link
.attr("x1", function (d) {
return d.source.x;
})
.attr("y1", function (d) {
return d.source.y;
})
.attr("x2", function (d) {
return d.target.x;
})
.attr("y2", function (d) {
return d.target.y;
});
// update node positions
node
.attr("transform", function (d) {
return "translate(" + d.x + ", " + d.y + ")";
});
}
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = undefined;
d.fy = undefined;
}
</script>
</body>
</html>
Any idea how to solve this?
The general issue is dealt with in this question with accepted answer by @Andrew Reid
For your specific issue, try the following to illustrate what is happening:
console.log(point) in your mousemoveSVG function[x, y] in the consoledragline about - you will see the [x, y] has changed and the dragline is offmousemoveSVG update this line to const point = d3.pointer(event, svg.node());[x,y] for any different pan/ zoom actionsThis 2nd argument to d3.pointer called target:
If the target is an SVG element, the event’s coordinates are transformed using the inverse of the screen coordinate transformation matrix.
Transforming the [x,y] of the pointer with this screen coordinate transformation matrix is basically saying keep my mouse in step with the zoom.
Here is your full code with the update - I commented out the log as it stuffs up the snippet, but you can uncomment to try the steps above.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3v6 Refactor</title>
<!-- call external d3.js framework -->
<script src="https://d3js.org/d3.v6.js"></script>
</head>
<style>
body {
overflow: hidden;
background-color: rgb(220, 220, 220);
margin: 0px;
}
.node {
stroke: white;
stroke-width: 2px;
cursor: pointer;
}
.node:hover {
stroke: red
}
.link {
fill: none;
cursor: default;
stroke: rgb(0, 0, 0);
stroke-width: 3px;
}
.dragline {
stroke-width: 2;
pointer-events: none;
}
.dragline.hidden {
stroke-width: 0;
}
#context-menu-node {
font-family: "Open Sans", sans-serif;
position: fixed;
z-index: 10000;
width: 190px;
background: whitesmoke;
border: 2px;
border-radius: 6px;
border-color: white;
border-style: solid;
transform: scale(0);
transform-origin: top left;
}
#context-menu-node.active {
transform: scale(1);
transition: transform 200ms ease-in-out;
}
#context-menu-node .item {
padding: 8px 10px;
font-size: 15px;
color: black;
}
#context-menu-node .item i {
display: inline-block;
margin-right: 5px;
}
#context-menu-node hr {
margin: 5px 0px;
border-color: whitesmoke;
}
#context-menu-node .item:hover {
background: lightblue;
}
</style>
<body>
<!-- right click context menu node -->
<div id="context-menu-node">
<div id="addLink" class="item">
<i class="fas fa-link"></i></i> Add Link
</div>
<hr>
<div id="addNode" class="item">
<i class="fas fa-link"></i></i> Add Node
</div>
</div>
<svg id="svg"> </svg>
<script>
var graph = {
"nodes": [
{
"id": 0,
},
{
"id": 1,
},
{
"id": 2,
}
],
"links": [
{
"source": 0,
"target": 1,
},
{
"source": 1,
"target": 2,
}
]
}
var width = window.innerWidth
var height = window.innerHeight
var svg = d3.select("svg")
.attr("class", "canvas")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function (event) {
svg.attr("transform", event.transform)
}))
//.on("mousedown", mousedownSVG)
.on("mousemove", mousemoveSVG)
.on("mouseup", mouseupSVG)
.append("g")
// remove zoom on dblclick listener
d3.select("svg").on("dblclick.zoom", null)
var linkContainer = svg.append("g").attr("class", "linkContainer")
var nodeContainer = svg.append("g").attr("class", "nodeContainer")
var mousedownNode = "";
var targetLink = "";
var addLinkClicked = false;
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) {
return d.id;
}).distance(100))
.force("charge", d3.forceManyBody().strength(-500))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(50))
var dragLine = svg.append("path")
.attr("class", "link dragline")
.attr("d", "M0,0 L0,0");
initialize()
function initialize() {
link = linkContainer.selectAll(".link")
.data(graph.links)
.join("line")
.attr("class", "link")
node = nodeContainer.selectAll(".node")
.data(graph.nodes, d => d.id)
.join("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
node
.data(graph.nodes, d => d.id)
.append("circle")
.attr("r", 30)
.style("fill", "whitesmoke")
.on("contextmenu", contextMenuNode)
.on("mouseenter", (event, d) => {
targetLink = d
})
.on("mousedown", (event, d) => {
mousedownNode = d;
})
.on("click", function (event, d) {
if (!addLinkClicked) return
// needed by FF
dragLine
.classed('hidden', true)
console.log(targetLink.id)
graph.links.push({ source: thisElement.id, target: targetLink.id });
addLinkClicked = false;
initialize();
simulation.alpha(0.3).restart()
});
node
.append("text")
.style("class", "icon")
.attr("font-family", "FontAwesome")
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.attr("font-size", 30)
.attr("fill", "black")
.attr("stroke-width", "0px")
.attr("pointer-events", "none")
.text((d) => {
return d.id
})
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation
.force("link")
.links(graph.links)
}
function contextMenuNode(event, d) {
thisElement = d
event.preventDefault()
document.getElementById("context-menu-node").classList.remove("active")
var contextMenu = document.getElementById("context-menu-node")
contextMenu.style.top = event.clientY + "px"
contextMenu.style.left = event.clientX + "px"
contextMenu.classList.add("active")
window.addEventListener("click", function () {
contextMenu.classList.remove("active")
})
document.getElementById("addNode").addEventListener("click", addNode)
document.getElementById("addLink").addEventListener("click", addLink)
}
function addLink() {
addLinkClicked = true
const point = d3.pointer(event)
dragLine
.classed("hidden", false)
.attr('d', `M${thisElement.x},${thisElement.y}L${thisElement.x},${thisElement.y}`);
}
function addNode() {
var newID = Math.floor(Math.random() * 1000)
graph.nodes.push({
"id": newID
})
graph.links.push({ source: newID, target: thisElement.id })
initialize()
simulation.alpha(0.3).restart()
}
/*
function mousedownSVG(event) {
var point = d3.pointer(event)
var newNode = { id: graph.nodes.length, x: point[0], y: point[1] }
graph.nodes.push(newNode)
initialize()
simulation.alpha(0.3).restart()
}
*/
function mousemoveSVG(event) {
if (!addLinkClicked) return
const point = d3.pointer(event, svg.node()); // <--- add svg.node() as 'target'
//console.log(point);
// update drag line
dragLine.attr('d', `M${mousedownNode.x},${mousedownNode.y}L${point[0]},${point[1]}`);
}
function mouseupSVG(event) {
if (mousedownNode) {
//hide drag line
dragLine
.classed("hidden", true)
}
}
function ticked() {
// update link positions
link
.attr("x1", function (d) {
return d.source.x;
})
.attr("y1", function (d) {
return d.source.y;
})
.attr("x2", function (d) {
return d.target.x;
})
.attr("y2", function (d) {
return d.target.y;
});
// update node positions
node
.attr("transform", function (d) {
return "translate(" + d.x + ", " + d.y + ")";
});
}
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = undefined;
d.fy = undefined;
}
</script>
</body>
</html>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With