Reputation: 2517
Following is the code for making a force directed graph using d3.js:
How can I make dragging a node only when you drag a node?
Cause currently when you drag anywhere over the stage some nearby node get dragged. Instead, how can I constrain the drag to occur only if I'm dragging a real node (rather than the surrounding area)
Also how can I make the stage zoomable & draggable?
const graph = {
nodes: [{
name: 'john',
age: 35
},
{
name: 'simon',
age: 37
},
{
name: 'manjoor',
age: 35
},
{
name: 'lorg',
age: 34
},
{
name: 'kilvin',
age: 32
},
],
links: [{
source: 'john',
target: 'simon'
},
{
source: 'john',
target: 'manjoor'
},
{
source: 'simon',
target: 'lorg'
},
{
source: 'simon',
target: 'kilvin'
},
{
source: 'manjoor',
target: 'kilvin'
},
{
source: 'lorg',
target: 'john'
},
{
source: 'lorg',
target: 'manjoor'
},
{
source: 'kilvin',
target: 'manjoor'
},
]
}
const canvas = d3.select('#network')
const width = canvas.attr('width')
const height = canvas.attr('height')
const r = 30
const ctx = canvas.node().getContext('2d')
const color = d3.scaleOrdinal(d3.schemeAccent);
const simulation = d3.forceSimulation()
.force('x', d3.forceX(width / 2))
.force('y', d3.forceY(height / 2))
.force('collide', d3.forceCollide(r + 20))
.force('charge', d3.forceManyBody().strength(-100))
.force('link', d3.forceLink().id(node => node.name))
.on('tick', update)
simulation.nodes(graph.nodes)
simulation.force('link').links(graph.links)
canvas.call(d3.drag()
.container(canvas.node())
.subject(dragsubject).on('start', dragstarted)
.on('drag', dragged).on('end', dragended)
)
function update() {
ctx.clearRect(0, 0, width, height)
ctx.beginPath()
ctx.globalAlpha = 0.5
ctx.strokeStyle = 'blue'
graph.links.forEach(drawLink)
ctx.stroke()
ctx.beginPath()
ctx.globalAlpha = 1
graph.nodes.forEach(drawNode)
ctx.fill()
}
function dragsubject(event) {
return simulation.find(event.x, event.y);
}
function drawNode(d) {
ctx.fillStyle = color(d.party)
ctx.moveTo(d.x, d.y)
ctx.arc(d.x, d.y, r, 0, Math.PI * 2)
}
function drawLink(l) {
ctx.moveTo(l.source.x, l.source.y)
ctx.lineTo(l.target.x, l.target.y)
}
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
update()
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.js"></script>
<canvas id="network" width="600" height="300"></canvas>
Upvotes: 1
Views: 961
Reputation: 13129
From the documentation:
simulation.find(x, y[, radius]) - Returns the node closest to the position ⟨x,y⟩ with the given search radius. If radius is not specified, it defaults to infinity. If there is no node within the search area, returns undefined.
Passing a radius of r
- which is the same as the ball radius - made it possible to only register dragging when the action is r
from the centre of the node.
const graph = {
nodes: [{
name: 'john',
age: 35
},
{
name: 'simon',
age: 37
},
{
name: 'manjoor',
age: 35
},
{
name: 'lorg',
age: 34
},
{
name: 'kilvin',
age: 32
},
],
links: [{
source: 'john',
target: 'simon'
},
{
source: 'john',
target: 'manjoor'
},
{
source: 'simon',
target: 'lorg'
},
{
source: 'simon',
target: 'kilvin'
},
{
source: 'manjoor',
target: 'kilvin'
},
{
source: 'lorg',
target: 'john'
},
{
source: 'lorg',
target: 'manjoor'
},
{
source: 'kilvin',
target: 'manjoor'
},
]
}
const canvas = d3.select('#network')
const width = canvas.attr('width')
const height = canvas.attr('height')
const r = 30
const ctx = canvas.node().getContext('2d')
const color = d3.scaleOrdinal(d3.schemeAccent);
const simulation = d3.forceSimulation()
.force('x', d3.forceX(width / 2))
.force('y', d3.forceY(height / 2))
.force('collide', d3.forceCollide(r + 20))
.force('charge', d3.forceManyBody().strength(-100))
.force('link', d3.forceLink().id(node => node.name))
.on('tick', update)
simulation.nodes(graph.nodes)
simulation.force('link').links(graph.links)
canvas.call(d3.drag()
.container(canvas.node())
.subject(dragsubject)
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended)
)
function update() {
ctx.clearRect(0, 0, width, height)
ctx.beginPath()
ctx.globalAlpha = 0.5
ctx.strokeStyle = 'blue'
graph.links.forEach(drawLink)
ctx.stroke()
ctx.beginPath()
ctx.globalAlpha = 1
graph.nodes.forEach(drawNode)
ctx.fill()
}
function dragsubject(event) {
return simulation.find(event.x, event.y, r);
}
function drawNode(d) {
ctx.fillStyle = color(d.party)
ctx.moveTo(d.x, d.y)
ctx.arc(d.x, d.y, r, 0, Math.PI * 2)
}
function drawLink(l) {
ctx.moveTo(l.source.x, l.source.y)
ctx.lineTo(l.target.x, l.target.y)
}
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
update()
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.js"></script>
<canvas id="network" width="600" height="300"></canvas>
Upvotes: 2