I am trying create a Force directed graph where I want my nodes to be draggable but somehow the event handlers dragStarted, dragged, and dragEnded are being triggered initially, but they’re not getting triggered during the actual drag operation.
Here is my code:
import React, { useEffect, useRef } from 'react';
import * as d3 from 'd3';
const GraphComp = ({ graphData }) => {
const svgRef = useRef(null);
useEffect(() => {
const graph = graphData;
const nodes = graph.nodes;
const links = graph.links;
const conceptWidth = 150;
const conceptHeight = 50;
const padding = 10;
const width = window.innerWidth; // Assuming full window width
const height = window.innerHeight; // Assuming full window height
const svg = d3.select(svgRef.current)
.attr("width", "100%")
.attr("height", "100%");
const drag = d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded);
const link = svg.append("g")
.selectAll(".link")
.data(links)
.enter()
.append("polyline")
.attr("class", "link")
.style("stroke-width", "1")
.style("stroke", "black")
.style("fill", "none")
.attr("id", function (d, i) { return 'link' + i; });
// Check if the marker already exists
let markerFor = svg.select("defs").select("#dirArrowFor");
if (markerFor.empty()) {
const markerFor = svg.append("defs")
.selectAll("marker")
.data(["forward"])
.enter()
.append("marker")
.attr("id", "dirArrowFor")
.attr("viewBox", "0 -5 10 10")
.attr("markerUnits", "strokeWidth")
.attr("markerWidth", 10)
.attr("markerHeight", 10)
.attr("refX", 10)
.attr("refY", 0)
.attr("overflow", "visible")
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.style("fill", "#000000");
}
link.attr("marker-mid", checkDir);
const linkNode = svg.append("g").selectAll(".link")
.data(links)
.enter()
.append("circle")
.attr("class", "link-node")
.attr("r", 4)
.style("fill", "#c00");
linkNode.append("title")
.text(function (d) { return d.linkingWord; });
const node = svg.append("g").selectAll(".node")
.data(nodes)
.enter()
.append("rect")
.attr("class", "node")
.attr("width", conceptWidth)
.attr("height", conceptHeight)
.attr("x", function (d) { return d.x = d.x })
.attr("y", function (d) { return d.y = d.y })
.attr("rx", 20)
.attr("ry", 20)
.style("cursor", "move")
.style('fill', function (d) { return d.color; })
// .call(d3.drag()
// .on("start", dragStarted)
// .on("drag", dragged)
// .on("end", dragEnded));
.call(drag)
const labels = svg.append("g")
.selectAll(".labels")
.data(nodes)
.enter()
.append("text")
.attr("class", "labels")
.text(function (d) { return d.name; })
.style("text-anchor", "middle")
.style("cursor", "move")
.attr("dx", conceptWidth / 2)
.attr("dy", conceptHeight / 2 + 5)
// .call(d3.drag()
// .on("start", dragStarted)
// .on("drag", dragged)
// .on("end", dragEnded));
.call(drag)
const force = d3.forceSimulation()
.force("collision", d3.forceCollide(conceptWidth / 2 + 1).iterations(1))
.force("link", d3.forceLink().id(function (d) { return d.name; }))
.on("tick", tick);
force.nodes(nodes);
force.force("link").links(links);
function tick() {
link.attr("points", function (d) {
var interForw = pointOnRect(d.source.x + conceptWidth / 2, d.source.y + conceptHeight / 2,
d.target.x + conceptWidth / 2 - conceptWidth / 2, d.target.y + conceptHeight / 2 - conceptHeight / 2,
d.target.x + conceptWidth / 2 + conceptWidth / 2, d.target.y + conceptHeight / 2 + conceptHeight / 2);
var interRev = pointOnRect(d.target.x, d.target.y,
d.source.x - conceptWidth / 2, d.source.y - conceptHeight / 2,
d.source.x + conceptWidth / 2, d.source.y + conceptHeight / 2);
if (d.direction === "forward") {
return boundXInter(d.source.x + conceptWidth / 2) + " "
+ boundYInter((d.source.y + conceptHeight / 2)) + ","
+ boundXIntersectionPoint(((interForw.x))) + " "
+ boundYIntersectionPoint((interForw.y)) + ","
+ boundXInter(d.target.x + conceptWidth / 2) + " "
+ boundYInter((d.target.y + conceptHeight / 2));
}
});
node.attr("x", function (d) { return d.x = boundX(d.x); })
.attr("y", function (d) { return d.y = boundY(d.y); });
labels.attr("x", function (d) { return d.x = boundX(d.x); })
.attr("y", function (d) { return d.y = boundY(d.y); });
linkNode.attr("cx", function (d) {
var interForw = pointOnRect(d.source.x, d.source.y,
d.target.x - conceptWidth / 2, d.target.y - conceptHeight / 2,
d.target.x + conceptWidth / 2, d.target.y + conceptHeight / 2);
var interRev = pointOnRect(d.target.x, d.target.y,
d.source.x - conceptWidth / 2, d.source.y - conceptHeight / 2,
d.source.x + conceptWidth / 2, d.source.y + conceptHeight / 2);
return d.cx = boundLinkNodesX((interForw.x + interRev.x) / 2) + conceptWidth / 2;
})
.attr("cy", function (d) {
var interForw = pointOnRect(d.source.x, d.source.y,
d.target.x - conceptWidth / 2, d.target.y - conceptHeight / 2,
d.target.x + conceptWidth / 2, d.target.y + conceptHeight / 2);
var interRev = pointOnRect(d.target.x, d.target.y,
d.source.x - conceptWidth / 2, d.source.y - conceptHeight / 2,
d.source.x + conceptWidth / 2, d.source.y + conceptHeight / 2);
return d.cy = boundLinkNodesY((interForw.y + interRev.y) / 2) + conceptHeight / 2;
});
}
function boundYIntersectionPoint(y) {
return Math.max(conceptHeight / 2, Math.min(width - (conceptHeight / 2), y));
}
function boundXIntersectionPoint(x) {
return Math.max(conceptWidth / 2, Math.min(width - (conceptWidth / 2), x));
}
function boundXInter(x) {
return Math.max(conceptWidth / 2, Math.min(width - (conceptWidth / 2 + padding), x));
}
function boundYInter(y) {
return Math.max(conceptHeight / 2, Math.min(height - (conceptHeight / 2 + padding), y));
}
function boundLinkNodesX(x) {
return Math.max(0, Math.min(width - (conceptWidth / 2 + padding), x));
}
function boundLinkNodesY(y) {
return Math.max(0, Math.min(height - (conceptHeight / 2 + padding), y));
}
function boundX(x) {
return Math.max(0, Math.min(width - (conceptWidth + padding), x));
}
function boundY(y) {
return Math.max(0, Math.min(height - (conceptHeight + padding), y))
}
function dragStarted(d) {
const event = d3.event;
if (!event || !event.sourceEvent) return; // Ignore if no source event
const sourceEvent = event.sourceEvent;
if (sourceEvent.type !== 'mousedown' && sourceEvent.type !== 'touchstart') return; // Ignore if not mousedown or touchstart
force.alphaTarget(0.03).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
const event = d3.event;
if (!event || !event.sourceEvent) return; // Ignore if no source event
d.fx = event.x;
d.fy = event.y;
}
function dragEnded(d) {
const event = d3.event;
if (!event || !event.sourceEvent) return; // Ignore if no source event
if (!event.sourceEvent.type === 'touchend') {
// For mouse event, check if it's left button release
if (event.sourceEvent.button !== 0) return;
}
force.alphaTarget(0);
d.fx = null;
d.fy = null;
}
console.log(dragStarted)
console.log(dragged)
console.log(dragEnded)
function checkDir(d) {
if (d.direction === "forward") {
return "url(#dirArrowFor)";
}
}
function pointOnRect(x, y, minX, minY, maxX, maxY, check) {
if (check && (minX <= x && x <= maxX) && (minY <= y && y <= maxY))
throw "Point " + [x, y] + "cannot be inside " + "the rectangle: " + [minX, minY] + " - " + [maxX, maxY] + ".";
var midX = (minX + maxX) / 2;
var midY = (minY + maxY) / 2;
var m = (midY - y) / (midX - x);
if (x <= midX) {
var minXy = m * (minX - x) + y;
if (minY <= minXy && minXy <= maxY)
return {
x: minX,
y: minXy
};
}
if (x >= midX) {
var maxXy = m * (maxX - x) + y;
if (minY <= maxXy && maxXy <= maxY)
return {
x: maxX,
y: maxXy
};
}
if (y <= midY) {
var minYx = (minY - y) / m + x;
if (minX <= minYx && minYx <= maxX)
return {
x: minYx,
y: minY
};
}
if (y >= midY) {
var maxYx = (maxY - y) / m + x;
if (minX <= maxYx && maxYx <= maxX)
return {
x: maxYx,
y: maxY
};
}
throw "Cannot find intersection for " + [x, y] + " inside rectangle " + [minX, minY] + " - " + [maxX, maxY] + ".";
}
return () => {
// Cleanup D3 elements if needed
};
}, [graphData]);
return <svg ref={svgRef}></svg>;
};
export default GraphComp;
New contributor
Ankit kumar is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.