I am working on a small D3.js graph and want to hide the connected smaller nodes if the parent larger node was clicked. So far I tried several approaches and to filter the links first to receive the source nodes afterwards might be my best attempt.
Unfortunately I receive newLinks.map is not a function error
, either I totally missunderstand the map function or simply miss the final peace.
function onClick(event, d) {
const newLinks = link.filter(link => link.target.id === d.id);
console.log(newLinks)
const newNodes = newLinks.map(link => data.nodes.find(newNode => newNode.id === link.source.id))
console.log(newNodes)
}
Question: How can I achieve what I am seeking for? I want to hide the smaller attached nodes if the parent node was clicked.
var svg = d3.select("body").append("svg")
.attr("width", window.innerWidth)
.attr("height", window.innerHeight)
var data = {
"nodes": [{
"id": "A",
"type": "parent"
},
{
"id": "B",
"type": "parent"
},
{
"id": "C",
"type": "child"
},
{
"id": "D",
"type": "child"
},
{
"id": "E",
"type": "child"
},
],
"links": [{
"source": "A",
"target": "B",
"distance": 125
},
{
"source": "C",
"target": "A",
"distance": 20
},
{
"source": "D",
"target": "A",
"distance": 20
},
{
"source": "E",
"target": "B",
"distance": 20
},
]
}
var force = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id
}).distance(function(d) {
return d.distance
}))
.force("center", d3.forceCenter(window.innerWidth / 2, window.innerHeight / 2))
.force("charge", d3.forceManyBody().strength(-1000))
.force("collision", d3.forceCollide().radius(setSize))
var linksContainer = svg.append("g").attr("class", "linksContainer")
var nodesContainer = svg.append("g").attr("class", "nodesContainer")
initialize()
function initialize() {
link = linksContainer.selectAll("line")
.data(data.links)
.join("line")
.attr("class", "line")
node = nodesContainer.selectAll(".node")
.data(data.nodes, d => d.id)
.join("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
node.selectAll("circle")
.data(d => [d])
.join("circle")
.attr("r", setSize)
.on("click", onClick)
force
.nodes(data.nodes)
.on("tick", ticked)
force
.force("link")
.links(data.links)
}
function setSize(d) {
switch (d.type) {
case "parent":
return 40
case "child":
return 20
default:
return 40
}
}
function onClick(event, d) {
const newLinks = link.filter(link => link.target.id === d.id);
console.log(newLinks)
const newNodes = newLinks.map(link => data.nodes.find(newNode => newNode.id === link.source.id))
console.log(newNodes)
}
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("transform", function(d) {
return "translate(" d.x ", " d.y ")";
});
}
function dragStarted(event, d) {
if (!event.active) force.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) force.alphaTarget(0);
d.fx = undefined;
d.fy = undefined;
}
body {
overflow: hidden;
background: #e6e7ee;
margin: 0;
}
circle {
fill: whitesmoke;
stroke: black;
}
line {
stroke: black;
}
<script src="https://d3js.org/d3.v7.js"></script>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
CodePudding user response:
You need to understand the difference between an array of data and a d3 selection. In your code, link
and node
are d3 selections representing the circles and lines. They are not representations of the underlying data.
They do, however, supply some functions that can be useful in this context. For example, you can use .each(...)
instead of forEach
to loop over the elements, or .data()
to get the objects that the d3 selection represents as an array, so link.data().map
is definitely a valid function.
I implemented what you want by adding a property isVisible
to every node or link that determines visibility. However, this is absolutely not the best or only way to do this, so do feel free to explore alternatives.
var svg = d3.select("body").append("svg")
.attr("width", window.innerWidth)
.attr("height", window.innerHeight)
var data = {
"nodes": [{
"id": "A",
"type": "parent"
},
{
"id": "B",
"type": "parent"
},
{
"id": "C",
"type": "child"
},
{
"id": "D",
"type": "child"
},
{
"id": "E",
"type": "child"
},
],
"links": [{
"source": "A",
"target": "B",
"distance": 125
},
{
"source": "C",
"target": "A",
"distance": 20
},
{
"source": "D",
"target": "A",
"distance": 20
},
{
"source": "E",
"target": "B",
"distance": 20
},
]
}
var force = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id
}).distance(function(d) {
return d.distance
}))
.force("center", d3.forceCenter(window.innerWidth / 2, window.innerHeight / 2))
.force("charge", d3.forceManyBody().strength(-1000))
.force("collision", d3.forceCollide().radius(setSize))
var linksContainer = svg.append("g").attr("class", "linksContainer")
var nodesContainer = svg.append("g").attr("class", "nodesContainer")
initialize()
function initialize() {
const links = data.links
.filter(link => link.isVisible !== false);
const nodes = data.nodes.filter(node =>
node.isVisible !== false);
link = linksContainer.selectAll("line")
.data(links)
.join("line")
.attr("class", "line")
node = nodesContainer.selectAll(".node")
.data(nodes, d => d.id)
.join("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
node.selectAll("circle")
.data(d => [d])
.join("circle")
.attr("r", setSize)
.on("click", onClick)
force
.nodes(nodes)
.on("tick", ticked)
force
.force("link")
.links(links)
}
function setSize(d) {
switch (d.type) {
case "parent":
return 40
case "child":
return 20
default:
return 40
}
}
function onClick(event, d) {
link.data()
.forEach(link => {
link.isVisible = link.target.id === d.id;
});
const visibleNodeIds = [
d.id,
...link.data()
.filter(l => l.isVisible)
.map(l => l.source.id)
];
node.data()
.forEach(node => {
node.isVisible = visibleNodeIds.includes(node.id);
});
initialize();
}
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("transform", function(d) {
return "translate(" d.x ", " d.y ")";
});
}
function dragStarted(event, d) {
if (!event.active) force.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) force.alphaTarget(0);
d.fx = undefined;
d.fy = undefined;
}
body {
overflow: hidden;
background: #e6e7ee;
margin: 0;
}
circle {
fill: whitesmoke;
stroke: black;
}
line {
stroke: black;
}
<script src="https://d3js.org/d3.v7.js"></script>
<iframe name="sif2" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>