I have a force layout graph where user adds nodes dynamically. How can i center all my nodes with a bit distance between, and make them move independently and not around the center.
I have tried removing the d3.forceCenter(width / 2, height / 2) which makes nodes move independently but then it possitions all nodes at (0, 0).
simulation = d3.forceSimulation()
.force('charge', d3.forceManyBody().strength(0))
.force('center', d3.forceCenter(width / 2, height / 2));
I want all the nodes to be centered and move independently.
EDIT:
I tried setting cx and cy values but that did not work either.
const nodeEnter = nodeElements
.enter()
.append('circle')
.attr('r', 20)
.attr('fill', 'orange')
.attr('cx', (d, i) => {
return (width / 2) + i * 10;
})
.attr('cy', (d, i) => {
return (height / 2) + i * 10;
})
.call(dragDrop(simulation))
.on('click', ({ id }) => handleClick(id));
Given what you said in your comment...
If i move 1 node then all other nodes move relatively in order to keep the center of mass at the same spot.
... you already know that forceCenter is the wrong tool for the task, since it will keep the centre of mass.
Therefore, just replace it for forceX and forceY:
const simulation = d3.forceSimulation()
.force('centerX', d3.forceX(width / 2))
.force('centerY', d3.forceY(height / 2));
Since you didn't provide enough code here is a general demo:
svg {
background-color: wheat;
}
<svg width="400" height="300"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
const svg = d3.select('svg');
const width = svg.attr('width');
const height = svg.attr('height');
const data = d3.range(50).map(() => ({}));
const node = svg.selectAll()
.data(data)
.enter()
.append('circle')
.attr('r', 10)
.attr('fill', 'teal')
.attr('stroke', 'black')
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended));
const simulation = d3.forceSimulation()
.force('charge', d3.forceManyBody().strength(-15))
.force('centerX', d3.forceX(width / 2))
.force('centerY', d3.forceY(height / 2));
simulation
.nodes(data)
.on('tick', ticked);
function ticked() {
node.attr('cx', d => d.x)
.attr('cy', d => d.y);
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
</script>
Related
I am new to d3 and I'm a trying to make a visualization with interactive nodes where each node can be clicked. When the node is clicked it should expand to show child nodes. I was able to get all the nodes to display interactively and I added an on click event, but I am not sure how I can get the child nodes to expand on click.
I am using the data from data.children in the onclick function and passing it to d3.hierarchy to set the data as the root. I am just not sure how to expand the data.
I am looking to make something like this where the circle node is in the center and the child nodes expand around it/outwards.
child child
\ /
node
|
child
Does anyone have any suggestions on how I could achieve this? I found d3.tree in the docs but that is more of a horizontal tree structure.
export default function ThirdTab(): React.MixedElement {
const ref = useRef();
const viewportDimension = getViewportDimension();
useEffect(() => {
const width = viewportDimension.width - 150;
const height = viewportDimension.height - 230;
const svg = d3
.select(ref.current)
.style('width', width)
.style('height', height);
const zoomG = svg.attr('width', width).attr('height', height).append('g');
const g = zoomG
.append('g')
.attr('transform', `translate(500,280) scale(0.31)`);
svg.call(
d3.zoom().on('zoom', () => {
zoomG.attr('transform', d3.event.transform);
}),
);
const nodes = g.selectAll('g').data(annotationData);
const group = nodes
.enter()
.append('g')
.attr('cx', width / 2)
.attr('cy', height / 2)
.attr('class', 'dotContainer')
.style('cursor', 'pointer')
.call(
d3
.drag()
.on('start', function dragStarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.03).restart();
d.fx = d.x;
d.fy = d.y;
})
.on('drag', function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
})
.on('end', function dragEnded(d) {
if (!d3.event.active) simulation.alphaTarget(0.03);
d.fx = null;
d.fy = null;
}),
);
const circle = group
.append('circle')
.attr('class', 'dot')
.attr('r', 20)
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.style('fill', '#33adff')
.style('fill-opacity', 0.3)
.attr('stroke', '#b3a2c8')
.style('stroke-width', 4)
.attr('id', d => d.name)
.on('click', function click(data) {
const root = d3.hierarchy(data.children);
const links = root.links();
const nodes = root.descendants();
console.log(nodes);
});
const label = group
.append('text')
.attr('x', d => d.x)
.attr('y', d => d.y)
.text(d => d.name)
.style('text-anchor', 'middle')
.style('fill', '#555')
.style('font-family', 'Arial')
.style('font-size', 15);
const simulation = d3
.forceSimulation()
.force(
'center',
d3
.forceCenter()
.x(width / 2)
.y(height / 2),
)
.force('charge', d3.forceManyBody().strength(1))
.force(
'collide',
d3.forceCollide().strength(0.1).radius(170).iterations(1),
);
simulation.nodes(annotationData).on('tick', function () {
circle
.attr('cx', function (d) {
return d.x;
})
.attr('cy', function (d) {
return d.y;
});
label
.attr('x', function (d) {
return d.x;
})
.attr('y', function (d) {
return d.y + 40;
});
});
}, [viewportDimension.width, viewportDimension.height]);
return (
<div className="third-tab-content">
<style>{`
.tooltip {
position: absolute;
z-index: 10;
visibility: hidden;
background-color: lightblue;
text-align: center;
padding: 4px;
border-radius: 4px;
font-weight: bold;
color: rgb(179, 162, 200);
}
`}</style>
<svg
ref={ref}
id="annotation-container"
role="img"
title="Goal Tree Container"></svg>
</div>
);
}
useEffect(() => {
const width = viewportDimension.width - 150;
const height = viewportDimension.height - 230;
const svg = d3
.select(ref.current)
.style('width', width)
.style('height', height);
const zoomG = svg.attr('width', width).attr('height', height).append('g');
const g = zoomG
.append('g')
.attr('transform', `translate(500,280) scale(0.31)`);
svg.call(
d3.zoom().on('zoom', () => {
zoomG.attr('transform', d3.event.transform);
}),
);
const nodes = g.selectAll('g').data(annotationData);
const simulation = d3
.forceSimulation(annotationData)
.force(
'center',
d3
.forceCenter()
.x(width / 2)
.y(height / 2),
)
.force('charge', d3.forceManyBody().strength(1));
const group = nodes
.enter()
.append('g')
.attr('x', d => d.x)
.attr('y', d => d.y)
.attr('id', d => 'container' + d.index)
.attr('class', 'dotContainer')
.style('white-space', 'pre')
.style('cursor', 'pointer')
.call(
d3
.drag()
.on('start', function dragStarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.03).restart();
d.fx = d.x;
d.fy = d.y;
})
.on('drag', function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
})
.on('end', function dragEnded(d) {
if (!d3.event.active) simulation.alphaTarget(0.03);
d.fx = null;
d.fy = null;
}),
);
simulation.on('tick', function () {
group.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
children
.attr('x', function (d) {
return d.x;
})
.attr('y', function (d) {
return d.y;
});
});
simulation.force(
'collide',
d3.forceCollide().strength(0.1).radius(170).iterations(10),
);
let currentlyExpandedNode;
let currentNode;
const circle = group
.append('circle')
.attr('class', 'dot')
.attr('id', d => {
return 'circle' + d.index;
})
.attr('r', 20)
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.style('fill', '#33adff')
.style('fill-opacity', 0.3)
.attr('stroke', 'gray')
.style('stroke-width', 4)
.on('click', function click(data) {
currentNode = d3.select(`#container${data.index}`);
if (currentlyExpandedNode) {
d3.selectAll('.child').remove();
d3.selectAll('#child-text').remove();
}
currentlyExpandedNode = data;
const pie = d3
.pie()
.value(() => 1)
.sort(null);
const circlex1 = +currentNode.select(`#circle${data.index}`).attr('cx');
const circley1 = +currentNode.select(`#circle${data.index}`).attr('cy');
const children = currentNode
.selectAll('line.child')
.data(pie(data.children))
.enter()
.append('line')
.attr('stroke', 'gray')
.attr('stroke-width', 1)
.attr('stroke-dasharray', '5 2')
.attr('class', 'child')
.attr('x1', circlex1) // starting point
.attr('y1', circley1)
.attr('x2', circlex1) // transition starting point
.attr('y2', circley1)
.transition()
.duration(300)
.attr('x2', circlex1 + 62) // end point
.attr('y2', circley1 - 62);
const childrenCircles = currentNode
.selectAll('circle.child')
.data(data.children)
.enter()
.append('circle')
.attr('class', 'child')
.attr('cx', () => circlex1 + 70)
.attr('cy', () => circley1 - 70)
.attr('r', 10)
.style('fill', '#b3a2c8')
.style('fill-opacity', 0.8)
.attr('stroke', 'gray')
.style('stroke-width', 2);
children.each(childData => {
currentNode
.append('text')
.attr('x', () => circlex1 + 80)
.attr('y', () => circley1 - 100)
.text(childData.data.name)
.attr('id', 'child-text')
.style('text-anchor', 'middle')
.style('fill', '#555')
.style('font-family', 'Arial')
.style('font-size', 15);
});
});
group
.append('text')
.attr('x', d => d.x)
.attr('y', d => d.y + 50)
.text(d => d.name)
.style('text-anchor', 'middle')
.style('fill', '#555')
.style('font-family', 'Arial')
.style('font-size', 15);
const children = group.selectAll('.child-element');
}, [viewportDimension.width, viewportDimension.height]);`
I would like to make so that my nodes fit a rectangle shaped space instead of a circle (default gravity).
I have read a similar topic here but was unable to make it work with my code.
// Construct the forces.
const forceNode = d3.forceManyBody();
const forceLink = d3.forceLink(links).id(({index: i}) => N[i]);
forceNode.strength(-150);
forceLink.strength(1);
forceLink.distance(50)
const simulation = d3.forceSimulation(nodes)
.force(link, forceLink)
.force("charge", d3.forceManyBody().strength(-150))
.force("collide", d3.forceCollide(10).strength(10).iterations(1))
.force('x', d3.forceX(width/4).strength(1))
.force('y', d3.forceY(height/4).strength(10))
.on("tick", ticked);
function ticked() {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";});
function drag(simulation) {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart(); //comment to remove sim
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); //comment to remove sim
event.subject.fx = null;
event.subject.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Then I apply ".call(drag(simulation));" to my node element
When the map loads, the nodes and links are all cramed into a vertical line, then after a few seconds and a few lag spikes they end up forming a sort of oval. The map remains messy and unreadable.
Any clue what I am doing wrong?
Please see a video clip of the behavior here: https://imgur.com/a/Hs4iuC5
Each block is pushed into it's location by an X and Y force, and a collide force is used to pushed them apart while being dragged.
The odd behavior is that the collision gets offset away from the drag start position. The farther you move from the drag start position the greater the offset.
var size = 40;
var items = [{
cx: 50,
cy: 200,
size: size,
collideR: size * 0.5
},
{
cx: 100,
cy: 200,
size: size,
collideR: size * 0.5
},
{
cx: 600,
cy: 200,
size: size,
collideR: size * 0.5
},
{
cx: 750,
cy: 200,
size: size,
collideR: size * 0.5
}
];
var alphaTarget = 0.03;
var sim;
var nodes =
d3.select(".grid-svg")
.selectAll("rect")
.data(items)
.enter()
.append("rect")
.attr("class", "grid-item-block")
.attr('x', (d) => d.cx)
.attr('y', (d) => d.cy)
.attr("width", (d) => d.size)
.attr("height", (d) => d.size)
.attr("transform", (d) => `translate(-${d.size / 2}, -${d.size / 2})`)
.call(
d3
.drag()
.on("start", (event, d) => {
if (!event.active) sim.alphaTarget(alphaTarget).restart();
d.fx = d.x;
d.fy = d.y;
sim.force(
"collide",
d3.forceCollide().radius((dc) => dc.collideR)
);
})
.on("drag", (event, d) => {
d.fx = event.x;
d.fy = event.y;
})
.on("end", (event, d) => {
if (!event.active) sim.alphaTarget(alphaTarget);
d.fx = null;
d.fy = null;
sim.force("collide", null);
})
);
sim = d3
.forceSimulation()
.alphaDecay(0.2)
.alphaMin(0.005)
.force(
"x",
d3
.forceX()
.strength(3.0)
.x((d) => d.cx)
)
.force(
"y",
d3
.forceY()
.strength(3.0)
.y((d) => d.cy)
);
sim.nodes(items).on("tick", () => {
nodes.attr("x", (d) => d.x).attr("y", (d) => d.y);
});
.grid-item-block {
fill: #009900;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg class="grid-svg" width="800" height="300"></svg>
In the example, if you drag the left block near the other 3 blocks you'll notice that the effect on block #2 is roughly symmetric, block #3 doesn't start until you start to overlap the block and is stronger on the right of block #3, and an even larger offset difference with block #4
I've found the underlying reason: the farther you get from the point d.cx, d.cy, the more it pulls on the block to get back. The enormous pull on the block makes d3 think that there is no reason to move the block it's colliding with. After all, after one tick, the block you're dragging will no longer overlap, because it's pulled back so hard.
I fixed this by re-initialising the centering forces, and only give them any strength if it was not the currently dragged block. Otherwise, the strength is 0 and the force is effectively not applied.
var size = 20;
var width = 400;
var height = 200;
var items = [{
cx: 2 * size,
cy: height / 2,
size: size,
collideR: size
},
{
cx: width - 2 * size,
cy: height / 2,
size: size,
collideR: size
}
];
var alphaTarget = 0.03;
var sim = d3
.forceSimulation()
.alphaDecay(0.2)
.alphaMin(0.005);
var nodes =
d3.select(".grid-svg")
.selectAll("rect")
.data(items)
.enter()
.append("rect")
.attr("class", "grid-item-block")
.attr('x', (d) => d.cx)
.attr('y', (d) => d.cy)
.attr("width", (d) => d.size)
.attr("height", (d) => d.size)
//.attr("transform", (d) => `translate(-${d.size / 2}, -${d.size / 2})`)
.call(
d3
.drag()
.on("start", function(event, d) {
if (!event.active) sim.alphaTarget(alphaTarget).restart();
d.isDragging = true;
d.fx = d.x;
d.fy = d.y;
sim.force(
"collide",
d3.forceCollide().radius((dc) => dc.collideR)
);
setForces();
})
.on("drag", (event, d) => {
d.fx = event.x;
d.fy = event.y;
})
.on("end", (event, d) => {
if (!event.active) sim.alphaTarget(alphaTarget);
d.isDragging = false;
d.fx = null;
d.fy = null;
sim.force("collide", null);
setForces();
})
);
function setForces() {
sim
.force(
"x",
d3
.forceX()
.strength((d) => d.isDragging ? 0 : 3.0)
.x((d) => d.cx)
)
.force(
"y",
d3
.forceY()
.strength((d) => d.isDragging ? 0 : 3.0)
.y((d) => d.cy)
);
}
setForces();
sim.nodes(items).on("tick", () => {
nodes.attr("x", (d) => d.x).attr("y", (d) => d.y);
});
.grid-item-block {
fill: #009900;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.js"></script>
<svg class="grid-svg" width="400" height="200"></svg>
So I'm working on freecodecamp's D3 force layout challenge : https://www.freecodecamp.com/challenges/show-national-contiguity-with-a-force-directed-graph
And as part of the challenge, I'm trying to append images of flags as the nodes in a force layout.
I've managed to append the flags and they are showing. When you click and drag on them, the links also move too. The problem is that they are stuck in the same position.
This is what I mean:
javascript (it's made within React):
createForceGraph() {
const { nodes, links } = this.state;
console.log(nodes);
console.log(links);
const w = 800;
const h = 500;
const margin = {
top: 30,
right: 30,
bottom: 80,
left: 80
};
const svg = d3.select('.chart')
.append('svg')
.attr('width', w)
.attr('height', h);
const simulation = d3.forceSimulation()
.force('link', d3.forceLink().id(function(d, i) { return i }).distance(1))
.force('charge', d3.forceManyBody().strength(1))
.force('center', d3.forceCenter(w / 2, h / 2))
.force('collision', d3.forceCollide(12));
const link = svg.append('g')
.attr('class', 'links')
.selectAll('line')
.data(links)
.enter()
.append('line')
.attr('stroke', 'black');
const node = d3.select('.nodes')
.selectAll('img')
.data(nodes)
.enter()
.append('img')
.attr('class', d => {
return `flag flag-${d.code}`;
})
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended));
simulation.nodes(nodes)
.on('tick', ticked);
simulation.force('link')
.links(links);
function ticked() {
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; });
node
.style("left", function(d) { return d.x + 'px'; })
.style("top", function(d) { return d.y + 'px'; });
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}
HTML:
<div>D3 Force-Directed Layout
<div className='chart'>
<div className='nodes'></div>
</div>
</div>
Maybe this can help you solve your problem?
https://bl.ocks.org/mbostock/950642
I'm trying to create a forceSimulation in d3 v4 which does not let the nodes float outside the boundries of the svg in the same way that this example has it for d3 v3 https://bl.ocks.org/mbostock/1129492.
Have tried a few different things in simulation.on("tick", ticked) to no avail. My codePen is below. Any ideas on how to achieve this?
https://codepen.io/mtsvelik/pen/rzxVrE
//Read the data from the mis element
var graph = document.getElementById('json').innerHTML;
graph = JSON.parse(graph);
render(graph);
function render(graph){
// Dimensions of sunburst.
var radius = 6;
var maxValue = d3.max(graph.links, function(d, i, data) {
return d.value;
});
//sub-in max-value from
d3.select("div").html('<form class="force-control" ng-if="formControl">Link threshold 0 <input type="range" id="thersholdSlider" name="points" value="0" min="0" max="'+ maxValue +'">'+ maxValue +'</form>');
document.getElementById("thersholdSlider").onchange = function() {threshold(this.value);};
var svg = d3.select("svg");
var width = svg.attr("width");
var height = svg.attr("height");
console.log(graph);
var graphRec = JSON.parse(JSON.stringify(graph)); //Add this line
//graphRec = graph; //Add this line
console.log(graphRec);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody().strength(Number(-1000 + (1.25*graph.links.length)))) //default force is -30, making weaker to increase size of chart
.force("center", d3.forceCenter(width / 2, height / 2));
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); });
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", radius)
.attr("fill", function(d) { return d.color; })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node.append("title")
.text(function(d) { return d.id; });
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
console.log(link.data(graph.links));
function ticked() {
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; });
node
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function threshold(thresh) {
thresh = Number(thresh);
graph.links.splice(0, graph.links.length);
for (var i = 0; i < graphRec.links.length; i++) {
if (graphRec.links[i].value > thresh) {graph.links.push(graphRec.links[i]);}
}
console.log(graph.links);
/*var threshold_links = graph.links.filter(function(d){ return (d.value > thresh);});
console.log(graph.links);
restart(threshold_links);*/
restart();
}
//Restart the visualisation after any node and link changes
// function restart(threshold_links) {
function restart() {
//DATA JOIN
//link = link.data(threshold_links);
link = link.data(graph.links);
console.log(link);
//EXIT
link.exit().remove();
console.log(link);
// ENTER - https://bl.ocks.org/colbenkharrl/21b3808492b93a21de841bc5ceac4e47
// Create new links as needed.
link = link.enter().append("line")
.attr("class", "link")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); }).merge(link);
console.log(link);
// DATA JOIN
node = node.data(graph.nodes);
/*
// EXIT
node.exit().remove();
// ENTER
node = node.enter().append("circle")
.attr("class", "node")
.attr("r", radius)
.attr("fill", function(d) {return d.color;})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
)
.merge(node);
node.append("title")
.text(function(d) { return d.id; });
*/
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
simulation.alphaTarget(0.3).restart();
}
}
In the tick function restrict the nodes to move out from the boundary:
node
.attr("cx", function(d) {
return (d.x = Math.max(radius, Math.min(width - radius, d.x)));
})
.attr("cy", function(d) {
return (d.y = Math.max(radius, Math.min(height - radius, d.y)));
})
//now update the links.
working code here
You can also use d3.forceBoundary that allows you to set a boundary with a strength. In your code
import it
<script src="https://unpkg.com/d3-force-boundary#0.0.1/dist/d3-force-boundary.min.js"></script>
then
var simulation = d3.forceSimulation()
.force("boundary", forceBoundary(0,0,width, height))
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody().strength(Number(-1000 + (1.25*graph.links.length)))) //default force is -30, making weaker to increase size of chart
.force("center", d3.forceCenter(width / 2, height / 2));
your pen fixed https://codepen.io/duto_guerra/pen/XWXagqm