Reputation: 1542
I am trying to create a bounded force graph with labels. This is an official example of a bounded force graph without labels. Once, I add the labels I need to use a different tick function. When I do use the appropriate tick function the circle nodes indeed stay within the boundaries, however, the lines keep going if dragged beyond the boundary which is not the case for the example I linked. Here is the jsfiddle of my bounded graph with labels, but with lines still going if dragged beyond the boundary (you can drag the whole graph away except for the nodes). Below is the code. If you could help me figure out how to keep the lines within the boundary and mimic the behavior of the example I linked that would be great. Thanks.
js
var width = 280
height = 370
radius = 6;
var force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(50)
.charge(-200)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link")
.data(force.links())
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", click)
.on("dblclick", dblclick)
.call(force.drag);
node.append("circle")
.attr("r", radius)
.style("fill", "#C71585");
node.append("text")
.attr("x", 14)
.attr("dy", ".35em")
.style("fill", "#333")
.text(function (d) {
return d.name;
});
function tick() {
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;
});
var dx = function(d) {return Math.max(radius, Math.min(width - radius, d.x))}
var dy = function(d) {return Math.max(radius, Math.min(width - radius, d.y))}
node.attr("transform", function (d) {
return "translate(" + dx(d) + "," + dy(d) + ")";
});
}
function mouseover() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 16);
}
function mouseout() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 12);
}
// action to take on mouse click
function click() {
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 22)
.style("stroke-width", ".5px")
.style("opacity", 1)
.style("fill", "#E34A33")
.style("font", "17.5px serif");
d3.select(this).select("circle").transition()
.duration(750)
.style("fill", "#E34A33")
.attr("r", 16)
}
// action to take on mouse double click
function dblclick() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 12)
.style("fill", "#E34A33");
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 14)
.style("stroke", "none")
.style("fill", "#E34A33")
.style("stroke", "none")
.style("opacity", 0.6)
.style("font", "14px serif");
}
var links = [{
"source": "Analytics",
"target": "Science"
}, {
"source": "Analytics",
"target": "Software"
}, {
"source": "Analytics",
"target": "Story"
}, {
"source": "Science",
"target": "Math"
}, {
"source": "Science",
"target": "Statistics"
}, {
"source": "Software",
"target": "R"
}, {
"source": "Software",
"target": "SAS"
}, {
"source": "Software",
"target": "Other"
}, {
"source": "Story",
"target": "Business Communication"
}, {
"source": "Story",
"target": "Visualization"
}];
var nodes = {}
// Compute the distinct nodes from the links.
links.forEach(function(link) {
link.source = nodes[link.source] || (nodes[link.source] = {
name: link.source
});
link.target = nodes[link.target] || (nodes[link.target] = {
name: link.target
});
link.value = +link.value;
});
var width = 280
height = 370
radius = 6;
var force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(50)
.charge(-200)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link")
.data(force.links())
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", click)
.on("dblclick", dblclick)
.call(force.drag);
node.append("circle")
.attr("r", radius)
.style("fill", "#C71585");
node.append("text")
.attr("x", 14)
.attr("dy", ".35em")
.style("fill", "#333")
.text(function(d) {
return d.name;
});
function tick() {
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;
});
var dx = function(d) {
return Math.max(radius, Math.min(width - radius, d.x))
}
var dy = function(d) {
return Math.max(radius, Math.min(width - radius, d.y))
}
node.attr("transform", function(d) {
return "translate(" + dx(d) + "," + dy(d) + ")";
});
}
function mouseover() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 16);
}
function mouseout() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 12);
}
// action to take on mouse click
function click() {
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 22)
.style("stroke-width", ".5px")
.style("opacity", 1)
.style("fill", "#E34A33")
.style("font", "17.5px serif");
d3.select(this).select("circle").transition()
.duration(750)
.style("fill", "#E34A33")
.attr("r", 16)
}
// action to take on mouse double click
function dblclick() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 12)
.style("fill", "#E34A33");
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 14)
.style("stroke", "none")
.style("fill", "#E34A33")
.style("stroke", "none")
.style("opacity", 0.6)
.style("font", "14px serif");
}
.link {
stroke: #666;
opacity: 0.6;
stroke-width: 1.5px;
}
.node circle {
stroke: #fff;
opacity: 0.6;
stroke-width: 1.5px;
}
text {
font: 15px serif;
opacity: 0.6;
pointer-events: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Upvotes: 0
Views: 521
Reputation: 14589
Slight changes are required in the tick function. Update d.x and d.y positions with the calculated bounded values inside the node transform function and then update the link position attributes.
I have changed the background color of svg to see the bounding effect clearly.
var links = [{
"source": "Analytics",
"target": "Science"
}, {
"source": "Analytics",
"target": "Software"
}, {
"source": "Analytics",
"target": "Story"
}, {
"source": "Science",
"target": "Math"
}, {
"source": "Science",
"target": "Statistics"
}, {
"source": "Software",
"target": "R"
}, {
"source": "Software",
"target": "SAS"
}, {
"source": "Software",
"target": "Other"
}, {
"source": "Story",
"target": "Business Communication"
}, {
"source": "Story",
"target": "Visualization"
}];
var nodes = {}
// Compute the distinct nodes from the links.
links.forEach(function(link) {
link.source = nodes[link.source] || (nodes[link.source] = {
name: link.source
});
link.target = nodes[link.target] || (nodes[link.target] = {
name: link.target
});
link.value = +link.value;
});
var width = 280
height = 370
radius = 6;
var force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(50)
.charge(-200)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link")
.data(force.links())
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", click)
.on("dblclick", dblclick)
.call(force.drag);
node.append("circle")
.attr("r", radius)
.style("fill", "#C71585");
node.append("text")
.attr("x", 14)
.attr("dy", ".35em")
.style("fill", "#333")
.text(function(d) {
return d.name;
});
function tick() {
node.attr("transform", function(d) {
var r = 16;
d.x = Math.max(r, Math.min(width - r, d.x));
d.y = Math.max(r, Math.min(height - r, d.y));
return "translate(" + d.x + "," + d.y + ")"
});
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;
});
}
function mouseover() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 16);
}
function mouseout() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 12);
}
// action to take on mouse click
function click() {
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 22)
.style("stroke-width", ".5px")
.style("opacity", 1)
.style("fill", "#E34A33")
.style("font", "17.5px serif");
d3.select(this).select("circle").transition()
.duration(750)
.style("fill", "#E34A33")
.attr("r", 16)
}
// action to take on mouse double click
function dblclick() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 12)
.style("fill", "#E34A33");
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 14)
.style("stroke", "none")
.style("fill", "#E34A33")
.style("stroke", "none")
.style("opacity", 0.6)
.style("font", "14px serif");
}
.link {
stroke: #666;
opacity: 0.6;
stroke-width: 1.5px;
}
.node circle {
stroke: #fff;
opacity: 0.6;
stroke-width: 1.5px;
}
text {
font: 15px serif;
opacity: 0.6;
stroke: green;
pointer-events: none;
}
svg{
background-color: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Upvotes: 1
Reputation: 8223
If you want to create a boundaries for drag then you can redefine the drag method instead of the tick.
Create a new drag:
var drag = force.drag()
.on("drag", dragmove);
Use this drag on node.
And make a constraint on x,y coordinates of the node in the new dragmove method:
function dragmove(d) {
var dx = function(d) {return Math.max(radius, Math.min(width - radius, d.x))}
var dy = function(d) {return Math.max(radius, Math.min(width - radius, d.y))}
d.px = Math.min(d3.event.x,200);
d.py = Math.min(d3.event.y, 200);
d.x = Math.min(d3.event.x, 200);
d.y = Math.min(d3.event.y, 200);
}
You can check it out here: https://jsfiddle.net/n58to0tn/10/
Upvotes: 1