I have created a d3 network graph which is working fine, issue is if there is a connection between (A -> B and B -> A) i want separate links for that. How can i do that?
This is what i have done so far
onMount(() => {
const svg = d3
.select(svgContainer)
.attr('width', width)
.attr('height', height);
const tooltip = d3.select(tooltipElement);
// Modify nodes to ensure they have vx, vy, x, y properties
const nodesWithPositions = data.nodes.map((node: CustomNode) => ({
...node,
vx: 0, // Initialize vx for D3 simulation
vy: 0, // Initialize vy for D3 simulation
x: node.x || 0, // Ensure x is initialized if missing
y: node.y || 0, // Ensure y is initialized if missing
}));
// Create a force simulation using nodes and links
const simulation = d3
.forceSimulation(nodesWithPositions) // Use the updated nodes
.force(
'link',
d3
.forceLink(data.links)
.id((d) => (d as CustomNode).id) // Use the correct id function
.distance(200) // Define the link distance
)
.force('charge', d3.forceManyBody().strength(-300)) // Charge force to push nodes apart
.force('center', d3.forceCenter(width / 2, height / 2)); // Center force
// Add links to the SVG and apply the arrow marker
const link = svg
.append('g')
.selectAll('line')
.data(data.links) // Use the original links (no duplication)
.enter()
.append('line')
.attr('stroke', '#999')
.attr('stroke-opacity', 0.6)
.attr('stroke-width', (d) => Math.sqrt(Number(d.cycleCount))) // Set link stroke width based on cycle count
// Add cycleCount label to the middle of each link
const linkText = svg
.append('g')
.selectAll('text')
.data(data.links)
.enter()
.append('text')
.attr('x', (d: any) => (d.source.x + d.target.x) / 2) // Calculate midpoint of the link
.attr('y', (d: any) => (d.source.y + d.target.y) / 2) // Calculate midpoint of the link
.attr('dy', -10) // Move the text slightly above the midpoint
.attr('text-anchor', 'middle')
.attr('font-size', 12)
.attr('fill', '#333')
.text((d: any) => d.cycleCount); // Display the cycle count
// Add nodes to the SVG
const node = svg
.append('g')
.selectAll('circle')
.data(nodesWithPositions) // Pass the updated nodes with vx, vy, x, y
.enter()
.append('circle')
.attr('r', 10) // Set the radius of the nodes
.attr('fill', '#69b3a2') // Set the fill color of the nodes
.attr('stroke', '#333') // Set the border color of the nodes
.attr('stroke-width', 1.5) // Set the border width
.call(
d3.drag<SVGCircleElement, CustomNode>()
.on('start', (event: d3.D3DragEvent<SVGCircleElement, CustomNode, CustomNode>, d: CustomNode) => {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x; // Set the fixed x position
d.fy = d.y; // Set the fixed y position
})
.on('drag', (event: d3.D3DragEvent<SVGCircleElement, CustomNode, CustomNode>, d: CustomNode) => {
d.fx = event.x; // Update fixed x position during drag
d.fy = event.y; // Update fixed y position during drag
})
.on('end', (event: d3.D3DragEvent<SVGCircleElement, CustomNode, CustomNode>, d: CustomNode) => {
if (!event.active) simulation.alphaTarget(0);
d.fx = null; // Release the fixed x position
d.fy = null; // Release the fixed y position
})
);
// Add tooltips on node hover
node
.on('mouseover', (event, d: CustomNode) => {
tooltip
.classed('opacity-100', true)
.classed('opacity-0', false)
.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY - 25}px`)
.html(`Geozone: ${d.name}`);
})
.on('mouseout', () => {
tooltip.classed('opacity-0', true).classed('opacity-100', false);
});
// Update link and node positions on each tick
simulation.on('tick', () => {
link
.attr('x1', (d: any) => d.source.x)
.attr('y1', (d: any) => d.source.y)
.attr('x2', (d: any) => d.target.x)
.attr('y2', (d: any) => d.target.y);
node.attr('cx', (d: any) => d.x).attr('cy', (d: any) => d.y);
// Update link text position to remain at the midpoint
linkText
.attr('x', (d: any) => (d.source.x + d.target.x) / 2)
.attr('y', (d: any) => (d.source.y + d.target.y) / 2);
});
});
I want all nodes and i want separate links if there is a connection from (A -> B) and then again from (B -> A)
I have created a d3 network graph which is working fine, issue is if there is a connection between (A -> B and B -> A) i want separate links for that. How can i do that?
This is what i have done so far
onMount(() => {
const svg = d3
.select(svgContainer)
.attr('width', width)
.attr('height', height);
const tooltip = d3.select(tooltipElement);
// Modify nodes to ensure they have vx, vy, x, y properties
const nodesWithPositions = data.nodes.map((node: CustomNode) => ({
...node,
vx: 0, // Initialize vx for D3 simulation
vy: 0, // Initialize vy for D3 simulation
x: node.x || 0, // Ensure x is initialized if missing
y: node.y || 0, // Ensure y is initialized if missing
}));
// Create a force simulation using nodes and links
const simulation = d3
.forceSimulation(nodesWithPositions) // Use the updated nodes
.force(
'link',
d3
.forceLink(data.links)
.id((d) => (d as CustomNode).id) // Use the correct id function
.distance(200) // Define the link distance
)
.force('charge', d3.forceManyBody().strength(-300)) // Charge force to push nodes apart
.force('center', d3.forceCenter(width / 2, height / 2)); // Center force
// Add links to the SVG and apply the arrow marker
const link = svg
.append('g')
.selectAll('line')
.data(data.links) // Use the original links (no duplication)
.enter()
.append('line')
.attr('stroke', '#999')
.attr('stroke-opacity', 0.6)
.attr('stroke-width', (d) => Math.sqrt(Number(d.cycleCount))) // Set link stroke width based on cycle count
// Add cycleCount label to the middle of each link
const linkText = svg
.append('g')
.selectAll('text')
.data(data.links)
.enter()
.append('text')
.attr('x', (d: any) => (d.source.x + d.target.x) / 2) // Calculate midpoint of the link
.attr('y', (d: any) => (d.source.y + d.target.y) / 2) // Calculate midpoint of the link
.attr('dy', -10) // Move the text slightly above the midpoint
.attr('text-anchor', 'middle')
.attr('font-size', 12)
.attr('fill', '#333')
.text((d: any) => d.cycleCount); // Display the cycle count
// Add nodes to the SVG
const node = svg
.append('g')
.selectAll('circle')
.data(nodesWithPositions) // Pass the updated nodes with vx, vy, x, y
.enter()
.append('circle')
.attr('r', 10) // Set the radius of the nodes
.attr('fill', '#69b3a2') // Set the fill color of the nodes
.attr('stroke', '#333') // Set the border color of the nodes
.attr('stroke-width', 1.5) // Set the border width
.call(
d3.drag<SVGCircleElement, CustomNode>()
.on('start', (event: d3.D3DragEvent<SVGCircleElement, CustomNode, CustomNode>, d: CustomNode) => {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x; // Set the fixed x position
d.fy = d.y; // Set the fixed y position
})
.on('drag', (event: d3.D3DragEvent<SVGCircleElement, CustomNode, CustomNode>, d: CustomNode) => {
d.fx = event.x; // Update fixed x position during drag
d.fy = event.y; // Update fixed y position during drag
})
.on('end', (event: d3.D3DragEvent<SVGCircleElement, CustomNode, CustomNode>, d: CustomNode) => {
if (!event.active) simulation.alphaTarget(0);
d.fx = null; // Release the fixed x position
d.fy = null; // Release the fixed y position
})
);
// Add tooltips on node hover
node
.on('mouseover', (event, d: CustomNode) => {
tooltip
.classed('opacity-100', true)
.classed('opacity-0', false)
.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY - 25}px`)
.html(`Geozone: ${d.name}`);
})
.on('mouseout', () => {
tooltip.classed('opacity-0', true).classed('opacity-100', false);
});
// Update link and node positions on each tick
simulation.on('tick', () => {
link
.attr('x1', (d: any) => d.source.x)
.attr('y1', (d: any) => d.source.y)
.attr('x2', (d: any) => d.target.x)
.attr('y2', (d: any) => d.target.y);
node.attr('cx', (d: any) => d.x).attr('cy', (d: any) => d.y);
// Update link text position to remain at the midpoint
linkText
.attr('x', (d: any) => (d.source.x + d.target.x) / 2)
.attr('y', (d: any) => (d.source.y + d.target.y) / 2);
});
});
I want all nodes and i want separate links if there is a connection from (A -> B) and then again from (B -> A)
In your example code, d3 is actually applying links in both directions, but they are two lines laying on top of each other. The most cited example of doing what you want is this example. It changes the link to a path element with a curve so they don't overlay. It also adds a nice arrow marker to indicate direction. Here it is applied to your code (modified to remove the ts stuff):
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.js"></script>
</head>
<body>
<div id="svgContainer"></div>
<script>
const data = {
nodes: [
{ id: 'Alice'},
{ id: 'Bob' },
{ id: 'Carol'},
],
links: [
{ source: 'Alice', target: 'Bob' },
{ source: 'Bob', target: 'Carol' },
{ source: 'Carol', target: 'Bob' },
],
};
const width = 600,
height = 200;
const svg = d3
.select('#svgContainer')
.append('svg')
.attr('width', width)
.attr('height', height);
svg
.append('defs')
.append('marker')
.attr('id', "arrow")
.attr('viewBox', '0 -5 10 10')
.attr('refX', 15)
.attr('refY', -1.5)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,-5L10,0L0,5');
// Modify nodes to ensure they have vx, vy, x, y properties
const nodesWithPositions = data.nodes.map((node) => ({
...node,
vx: 0, // Initialize vx for D3 simulation
vy: 0, // Initialize vy for D3 simulation
x: node.x || 0, // Ensure x is initialized if missing
y: node.y || 0, // Ensure y is initialized if missing
}));
// Create a force simulation using nodes and links
const simulation = d3
.forceSimulation(nodesWithPositions) // Use the updated nodes
.force(
'link',
d3
.forceLink(data.links)
.id((d) => d.id) // Use the correct id function
.distance(200) // Define the link distance
)
.force('charge', d3.forceManyBody().strength(-300)) // Charge force to push nodes apart
.force('center', d3.forceCenter(width / 2, height / 2)); // Center force
// Add links to the SVG and apply the arrow marker
const link = svg
.append('g')
.selectAll('path')
.data(data.links) // Use the original links (no duplication)
.enter()
.append('path')
.attr('fill', 'none')
.attr("marker-end", "url(#arrow)")
.attr('stroke', '#999')
.attr('stroke-opacity', 0.6)
.attr('stroke-width', 2); // Set link stroke width based on cycle count
// Add cycleCount label to the middle of each link
const linkText = svg
.append('g')
.selectAll('text')
.data(data.links)
.enter()
.append('text')
.attr('x', (d) => (d.source.x + d.target.x) / 2) // Calculate midpoint of the link
.attr('y', (d) => (d.source.y + d.target.y) / 2) // Calculate midpoint of the link
.attr('dy', -10) // Move the text slightly above the midpoint
.attr('text-anchor', 'middle')
.attr('font-size', 12)
.attr('fill', '#333')
//.text((d) => d.cycleCount); // Display the cycle count
// Add nodes to the SVG
const node = svg
.append('g')
.selectAll('circle')
.data(nodesWithPositions) // Pass the updated nodes with vx, vy, x, y
.enter()
.append('circle')
.attr('r', 10) // Set the radius of the nodes
.attr('fill', '#69b3a2') // Set the fill color of the nodes
.attr('stroke', '#333') // Set the border color of the nodes
.attr('stroke-width', 1.5); // Set the border width;
// Update link and node positions on each tick
simulation.on('tick', () => {
link.attr('d', linkArc);
node.attr('cx', (d) => d.x).attr('cy', (d) => d.y);
// Update link text position to remain at the midpoint
linkText
.attr('x', (d) => (d.source.x + d.target.x) / 2)
.attr('y', (d) => (d.source.y + d.target.y) / 2);
});
function linkArc(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return (
'M' +
d.source.x +
',' +
d.source.y +
'A' +
dr +
',' +
dr +
' 0 0,1 ' +
d.target.x +
',' +
d.target.y
);
}
</script>
</body>
</html>