predefined = {
const quadtree = d3
.quadtree()
.x((d) => d.x)
.y((d) => d.y)
.addAll(nodes);
const tooltip = d3
.select("body")
.append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "hidden")
.style("background", "white")
.style("border", "1px solid #CCCCCC")
.style("border-radius", "3px")
.style("padding", "10px")
.text("tooltip");
const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");
const link = svg
.append("g")
.attr("stroke", "#CCCCCC")
.attr("stroke-opacity", 0.61)
.selectAll()
.data(links)
.join("line")
.attr("stroke-width", 2)
.attr("class", "link")
.style("opacity", 0.61);
const node = svg
.append("g")
.attr("stroke", "#CCCCCC")
.attr("stroke-width", 1.5)
.selectAll()
.data(nodes)
.join("circle")
.attr("r", 5)
.attr("fill", "lightblue")
.attr("class", "node")
.style("opacity", 1); // Initial opacity set to 0
// original tooltip text
// node.append("title").text((d) => d.productId);
link
.attr("x1", (d) => {
// console.log(d.source);
return d.source.x;
})
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
node.attr("cx", (d) => d.x).attr("cy", (d) => d.y);
function debounce(func, timeout = 100) {
let timer;
return function (event) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
// Apply the 'this' context and pass the event argument to the original function
func.call(this, event);
}, timeout);
};
}
// Function to handle mousemove with quadtree search
function handleMouseMove(event) {
// Find the closest node to the mouse using the quadtree
const [mx, my] = d3.pointer(event, svg.node());
const radius = 5; // Define a search radius
const closest = quadtree.find(mx, my, radius);
if (closest) {
// If a node is found within the radius, show the tooltip
tooltip
.style("visibility", "visible")
.style("top", event.pageY - 10 + "px")
.style("left", event.pageX + 10 + "px")
.html(`Product ID: ${closest.productId}`);
} else {
// If no node is found, hide the tooltip
tooltip.style("visibility", "hidden");
}
}
// Apply the debounced function to the mousemove event
svg.on("mousemove", debounce(handleMouseMove, 250));
const allTargets = new Set(links.map((link) => link.target.productId));
const graph = {};
links.forEach((link) => {
const sourceId = link.source.productId;
const targetId = link.target.productId;
if (!graph[sourceId]) {
graph[sourceId] = { nodeId: sourceId, targets: [] };
}
if (!graph[targetId]) {
graph[targetId] = { nodeId: targetId, targets: [] };
}
graph[sourceId].targets.push(graph[targetId]);
});
// Step 2: Traverse the graph to find the "source-target-source-target..." sequence
// This assumes there are no cycles in the graph and it's more like a tree structure
const sequence = [];
const visited = new Set();
function traverse(node) {
if (visited.has(node.nodeId)) return;
visited.add(node.nodeId);
sequence.push(node);
node.targets.forEach((targetNode) => {
sequence.push({ source: node, target: targetNode });
traverse(targetNode);
});
}
// Find an arbitrary start node (e.g., a node with no incoming links)
const startNode = graph[Object.keys(graph).find((id) => !allTargets.has(id))];
traverse(startNode);
// Step 3: Animate based on sequence
function animateSequence(sequence, index = 0) {
if (index >= sequence.length) return;
const item = sequence[index];
const nextItem = sequence[index + 1];
if (item.source && item.target) {
// This item is a link
animateLink(item.source, item.target, () =>
animateSequence(sequence, index + 1)
);
} else {
// This item is a node
animateNode(item, () => animateSequence(sequence, index + 1));
}
}
// Helper functions to animate nodes and links
function animateNode(node, onComplete) {
const nodeElement = svg
.selectAll(".node")
.nodes()
.find((n) => n.productId === node.nodeId);
gsap.to(nodeElement, {
duration: 1,
opacity: 1,
ease: "power1.inOut",
onComplete: onComplete
});
}
function animateLink(source, target, onComplete) {
const linkElement = svg
.selectAll(".link")
.nodes()
.find(
(l) =>
l.source.productId === source.nodeId &&
l.target.productId === target.nodeId
);
const totalLength = calculateLinkLength(source, target);
gsap.fromTo(
linkElement,
{ strokeDasharray: `0,${totalLength}` },
{
duration: 2,
strokeDasharray: `${totalLength},${totalLength}`,
ease: "power1.inOut",
onComplete: onComplete
}
);
}
// Helper function to calculate link length (you may already have this logic in your SVG drawing code)
function calculateLinkLength(source, target) {
const dx = target.x - source.x;
const dy = target.y - source.y;
return Math.sqrt(dx * dx + dy * dy);
}
// Begin the animation sequence
animateSequence(sequence);
return svg.node();
}