Reputation: 341
I got a small D3 forced graph with 3 main nodes. Those nodes contains a attribute shoes, which hold a integer. On top of the graph are two buttons, to either add or remove shoes. As soon as one of those buttons are clicked, I want to update the D3 forced graph data. Basically the integer value in the blue node should either increase or decrease.
I searched and found several stackoverflow articles which explain the steps to achieve my need. Unforutnately I was not able yet to successfully map those articles on my prototype.
The problem is: It adds an element to the last data node but does not visually change the amount in the blue circle. The console output instead shows the correct value and increases or decreases the amount of shoes correctly.
What do I miss?
var width = window.innerWidth,
height = window.innerHeight;
var buttons = d3.select("body").selectAll("button")
.data(["add Shoes", "remove Shoes"])
.enter()
.append("button")
.text(function(d) {
return d;
})
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function(event) {
svg.attr("transform", event.transform)
}))
.append("g")
////////////////////////
// outer force layout
var data = {
"nodes":[
{ "id": "A", "shoes": 1},
{ "id": "B", "shoes": 1},
{ "id": "C", "shoes": 0},
],
"links": [
{ "source": "A", "target": "B"},
{ "source": "B", "target": "C"},
{ "source": "C", "target": "A"}
]
};
var simulation = d3.forceSimulation()
.force("size", d3.forceCenter(width / 2, height / 2))
.force("charge", d3.forceManyBody().strength(-1000))
.force("link", d3.forceLink().id(function (d) { return d.id }).distance(250))
linksContainer = svg.append("g").attr("class", "linkscontainer")
nodesContainer = svg.append("g").attr("class", "nodesContainer")
var links = linksContainer.selectAll("g")
.data(data.links)
.join("g")
.attr("fill", "transparent")
var linkLine = linksContainer.selectAll(".linkPath")
.data(data.links)
.join("path")
.attr("stroke", "red")
.attr("fill", "transparent")
.attr("stroke-width", 3)
nodes = nodesContainer.selectAll(".nodes")
.data(data.nodes, function (d) { return d.id; })
.join("g")
.attr("class", "nodes")
.attr("id", function (d) { return d.id; })
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
nodes.selectAll("circle")
.data(d => [d])
.join("circle")
.style("fill", "lightgrey")
.style("stroke", "blue")
.attr("r", 40)
var smallCircle = nodes.selectAll("g")
//.data(d => d.shoes)
.data(d => [d])
.enter()
.filter(function(d) {
return d.shoes !== 0;
})
.append("g")
.attr("cursor", "pointer")
.attr("transform", function(d, i) {
const factor = (i / 40) * (15 / 2) * 5;
return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
});
smallCircle.append('circle')
.attr("class", "circle-small")
.attr('r', 15)
.attr("fill", "blue")
smallCircle.append("text")
.attr("font-size", 15)
.attr("fill", "white")
.attr("dominant-baseline", "central")
.style("text-anchor", "middle")
.attr("pointer-events", "cursor")
.text(function(d) {
return d.shoes;
})
simulation
.nodes(data.nodes)
.on("tick", tick)
simulation
.force("link")
.links(data.links)
function tick() {
linkLine.attr("d", function(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;
})
nodes
.attr("transform", d => `translate(${d.x}, ${d.y})`);
}
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
buttons.on("click", function(d) {
if (d.srcElement.__data__ == "add Shoes") {
data.nodes.forEach(function(item) {
item.shoes = item.shoes + 1
})
} else if (d.srcElement.__data__ == "remove Shoes") {
data.nodes.forEach(function(item) {
if (!item.shoes == 0) {
item.shoes = item.shoes - 1
}
})
}
restart()
})
function restart() {
// Apply the general update pattern to the nodes.
smallCircle = nodes.selectAll("g")
.data(d => [d])
.enter()
.filter(function(d) {
return d.shoes !== 0;
})
.append("g")
.attr("cursor", "pointer")
.attr("transform", function(d, i) {
const factor = (i / 40) * (15 / 2) * 5;
return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
});
smallCircle.append("circle")
.attr("class", "circle-small")
.attr('r', 15)
.attr("fill", "blue")
smallCircle.append("text")
.attr("font-size", 15)
.attr("fill", "white")
.attr("dominant-baseline", "central")
.style("text-anchor", "middle")
.text(function(d) {
return d.shoes;
})
smallCircle.exit().remove();
// Update and restart the simulation.
simulation.nodes(data.nodes);
simulation.restart()
}
body {
background: whitesmoke,´;
overflow: hidden;
margin: 0px;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3v7</title>
<!-- d3.js framework -->
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
</body>
</html>
For further d3 selection behavings.
I accepted the answer, which is completely correct. In addition I tried to add another g element on the same level and seems the d3 selector can´t handle selections by group. At least selectAll("gblue") behaves different than selectAll("g")
var width = window.innerWidth,
height = window.innerHeight;
var buttons = d3.select("body").selectAll("button")
.data(["add blue", "remove blue", "add red", "remove red"])
.enter()
.append("button")
.text(function (d) {
return d;
})
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function (event) {
svg.attr("transform", event.transform)
}))
.append("g")
////////////////////////
// outer force layout
var data = {
"nodes": [{
"id": "A",
"blue": 1,
"red": 0,
},
{
"id": "B",
"blue": 1,
"red": 1
},
{
"id": "C",
"blue": 0,
"red": 1
},
],
"links": [{
"source": "A",
"target": "B"
},
{
"source": "B",
"target": "C"
},
{
"source": "C",
"target": "A"
}
]
};
var simulation = d3.forceSimulation()
.force("size", d3.forceCenter(width / 2, height / 2))
.force("charge", d3.forceManyBody().strength(-1000))
.force("link", d3.forceLink().id(function (d) {
return d.id
}).distance(250))
linksContainer = svg.append("g").attr("class", "linkscontainer")
nodesContainer = svg.append("g").attr("class", "nodesContainer")
var links = linksContainer.selectAll("g")
.data(data.links)
.join("g")
.attr("fill", "transparent")
var linkLine = linksContainer.selectAll(".linkPath")
.data(data.links)
.join("path")
.attr("stroke", "red")
.attr("fill", "transparent")
.attr("stroke-width", 3)
nodes = nodesContainer.selectAll(".nodes")
.data(data.nodes, function (d) {
return d.id;
})
.join("g")
.attr("class", "nodes")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
nodes.selectAll("circle")
.data(d => [d])
.join("circle")
.style("fill", "lightgrey")
.style("stroke", "blue")
.attr("r", 40)
var blueNode = nodes.selectAll("gblue")
.data(d => d.blue ? [d] : [])
.enter()
.append("g")
.attr("class", "gblue")
.attr("cursor", "pointer")
.attr("transform", function (d, i) {
const factor = (i / 40) * (15 / 2) * 5;
return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
});
blueNode.append('circle')
.attr('r', 15)
.attr("fill", "blue")
blueNode.append("text")
.attr("font-size", 15)
.attr("fill", "white")
.attr("dominant-baseline", "central")
.style("text-anchor", "middle")
.attr("pointer-events", "cursor")
.text(function (d) {
return d.blue;
})
var redNode = nodes.selectAll("gred")
.data(d => d.red ? [d] : [])
.enter()
.append("g")
.attr("class", "gred")
.attr("cursor", "pointer")
.attr("transform", function (d, i) {
const factor = (i / 40) * (15 / 2) * 5;
return `translate(${40 * Math.cos(factor - Math.PI * 0.3)},${40 * Math.sin(factor - Math.PI * 0.3)})`;
});
redNode.append('circle')
.attr('r', 15)
.attr("fill", "red")
redNode.append("text")
.attr("font-size", 15)
.attr("fill", "white")
.attr("dominant-baseline", "central")
.style("text-anchor", "middle")
.attr("pointer-events", "cursor")
.text(function (d) {
return d.red;
})
simulation
.nodes(data.nodes)
.on("tick", tick)
simulation
.force("link")
.links(data.links)
function tick() {
linkLine.attr("d", function (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;
})
nodes
.attr("transform", d => `translate(${d.x}, ${d.y})`);
}
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
buttons.on("click", function (_, d) {
if (d === "add blue") {
data.nodes.forEach(function (item) {
item.blue = item.blue + 1
})
} else if (d === "remove blue") {
data.nodes.forEach(function (item) {
if (!item.blue == 0) {
item.blue = item.blue - 1
}
})
} else if (d === "add red") {
data.nodes.forEach(function (item) {
item.red = item.red + 1
})
} else if (d === "remove red") {
data.nodes.forEach(function (item) {
if (!item.red == 0) {
item.red = item.red - 1
}
})
}
restart()
})
function restart() {
// Apply the general update pattern to the nodes.
let blueNode = nodes.selectAll("g")
.data(d => d.blue ? [d] : []);
blueNode.exit().remove();
const blueNodeEnter = blueNode.enter()
.append("g")
.attr("class", "blue")
.attr("cursor", "pointer")
.attr("transform", function (d, i) {
const factor = (i / 40) * (15 / 2) * 5;
return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
});
blueNodeEnter.append("circle")
.attr('r', 15)
.attr("fill", "blue")
blueNodeEnter.append("text")
.attr("font-size", 15)
.attr("fill", "white")
.attr("dominant-baseline", "central")
.style("text-anchor", "middle")
.text(function (d) {
return d.blue;
});
blueNode = blueNodeEnter.merge(blueNode);
blueNode.select("text")
.text(function (d) {
return d.blue;
});
let redNode = nodes.selectAll("g")
.data(d => d.red ? [d] : []);
redNode.exit().remove();
const redNodeEnter = redNode.enter()
.append("g")
.attr("class", "red")
.attr("cursor", "pointer")
.attr("transform", function (d, i) {
const factor = (i / 40) * (15 / 2) * 5;
return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
});
redNodeEnter.append("circle")
.attr('r', 15)
.attr("fill", "blue")
redNodeEnter.append("text")
.attr("font-size", 15)
.attr("fill", "white")
.attr("dominant-baseline", "central")
.style("text-anchor", "middle")
.text(function (d) {
return d.red;
});
redNode = redNodeEnter.merge(redNode);
redNode.select("text")
.text(function (d) {
return d.red;
});
// Update and restart the simulation.
simulation.nodes(data.nodes);
simulation.restart()
}
.link {
stroke: #000;
stroke-width: 1.5px;
}
.nodes {
fill: whitesmoke;
}
<!DOCTYPE html>
<html lang="de">
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
<meta charset="utf-8">
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.3.js"></script>
<!-- D3 -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<!-- fontawesome stylesheet https://fontawesome.com/ -->
<script src="https://kit.fontawesome.com/98a5e27706.js" crossorigin="anonymous"></script>
</head>
<body>
</body>
</html>
Upvotes: 2
Views: 87
Reputation: 102174
First of all, do not mess with private variables, conventionally assigned as __foo__
. So, instead of...
buttons.on("click", function(d) {
if (d.srcElement.__data__ == "add Shoes") { etc...
...just do:
buttons.on("click", function(_, d) {
if (d == "add Shoes") {
Back to the problem: the issue here is an incorrect enter-update-exit pattern. This should be it:
//the update selection:
let smallCircle = nodes.selectAll("g")
.data(d => d.shoes ? [d] : []);
//the exit selection:
smallCircle.exit().remove();
//the enter selection:
const smallCircleEnter = smallCircle.enter()
.append("g")
//etc...
//appending elements in the enter selection only:
smallCircleEnter.append("circle")
//etc...
smallCircleEnter.append("text")
//etc...
//merging the enter and the update selections:
smallCircle = smallCircleEnter.merge(smallCircle);
//modifying the update selection
smallCircle.select("text")
.text(function(d) {
return d.shoes;
});
Also, I'm removing that filter and passing an empty array if the number of shoes is zero.
Here's your code with those changes:
var width = window.innerWidth,
height = window.innerHeight;
var buttons = d3.select("body").selectAll("button")
.data(["add Shoes", "remove Shoes"])
.enter()
.append("button")
.text(function(d) {
return d;
})
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function(event) {
svg.attr("transform", event.transform)
}))
.append("g")
////////////////////////
// outer force layout
var data = {
"nodes": [{
"id": "A",
"shoes": 1
},
{
"id": "B",
"shoes": 1
},
{
"id": "C",
"shoes": 0
},
],
"links": [{
"source": "A",
"target": "B"
},
{
"source": "B",
"target": "C"
},
{
"source": "C",
"target": "A"
}
]
};
var simulation = d3.forceSimulation()
.force("size", d3.forceCenter(width / 2, height / 2))
.force("charge", d3.forceManyBody().strength(-1000))
.force("link", d3.forceLink().id(function(d) {
return d.id
}).distance(250))
linksContainer = svg.append("g").attr("class", "linkscontainer")
nodesContainer = svg.append("g").attr("class", "nodesContainer")
var links = linksContainer.selectAll("g")
.data(data.links)
.join("g")
.attr("fill", "transparent")
var linkLine = linksContainer.selectAll(".linkPath")
.data(data.links)
.join("path")
.attr("stroke", "red")
.attr("fill", "transparent")
.attr("stroke-width", 3)
nodes = nodesContainer.selectAll(".nodes")
.data(data.nodes, function(d) {
return d.id;
})
.join("g")
.attr("class", "nodes")
.attr("id", function(d) {
return d.id;
})
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
nodes.selectAll("circle")
.data(d => [d])
.join("circle")
.style("fill", "lightgrey")
.style("stroke", "blue")
.attr("r", 40)
var smallCircle = nodes.selectAll("g")
//.data(d => d.shoes)
.data(d => [d])
.enter()
.filter(function(d) {
return d.shoes !== 0;
})
.append("g")
.attr("cursor", "pointer")
.attr("transform", function(d, i) {
const factor = (i / 40) * (15 / 2) * 5;
return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
});
smallCircle.append('circle')
.attr("class", "circle-small")
.attr('r', 15)
.attr("fill", "blue")
smallCircle.append("text")
.attr("font-size", 15)
.attr("fill", "white")
.attr("dominant-baseline", "central")
.style("text-anchor", "middle")
.attr("pointer-events", "cursor")
.text(function(d) {
return d.shoes;
})
simulation
.nodes(data.nodes)
.on("tick", tick)
simulation
.force("link")
.links(data.links)
function tick() {
linkLine.attr("d", function(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;
})
nodes
.attr("transform", d => `translate(${d.x}, ${d.y})`);
}
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
buttons.on("click", function(_, d) {
if (d === "add Shoes") {
data.nodes.forEach(function(item) {
item.shoes = item.shoes + 1
})
} else if (d === "remove Shoes") {
data.nodes.forEach(function(item) {
if (!item.shoes == 0) {
item.shoes = item.shoes - 1
}
})
}
restart()
})
function restart() {
// Apply the general update pattern to the nodes.
let smallCircle = nodes.selectAll("g")
.data(d => d.shoes ? [d] : []);
smallCircle.exit().remove();
const smallCircleEnter = smallCircle.enter()
.append("g")
.attr("cursor", "pointer")
.attr("transform", function(d, i) {
const factor = (i / 40) * (15 / 2) * 5;
return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
});
smallCircleEnter.append("circle")
.attr("class", "circle-small")
.attr('r', 15)
.attr("fill", "blue")
smallCircleEnter.append("text")
.attr("font-size", 15)
.attr("fill", "white")
.attr("dominant-baseline", "central")
.style("text-anchor", "middle")
.text(function(d) {
return d.shoes;
});
smallCircle = smallCircleEnter.merge(smallCircle);
smallCircle.select("text")
.text(function(d) {
return d.shoes;
});
// Update and restart the simulation.
simulation.nodes(data.nodes);
simulation.restart()
}
body {
background: whitesmoke, ´;
overflow: hidden;
margin: 0px;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3v7</title>
<!-- d3.js framework -->
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
</body>
</html>
Upvotes: 2