Related
I'm using d3.js v6 with a force layout to represent a network graph.
I'm adding and removing nodes but when I restart the simulation all the nodes jump to an upper left position and then come back to the original position.
I have this following code snippet that shows exactly what I mean, I've seen other examples online that work fine but haven't been able to find what I am doing wrong, any help is really appreciated.
var dataset = {
nodes: [
{
id: 1
},
{
id: 2
}
],
links: [{
id: 1,
source: 1,
target: 2
}]
};
let switchBool = false;
let svg = d3.select('svg')
.attr('width', '100%')
.attr('height', '100%');
const width = svg.node()
.getBoundingClientRect().width;
const height = svg.node()
.getBoundingClientRect().height;
console.log(`${width}, ${height}`);
svg = svg.append('g');
svg.append('g')
.attr('class', 'links');
svg.append('g')
.attr('class', 'nodes');
const simulation = d3.forceSimulation();
initSimulation();
let link = svg.select('.links')
.selectAll('line');
loadLinks();
let node = svg.select('.nodes')
.selectAll('.node');
loadNodes();
restartSimulation();
function initSimulation() {
simulation
.force('link', d3.forceLink())
.force('charge', d3.forceManyBody())
.force('collide', d3.forceCollide())
.force('center', d3.forceCenter())
.force('forceX', d3.forceX())
.force('forceY', d3.forceY());
simulation.force('center')
.x(width * 0.5)
.y(height * 0.5);
simulation.force('link')
.id((d) => d.id)
.distance(100)
.iterations(1);
simulation.force('collide')
.radius(10);
simulation.force('charge')
.strength(-100);
}
function loadLinks() {
link = svg.select('.links')
.selectAll('line')
.data(dataset.links, (d) => d.id)
.join(
enter => enter.append('line').attr('stroke', '#000000'),
);
}
function loadNodes() {
node = svg.select('.nodes')
.selectAll('.node')
.data(dataset.nodes, (d) => d.id)
.join(
enter => {
const nodes = enter.append('g')
.attr('class', 'node')
nodes.append('circle').attr('r', 10);
return nodes;
},
);
}
function restartSimulation() {
simulation.nodes(dataset.nodes);
simulation.force('link').links(dataset.links);
simulation.alpha(1).restart();
simulation.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', (d) => `translate(${d.x},${d.y})`);
}
function updateData() {
switchBool = !switchBool;
if (switchBool) {
dataset.nodes.push({id: 3});
dataset.links.push({id: 2, source: 1, target: 3});
} else {
dataset.nodes.pop();
dataset.links.pop();
}
loadLinks();
loadNodes();
restartSimulation();
}
<script src="https://d3js.org/d3.v6.min.js"></script>
<div>
<button onclick="updateData()">Add/Remove</button>
<svg></svg>
</div>
It's because you use d3.forceCenter() which does not coerce nodes to a center point:
The centering force translates nodes uniformly so that the mean
position of all nodes (the center of mass if all nodes have equal
weight) is at the given position ⟨x,y⟩. (docs)
So if your two nodes are located at directly and equally below/above the centering point for d3.forceCenter, the mass is balanced. Introduce a new node and the entire force has to be transalted so that the center of mass is the center. This translation is the jump you are seeing.
Remove forceCenter and specify the center values with d3.forceX and d3.forceY, which do nudge nodes towards the specified x and y values:
var dataset = {
nodes: [
{
id: 1
},
{
id: 2
}
],
links: [{
id: 1,
source: 1,
target: 2
}]
};
let switchBool = false;
let svg = d3.select('svg')
.attr('width', '100%')
.attr('height', '100%');
const width = svg.node()
.getBoundingClientRect().width;
const height = svg.node()
.getBoundingClientRect().height;
console.log(`${width}, ${height}`);
svg = svg.append('g');
svg.append('g')
.attr('class', 'links');
svg.append('g')
.attr('class', 'nodes');
const simulation = d3.forceSimulation();
initSimulation();
let link = svg.select('.links')
.selectAll('line');
loadLinks();
let node = svg.select('.nodes')
.selectAll('.node');
loadNodes();
restartSimulation();
function initSimulation() {
simulation
.force('link', d3.forceLink())
.force('charge', d3.forceManyBody())
.force('collide', d3.forceCollide())
.force('forceX', d3.forceX().x(width/2))
.force('forceY', d3.forceY().y(height/2));
simulation.force('link')
.id((d) => d.id)
.distance(100)
.iterations(1);
simulation.force('collide')
.radius(10);
simulation.force('charge')
.strength(-100);
}
function loadLinks() {
link = svg.select('.links')
.selectAll('line')
.data(dataset.links, (d) => d.id)
.join(
enter => enter.append('line').attr('stroke', '#000000'),
);
}
function loadNodes() {
node = svg.select('.nodes')
.selectAll('.node')
.data(dataset.nodes, (d) => d.id)
.join(
enter => {
const nodes = enter.append('g')
.attr('class', 'node')
nodes.append('circle').attr('r', 10);
return nodes;
},
);
}
function restartSimulation() {
simulation.nodes(dataset.nodes);
simulation.force('link').links(dataset.links);
simulation.alpha(1).restart();
simulation.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', (d) => `translate(${d.x},${d.y})`);
}
function updateData() {
switchBool = !switchBool;
if (switchBool) {
dataset.nodes.push({id: 3});
dataset.links.push({id: 2, source: 1, target: 3});
} else {
dataset.nodes.pop();
dataset.links.pop();
}
loadLinks();
loadNodes();
restartSimulation();
}
<script src="https://d3js.org/d3.v6.min.js"></script>
<div>
<button onclick="updateData()">Add/Remove</button>
<svg></svg>
</div>
I've actually just found the solution to the problem.
I had both forceX and forceY with default paramaters, which meant that there was a force pushing node towards (0,0), changing this bit of code I was able to fix it:
.force('x', d3.forceX().x(width * 0.5))
.force('y', d3.forceY().y(height * 0.5));
I'm having issues where my D3 Force Graph is showing with nodes but not connecting the links.
I'm not sure what the issue is because my strokes are defined.
I'm not sure if it's an issue with the JSON Data format or what it could be. Where could the issue be?
I am using Angular D3 with D3.Js & what I am trying to build is a Force Directed Network Graph.
JSON Data I'm using:
https://gist.github.com/KoryJCampbell/f18f8a11030269739eabc7de05b38b11
graph.ts
loadForceDirectedGraph(nodes: Node[], links: Link[]) {
const svg = d3.select('svg');
const width = +svg.attr('width');
const height = +svg.attr('height');
const color = d3.scaleOrdinal(d3.schemeTableau10);
const simulation = d3.forceSimulation()
.force('link', d3.forceLink().id((d: Node) => d.name))// the id of the node
.force("charge", d3.forceManyBody().strength(-5).distanceMax(0.1 * Math.min(width, height)))
.force('center', d3.forceCenter(width / 2, height / 2));
console.log(nodes, links);
const link = svg.append('g')
.attr('class', 'links')
.selectAll('line')
.data(links)
.enter()
.append('line')
.attr('stroke-width', d => Math.sqrt(d.index))
.attr('stroke', 'black');
const node = svg.append('g')
.attr('class', 'nodes')
.selectAll('circle')
.data(nodes)
.enter()
.append('circle')
.attr('r', 5)
.attr("fill", function(d) { return color(d.company); })
.call(d3.drag()
.on('start', dragStarted)
.on('drag', dragged)
.on('end', dragEnded)
);
node.append('title').text((d) => d.name);
simulation
.nodes(nodes)
.on('tick', ticked);
simulation.force<d3.ForceLink<any, any>>('link')
.links(links);
function ticked() {
node
.attr('cx', d => d.x)
.attr('cy', d => d.y);
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);
}
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;
}
}
I think your links format in json file is wrong.
change links to this format.
links: [
{
source: "<paste source's name property>",
target: "<paste target's name property>",
index: 0
},
]
because you are doing:
.force('link', d3.forceLink().id((d: Node) => d.name))// the name of the node
I want tooltips to appear next to my mouse when it hovers over a node. I tried solutions I found on SO, but so far, only got this solution by Boxun to work, although it's not quite what I had in mind (D3.js: Position tooltips using element position, not mouse position?).
I was wondering why in my listener function,
.on('mousemove', function(d) {})
, the functions
Tooltips
.style("left", d3.mouse(this)[0])
.style("top", (d3.mouse(this)[1]))
or
Tooltips
.style("left", d3.event.pageX + 'px')
.style("top", d3.event.pageY + 'px')
shows up on top of the svg instead of where my mouse is.
From reading the answers to the link above, I think I have to transform my coordinates somehow, but I was not able to get that to work.
Here, I am using d3.event.pageX and my mouse is over cherry node.
import * as d3_base from "d3";
import * as d3_dag from "d3-dag";
const d3 = Object.assign({}, d3_base, d3_dag);
drawDAG({
graph: [
["apples", "banana"],
["cherry", "tomato"],
["cherry", "avocado"],
["squash", "banana"],
["lychee", "cherry"],
["dragonfruit", "mango"],
["tomato", "mango"]
]
})
async function drawDAG(response) {
loadDag(response['graph'])
.then(layoutAndDraw())
.catch(console.error.bind(console));
}
async function loadDag(source) {
const [key, reader] = ["zherebko", d3_dag.dagConnect().linkData(() => ({}))]
return reader(source);
}
function layoutAndDraw() {
const width = 800;
const height = 800;
const d3 = Object.assign({}, d3_base, d3_dag);
function sugiyama(dag) {
const layout = d3.sugiyama()
.size([width, height])
.layering(d3.layeringSimplex())
.decross(d3.decrossOpt())
.coord(d3.coordVert());
layout(dag);
draw(dag);
}
return sugiyama;
function draw(dag) {
// Create a tooltip
const Tooltip = d3.select("root")
.append("div")
.attr("class", "tooltip")
.style('position', 'absolute')
.style("opacity", 0)
.style("background-color", "black")
.style("padding", "5px")
.style('text-align', 'center')
.style('width', 60)
.style('height', 30)
.style('border-radius', 10)
.style('color', 'white')
// This code only handles rendering
const nodeRadius = 100;
const svgSelection = d3.select("root")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", `${-nodeRadius} ${-nodeRadius} ${width + 2 * nodeRadius} ${height + 2 * nodeRadius}`);
const defs = svgSelection.append('defs');
const steps = dag.size();
const interp = d3.interpolateRainbow;
const colorMap = {};
dag.each((node, i) => {
colorMap[node.id] = interp(i / steps);
});
// How to draw edges
const line = d3.line()
.curve(d3.curveCatmullRom)
.x(d => d.x)
.y(d => d.y);
// Plot edges
svgSelection.append('g')
.selectAll('path')
.data(dag.links())
.enter()
.append('path')
.attr('d', ({
data
}) => line(data.points))
.attr('fill', 'none')
.attr('stroke-width', 3)
.attr('stroke', ({
source,
target
}) => {
const gradId = `${source.id}-${target.id}`;
const grad = defs.append('linearGradient')
.attr('id', gradId)
.attr('gradientUnits', 'userSpaceOnUse')
.attr('x1', source.x)
.attr('x2', target.x)
.attr('y1', source.y)
.attr('y2', target.y);
grad.append('stop').attr('offset', '0%').attr('stop-color', colorMap[source.id]);
grad.append('stop').attr('offset', '100%').attr('stop-color', colorMap[target.id]);
return `url(#${gradId})`;
});
// Select nodes
const nodes = svgSelection.append('g')
.selectAll('g')
.data(dag.descendants())
.enter()
.append('g')
.attr('width', 100)
.attr('height', 100)
.attr('transform', ({
x,
y
}) => `translate(${x}, ${y})`)
.on('mouseover', function(d) {
Tooltip
.style('opacity', .8)
.text(d.id)
})
.on('mouseout', function(d) {
Tooltip
.style('opacity', 0)
})
.on('mousemove', function(d) {
var matrix = this.getScreenCTM()
.translate(+this.getAttribute("cx"), +this.getAttribute("cy"));
Tooltip
.html(d.id)
.style("left", (window.pageXOffset + matrix.e - 50) + "px")
.style("top", (window.pageYOffset + matrix.f - 60) + "px");
})
// Plot node circles
nodes.append('rect')
.attr('y', -30)
.attr('x', (d) => {
return -(d.id.length * 15 / 2)
})
.attr('rx', 10)
.attr('ry', 10)
.attr('width', (d) => {
return d.id.length * 15;
})
.attr('height', (d) => 60)
.attr('fill', n => colorMap[n.id])
// Add text to nodes
nodes.append('text')
.text(d => {
let id = '';
d.id.replace(/_/g, ' ').split(' ').forEach(str => {
if (str !== 'qb')
id += str.charAt(0).toUpperCase() + str.substring(1) + '\n';
});
return id;
})
.attr('font-size', 25)
.attr('font-weight', 'bold')
.attr('font-family', 'sans-serif')
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.attr('fill', 'white')
.attr();
}
}
You can try using this instead, this will make sure that the tooltip is displayed on the exact mouse position.
d3.event.offsetY
Hello I have this code that I use to create a stacked area chart:
updateArea(yOffset, data = [], categories) {
const parseTime = this.parseTime;
const xScale = this.getScale(yOffset, data, categories).date;
const yScale = this.getScale(yOffset, data, categories).y;
const area = d3.area()
.curve(d3.curveCardinal)
.x(d => xScale(parseTime(d.data.date)))
.y0(d => yScale(d[0] || 0))
.y1(d => yScale(d[1] || 0));
const stack = d3.stack()
.keys(categories)
.order(d3.stackOrderReverse)
.offset(d3.stackOffsetNone);
if (data.length > 0) {
const stackContainer = this.vis.append('g')
.attr('class', 'stack');
const layer = stackContainer.selectAll('.layer')
.data(stack(data))
.enter()
.append('g')
.attr('class', 'layer');
layer.append('path')
.attr('class', 'area')
.style('fill', (d, i) => d3.schemeCategory20[i])
.attr('d', area);
}
const legend = this.vis.append('g')
.attr('class', 'legend');
legend.selectAll('.legend-item')
.data(stack(data))
.enter()
.append('circle')
.attr('r', 5)
.attr('cx', 20)
.attr('cy', (d, i) => yOffset + 20 + i * 12)
.attr('stroke', 'none')
.attr('fill', (d, i) => d3.schemeCategory20[i]);
legend.selectAll('.legend-item')
.data(stack(data))
.enter()
.append('text')
.attr('class', 'legend-item')
.attr('x', 30)
.attr('y', (d, i) => yOffset + 24 + i * 12)
.text(d => d.key);
}
I want to un stack the stacked areas so that they overlap and I can then make the areas opacity .3 or something.
When I try and do this:
.data(data)
.enter()
.append('g')
.attr('class', 'layer');
None of the areas show up. So just trying to figure out why!!
Thanks!
I just ended up doing this:
layer.append('path')
.attr('class', 'area')
.style('fill', 'transparent')
.style('stroke', (d, i) => d3.schemeCategory20[i])
.style('stroke-width', 1)
.attr('d', area);
and that seemed to work. Thanks everyone for the help!
I've made a force directed graph and I wanted to change shape of nodes for data which contains "entity":"company" so they would have rectangle shape, and other one without this part of data would be circles as they are now.
You can see my working example with only circle nodes here: http://jsfiddle.net/dzorz/uWtSk/
I've tried to add rectangles with if else statement in part of code where I append shape to node like this:
function(d)
{
if (d.entity == "company")
{
node.append("rect")
.attr("class", function(d){ return "node type"+d.type})
.attr("width", 100)
.attr("height", 50)
.call(force.drag);
}
else
{
node.append("circle")
.attr("class", function(d){ return "node type"+d.type})
.attr("r", function(d) { return radius(d.value) || 10 })
//.style("fill", function(d) { return fill(d.type); })
.call(force.drag);
}
}
But then I did not get any shape at all on any node.
What Is a proper way to set up this?
The whole code looks like this:
script:
var data = {"nodes":[
{"name":"Action 4", "type":5, "slug": "", "value":265000},
{"name":"Action 5", "type":6, "slug": "", "value":23000},
{"name":"Action 3", "type":4, "slug": "", "value":115000},
{"name":"Yahoo", "type":1, "slug": "www.yahoo.com", "entity":"company"},
{"name":"Google", "type":1, "slug": "www.google.com", "entity":"company"},
{"name":"Action 1", "type":2, "slug": "",},
{"name":"Action 2", "type":3, "slug": "",},
{"name":"Bing", "type":1, "slug": "www.bing.com", "entity":"company"},
{"name":"Yandex", "type":1, "slug": "www.yandex.com)", "entity":"company"}
],
"links":[
{"source":0,"target":3,"value":10},
{"source":4,"target":3,"value":1},
{"source":1,"target":7,"value":10},
{"source":2,"target":4,"value":10},
{"source":4,"target":7,"value":1},
{"source":4,"target":5,"value":10},
{"source":4,"target":6,"value":10},
{"source":8,"target":4,"value":1}
]
}
var w = 560,
h = 500,
radius = d3.scale.log().domain([0, 312000]).range(["10", "50"]);
var vis = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h);
vis.append("defs").append("marker")
.attr("id", "arrowhead")
.attr("refX", 17 + 3) /*must be smarter way to calculate shift*/
.attr("refY", 2)
.attr("markerWidth", 6)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("path")
.attr("d", "M 0,0 V 4 L6,2 Z"); //this is actual shape for arrowhead
//d3.json(data, function(json) {
var force = self.force = d3.layout.force()
.nodes(data.nodes)
.links(data.links)
.distance(100)
.charge(-1000)
.size([w, h])
.start();
var link = vis.selectAll("line.link")
.data(data.links)
.enter().append("svg:line")
.attr("class", function (d) { return "link" + d.value +""; })
.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; })
.attr("marker-end", function(d) {
if (d.value == 1) {return "url(#arrowhead)"}
else { return " " }
;});
function openLink() {
return function(d) {
var url = "";
if(d.slug != "") {
url = d.slug
} //else if(d.type == 2) {
//url = "clients/" + d.slug
//} else if(d.type == 3) {
//url = "agencies/" + d.slug
//}
window.open("//"+url)
}
}
var node = vis.selectAll("g.node")
.data(data.nodes)
.enter().append("svg:g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("class", function(d){ return "node type"+d.type})
.attr("r", function(d) { return radius(d.value) || 10 })
//.style("fill", function(d) { return fill(d.type); })
.call(force.drag);
node.append("svg:image")
.attr("class", "circle")
.attr("xlink:href", function(d){ return d.img_href})
.attr("x", "-16px")
.attr("y", "-16px")
.attr("width", "32px")
.attr("height", "32px")
.on("click", openLink());
node.append("svg:text")
.attr("class", "nodetext")
.attr("dx", 0)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name });
force.on("tick", function() {
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("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
});
//});
css:
.link10 { stroke: #ccc; stroke-width: 3px; stroke-dasharray: 3, 3; }
.link1 { stroke: #000; stroke-width: 3px;}
.nodetext { pointer-events: none; font: 10px sans-serif; }
.node.type1 {
fill:brown;
}
.node.type2 {
fill:#337147;
}
.node.type3 {
fill:blue;
}
.node.type4 {
fill:red;
}
.node.type5 {
fill:#1BC9E0;
}
.node.type6 {
fill:#E01B98;
}
image.circle {
cursor:pointer;
}
You can edit my jsfiddle linked on beginning of post...
Solution here: http://jsfiddle.net/Bull/4btFx/1/
I got this to work by adding a class to each node, then using "selectAll" for each class to add the shapes. In the code below, I'm adding a class "node" and a class returned by my JSON (d.type) which is either "rect" or "ellipse".
var node = container.append("g")
.attr("class", "nodes")
.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", function(d) {
return d.type + " node";
})
.call(drag);
Then you can add the shape for all elements of each class:
d3.selectAll(".rect").append("rect")
.attr("width", window.nodeWidth)
.attr("height", window.nodeHeight)
.attr("class", function(d) {
return "color_" + d.class
});
d3.selectAll(".ellipse").append("rect")
.attr("rx", window.nodeWidth*0.5)
.attr("ry", window.nodeHeight*0.5)
.attr("width", window.nodeWidth)
.attr("height", window.nodeHeight)
.attr("class", function(d) {
return "color_" + d.class
});
In the above example, I used rectangles with radius to draw the ellipses since it centers them the same way as the rectangles. But it works with other shapes too. In the jsfiddle I linked, the centering is off, but the shapes are right.
I implemented this behavior using the filter method that I gleaned from Filtering in d3.js on bl.ocks.org.
initGraphNodeShapes() {
let t = this;
let graphNodeCircles =
t.graphNodesEnter
.filter(d => d.shape === "circle")
.append("circle")
.attr("r", 15)
.attr("fill", "green");
let graphNodeRects =
t.graphNodesEnter
.filter(d => d.shape === "rect")
.append("rect")
.attr("width", 20)
.attr("height", 10)
.attr("x", -10) // -1/2 * width
.attr("y", -5) // -1/2 * height
.attr("fill", "blue");
return graphNodeCircles.merge(graphNodeRects);
}
I have this inside of initGraphNodeShapes call because my code is relatively large and refactored. The t.graphNodesEnter is a reference to the data selection after the data join enter() call elsewhere. Ping me if you need more context. Also, I use the d => ... version because I'm using ES6 which enables lambdas. If you're using pre-ES6, then you'll have to change it to the function(d)... form.
This is an older post, but I had the same trouble trying to get this concept working with D3 v5 in July of 2020. Here is my solution in case anyone else is trying to build a force-directed graph, I used both circle and rectangle elements to represent different types of nodes:
The approach was to create the elements, and then position them separately when invoking the force simulation (since a circle takes cx, cy, and r attributes, and the rect takes x, y, width and height). Much of this code follows the example in this blog post on medium: https://medium.com/ninjaconcept/interactive-dynamic-force-directed-graphs-with-d3-da720c6d7811
FYI I've declared 'svg' previously as the d3.select("some div with id or class"), along with a few helper functions not shown that read the data (setNodeSize, setNodeColor). I've used the D3.filter method to check for boolean field in the data - is the node initial or no?
Force simulation instance:
const simulation = d3.forceSimulation()
//the higher the strength (if negative), greater distance between nodes.
.force('charge', d3.forceManyBody().strength(-120))
//places the chart in the middle of the content area...if not it's top-left
.force('center', d3.forceCenter(width / 2, height / 2))
Create the circle nodes:
const nodeCircles = svg.append('g')
.selectAll('circle')
.data(nodes)
.enter()
.filter(d => d.initial)
.append('circle')
.attr('r', setNodeSize)
.attr('class', 'node')
.attr('fill', setNodeColor)
.attr('stroke', '#252525')
.attr('stroke-width', 2)
Then create the rectangle nodes:
const nodeRectangles = svg.append('g')
.selectAll('rect')
.data(nodes)
.enter()
.filter(d => !d.initial)
.append('rect')
.attr('width', setNodeSize)
.attr('height', setNodeSize)
.attr('class', 'node')
.attr('fill', setNodeColor)
.attr('stroke', '#252525')
.attr('stroke-width', 2)
And then when invoking the simulation:
simulation.nodes(nodes).on("tick", () => {
nodeCircles
.attr("cx", node => node.x)
.attr("cy", node => node.y)
nodeRectangles
.attr('x', node => node.x)
.attr('y', node => node.y)
.attr('transform', 'translate(-10, -7)')
Of course there's more to it to add the lines/links, text-labels etc. Feel free to ping me for more code. The medium post listed above is very helpful!
I am one step ahead of you :)
I resolved your problem with using "path" instead of "circle" or "rect", you can look my solution and maybe help me to fix problem which I have...
D3 force-directed graph: update node position