3

Here is the screen that I want to achieve with d3 js. actual requirement

On clicking of circle that are having child circle can be toggled (collapsed/expand).

I have tried with below snippet. Only thing I am not able to achieve is once parent circle collapses I need to shift all the linking of the child circle to its parent. Any lead would be great.

const data = {
    name: "root",
  children: [
    {
        name: "A",
      children: [
        {name: "A1", value: 7}, {name: "A2", value: 8}, {name: "A3", value: 9}, {name: "A4", value: 10}, {name: "A5", value: 10}
      ]
    },
    {
        name: "B",
      children: [
        {name: "B1", value: 11}, {name: "B2", value: 7}, {name: "B3", value: 8},
      ]
    },
    {
      name: "C",
      value: 10
    },
    {
      name: "D",
      value: 10
    },
    {
      name: "E",
      value: 10
    }
  ],
  links: [{from: "A3", to: "C"}, {from: "A2", to: "E"}, {from: "B1", to: "D"}, {from: "B2", to: "B3"}, {from: "B1", to: "C"}]
};

const findNode = (parent, name) => {
    if (parent.name === name)
    return parent;
  if (parent.children) {
    for (let child of parent.children) {
        const found = findNode(child, name);
      if (found) {
        return found;
      }
    }
  } 
  return null;
}

const svg = d3.select("svg");

const container = svg.append('g')
  .attr('transform', 'translate(0,0)')
  
const onClickNode = (e, d) => {
  e.stopPropagation();
  e.preventDefault();
  
  const node = findNode(data, d.data.name);
  if(node.children && !node._children) {
    node._children = node.children;
    node.children = undefined;
    node.value = 20;
    updateGraph(data);
  } else {
    if (node._children && !node.children) {
        node.children = node._children;
      node._children = undefined;
      node.value = undefined;
      updateGraph(data);
    }
  }
}  

const updateGraph = graphData => {
    const pack = data => d3.pack()
    .size([600, 600])
    .padding(0)
    (d3.hierarchy(data)
    .sum(d => d.value * 3.5)
    .sort((a, b) => b.value - a.value));

    const root = pack(graphData);    
    
    const nodes = root.descendants().slice(1);  
  console.log('NODES: ', nodes);

    const nodeElements = container
    .selectAll("g.node")
    .data(nodes, d => d.data.name);
    
    const addedNodes = nodeElements.enter()
    .append("g")
    .classed('node', true)
    .style('cursor', 'pointer')
    .on('click', (e, d) => onClickNode(e, d));
    
  addedNodes.append('circle')
    .attr('stroke', 'black')
  
  addedNodes.append("text")
    .text(d => d.data.name)
    .attr('text-anchor', 'middle')
    .attr('alignment-baseline', 'middle')
    .style('visibility', 'hidden')
    .style('fill', 'black');
  
  const mergedNodes = addedNodes.merge(nodeElements);
  mergedNodes
    .transition()
    .duration(500)
    .attr('transform', d => `translate(${d.x},${d.y})`);
    
  mergedNodes.select('circle')
    .attr("fill", d => d.children ? "#ffe0e0" : "#ffefef")
    .transition()
    .duration(1000)
    .attr('r', d => d.value)
    mergedNodes.select('text')
    .attr('dy', d => d.children ? d.value + 10 : 0)
    .transition()
    .delay(1000)
    .style('visibility', 'visible')
    
  const exitedNodes = nodeElements.exit()
  exitedNodes.select('circle')
    .transition()
    .duration(500)
    .attr('r', 1);
 exitedNodes.select('text')
   .remove();   
    
 exitedNodes   
    .transition()
    .duration(750)
    .remove();

    const linkPath = d => {
        const from = nodes.find(n => n.data.name === d.from);
        const to = nodes.find(n => n.data.name === d.to);
    if (!from || !to)
        return null;
      
        const length = Math.hypot(from.x - to.x, from.y - to.y);
        const fd = from.value / length;
        const fx = from.x + (to.x - from.x) * fd;
        const fy = from.y + (to.y - from.y) * fd;
 
        const td = to.value / length;
        const tx = to.x + (from.x - to.x) * td;
        const ty = to.y + (from.y - to.y) * td;
        return `M ${fx},${fy} L ${tx},${ty}`; 
    };  
  
  const linkElements = container.selectAll('path.link')
    .data(data.links.filter(linkPath));
  
  const addedLinks = linkElements.enter()
    .append('path')
    .classed('link', true)
    .attr('marker-end', 'url(#arrowhead-to)')
    .attr('marker-start', 'url(#arrowhead-from)');
    
    addedLinks.merge(linkElements)
        .transition()
    .delay(750)
    .attr('d', linkPath)
    
  linkElements.exit().remove();  
}  

updateGraph(data);
text {
  font-family: "Ubuntu";
  font-size: 12px;
}

.link {
  stroke: blue;
  fill: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>

<svg width="600" height="600">
  <defs>
    <marker id="arrowhead-to" markerWidth="10" markerHeight="7" 
    refX="10" refY="3.5" orient="auto">
      <polygon fill="blue" points="0 0, 10 3.5, 0 7" />
    </marker>
    <marker id="arrowhead-from" markerWidth="10" markerHeight="7" 
    refX="0" refY="3.5" orient="auto">
      <polygon fill="blue" points="10 0, 0 3.5, 10 7" />
    </marker>
  </defs>
</svg>
ranjeet kumar
  • 251
  • 2
  • 10

1 Answers1

2

Substiture the links for collapsed nodes:

const links = data.links.map(link => {
  let from = nodes.find(n => n.data.name === link.from);
  if (!from) {
    const ancestors = findNodeAncestors(data, link.from);
    for (let index = 1; !from && index < ancestors.length  -1; index++) {
      from = nodes.find(n => n.data.name === ancestors[index].name)
    }
  }
  let to = nodes.find(n => n.data.name === link.to);
  if (!to) {
    const ancestors = findNodeAncestors(data, link.to);
    for (let index = 1; !to && index < ancestors.length  -1; index++) {
      to = nodes.find(n => n.data.name === ancestors[index].name)
    }
  }
  return {from, to};
});

Here is a code for findNodeAncestors :

const findNodeAncestors = (parent, name) => {
  if (parent.name === name)
    return [parent];
  const children = parent.children || parent._children;   
  if (children) {
    for (let child of children) {
      const found = findNodeAncestors(child, name);
      if (found) {
        return [...found, parent];
      }
    }
  } 
  return null;
}

const data = {
    name: "root",
  children: [
    {
        name: "A",
      children: [
        {name: "A1", value: 7}, {name: "A2", value: 8}, {name: "A3", value: 9}, {name: "A4", value: 10}, {name: "A5", value: 10}
      ]
    },
    {
        name: "B",
      children: [
        {name: "B1", value: 11}, {name: "B2", value: 7}, {name: "B3", value: 8},
      ]
    },
    {
      name: "C",
      value: 10
    },
    {
      name: "D",
      value: 10
    },
    {
      name: "E",
      value: 10
    }
  ],
  links: [{from: "A3", to: "C"}, {from: "A2", to: "E"}, {from: "B1", to: "D"}, {from: "B2", to: "B3"}, {from: "B1", to: "C"}, {from: "B1", to: "A5"}]
};

const findNode = (parent, name) => {
    if (parent.name === name)
    return parent;
  if (parent.children) {
    for (let child of parent.children) {
        const found = findNode(child, name);
      if (found) {
        return found;
      }
    }
  } 
  return null;
}

const findNodeAncestors = (parent, name) => {
    if (parent.name === name)
    return [parent];
  const children = parent.children || parent._children;   
  if (children) {
    for (let child of children) {
        const found = findNodeAncestors(child, name);
      //console.log('FOUND: ', found);
      if (found) {
        return [...found, parent];
      }
    }
  } 
  return null;
}

const svg = d3.select("svg");

const container = svg.append('g')
  .attr('transform', 'translate(0,0)')
  
const onClickNode = (e, d) => {
  e.stopPropagation();
  e.preventDefault();
  
  const node = findNode(data, d.data.name);
  if(node.children && !node._children) {
    node._children = node.children;
    node.children = undefined;
    node.value = 20;
    updateGraph(data);
  } else {
    if (node._children && !node.children) {
        node.children = node._children;
      node._children = undefined;
      node.value = undefined;
      updateGraph(data);
    }
  }
}  

const updateGraph = graphData => {
    const pack = data => d3.pack()
    .size([600, 600])
    .padding(0)
    (d3.hierarchy(data)
    .sum(d => d.value * 3.5)
    .sort((a, b) => b.value - a.value));

    const root = pack(graphData);        
    const nodes = root.descendants().slice(1);  

    const nodeElements = container
    .selectAll("g.node")
    .data(nodes, d => d.data.name);
    
    const addedNodes = nodeElements.enter()
    .append("g")
    .classed('node', true)
    .style('cursor', 'pointer')
    .on('click', (e, d) => onClickNode(e, d));
    
  addedNodes.append('circle')
    .attr('stroke', 'black')
  
  addedNodes.append("text")
    .text(d => d.data.name)
    .attr('text-anchor', 'middle')
    .attr('alignment-baseline', 'middle')
    .style('visibility', 'hidden')
    .style('fill', 'black');
  
  const mergedNodes = addedNodes.merge(nodeElements);
  mergedNodes
    .transition()
    .duration(500)
    .attr('transform', d => `translate(${d.x},${d.y})`);
    
  mergedNodes.select('circle')
    .attr("fill", d => d.children ? "#ffe0e0" : "#ffefef")
    .transition()
    .duration(1000)
    .attr('r', d => d.value)
    mergedNodes.select('text')
    .attr('dy', d => d.children ? d.value + 10 : 0)
    .transition()
    .delay(1000)
    .style('visibility', 'visible')
    
  const exitedNodes = nodeElements.exit()
  exitedNodes.select('circle')
    .transition()
    .duration(500)
    .attr('r', 1);
 exitedNodes.select('text')
   .remove();   
    
 exitedNodes   
    .transition()
    .duration(750)
    .remove();

    const linkPath = d => {
        const length = Math.hypot(d.from.x - d.to.x, d.from.y - d.to.y);
        const fd = d.from.value / length;
        const fx = d.from.x + (d.to.x - d.from.x) * fd;
        const fy = d.from.y + (d.to.y - d.from.y) * fd;
 
        const td = d.to.value / length;
        const tx = d.to.x + (d.from.x - d.to.x) * td;
        const ty = d.to.y + (d.from.y - d.to.y) * td;
    
        return `M ${fx},${fy} L ${tx},${ty}`; 
    };
  
  const links = data.links.map(link => {
        let from = nodes.find(n => n.data.name === link.from);
    if (!from) {
        const ancestors = findNodeAncestors(data, link.from);
      for (let index = 1; !from && index < ancestors.length  -1; index++) {
        from = nodes.find(n => n.data.name === ancestors[index].name)
      }
    }
        let to = nodes.find(n => n.data.name === link.to);
    if (!to) {
        const ancestors = findNodeAncestors(data, link.to);
      for (let index = 1; !to && index < ancestors.length  -1; index++) {
        to = nodes.find(n => n.data.name === ancestors[index].name)
      }
    }
    return {from, to};
  });
  
  const linkElements = container.selectAll('path.link')
    .data(links.filter(l => l.from && l.to));
  
  const addedLinks = linkElements.enter()
    .append('path')
    .classed('link', true)
    .attr('marker-end', 'url(#arrowhead-to)')
    .attr('marker-start', 'url(#arrowhead-from)');
    
    addedLinks.merge(linkElements)
        .style('visibility', 'hidden')
    .transition()
    .delay(750)
    .attr('d', linkPath)
        .style('visibility', 'visible')
    
  linkElements.exit().remove();  
}  

updateGraph(data);
text {
  font-family: "Ubuntu";
  font-size: 12px;
}

.link {
  stroke: blue;
  fill: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>

<svg width="600" height="600">
  <defs>
    <marker id="arrowhead-to" markerWidth="10" markerHeight="7" 
    refX="10" refY="3.5" orient="auto">
      <polygon fill="blue" points="0 0, 10 3.5, 0 7" />
    </marker>
    <marker id="arrowhead-from" markerWidth="10" markerHeight="7" 
    refX="0" refY="3.5" orient="auto">
      <polygon fill="blue" points="10 0, 0 3.5, 10 7" />
    </marker>
  </defs>
</svg>
Michael Rovinsky
  • 6,807
  • 7
  • 15
  • 30
  • 1
    Thanks for your kind help. Things are working fine but now one challenge is there if there is connection of inner node of A and inner node of B and both are getting collapsed then link is not being shifted to A and B. I will try to debug from my side and in the mean time if you can also try to help me more then It will be good. Thanks again. – ranjeet kumar May 24 '21 at 04:03
  • 1
    Solved it there was some problem in link filtering. Thanks again for ur kind of help. – ranjeet kumar May 24 '21 at 05:18
  • Just fixed the error, `from` should be replaced with `to`. See it works in the snippet – Michael Rovinsky May 24 '21 at 05:24
  • 1
    Nice example.... why are the A child nodes fully inside the A container, but the B child nodes overlap the B container ? – Robin Mackenzie May 24 '21 at 06:19
  • 1
    @RobinMackenzie I use the standard D3 circle packing. The only parameters to change are size, padding and sum of `d3.pack`. I consider to use circle pack in my project and probably will wright a packing algorithm of my own instead of using the D3's one. – Michael Rovinsky May 24 '21 at 07:13
  • Also can we fix one particular outer circle position ?? May be at top left. – ranjeet kumar May 24 '21 at 09:06
  • @ranjeetkumar Try change the multiplier in `.sum(d => d.value * 3.5)` to 4 or 5... You can also change `size` and `padding` of `d3.pack` – Michael Rovinsky May 24 '21 at 09:08
  • In this particular example suppose If I want to fix node C at top left , how we can do that ? Any particular way (other node should not collide with each other). – ranjeet kumar May 24 '21 at 09:39
  • @ranjeetkumar You can abandon `d3,pack` and just assign hard-coded `x` and `y` attributes to every node, but it's a lot of work – Michael Rovinsky May 24 '21 at 09:42
  • yes That is the challenge. Can we fix Only C and other node should not impacted by its position (I mean it should follow the current positioning logic) – ranjeet kumar May 24 '21 at 09:43
  • and also when I try to assign mouseover event and if try to hover over inner circle multiple events are being triggered. I tried using e.stopPropagation(); e.preventDefault(); but no luck till now. For outer circle it is working fine. – ranjeet kumar May 24 '21 at 09:45
  • @ranjeetkumar You're asking a lot of questions at once. Let's try to solve them one by one. 1: Mouseover problem 2. Fixed layout problem. You can just post new questions for both and we'll try to solve them together (I think it can take some time). Please mark my answer as correct, so we can end the current thread and move to a new one. – Michael Rovinsky May 24 '21 at 09:51
  • 1
    other question link :https://stackoverflow.com/questions/67670161/need-to-fix-position-of-one-outer-circle-in-circle-pack-layout-d3 – ranjeet kumar May 24 '21 at 10:08
  • 1
    one more question link : https://stackoverflow.com/questions/67671817/mouseover-event-is-getting-triggered-multiple-times-for-child-circle-in-circle-p – ranjeet kumar May 24 '21 at 12:07
  • If number of circles and inner circle are dynamic then How we can avoid collision. – ranjeet kumar May 25 '21 at 09:17