Skip to content
Advertisement

Hide attached child nodes onClick()

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>

Advertisement

Answer

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>
User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement