2

I want to connect node inside one big circle to node inside another big circle or sometimes to another bigger circle itself. Is there a way to achieve the same ? I am able to connect nodes inside the same circle.

Below is the sample code that I have tried with :

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style type="text/css">
        .node {}

        .link { stroke: #999; stroke-opacity: .6; stroke-width: 1px; }
    </style>
    <script src="https://d3js.org/d3.v4.min.js" type="text/javascript"></script>
    <script src="https://d3js.org/d3-selection-multi.v1.js"></script>
</head>
<svg width="960" height="600"></svg>

<script type="text/javascript">
var data = {
  "nodes": [
    {
      "id": "Myriel", 
      "group": 1, 
      "value": 3, // basically in this ratio the circle radius will be
      "childNode" : [{
        "id": "child1",
        "value": 2
      },{
        "id": "child2",
        "value": 3
      },{
        "id": "child3",
        "value": 1
      }],
      "links": [{
        "source": "child1",
        "target": "child2",
        "isByDirectional": true    
      },{
        "source": "child1",
        "target": "child3",
        "isByDirectional": false    
      }
      ]
    },
    {
      "id": "Napoleon",
      "group": 1,
      "value": 2, // basically in this ratio the circle radius will be
      "childNode" : [{
        "id": "child4",
        "value": 2
      },{
        "id": "child5",
        "value": 3
      }],
      "links": null
    },
    {
      "id": "Mlle.Baptistine",
      "group": 1,
      "value": 1, // basically in this ratio the circle radius will be
    },
    {
      "id": "Mme.Magloire",
      "group": 1,
      "value" : 1,
    },
    {
      "id": "CountessdeLo",
      "group": 1,
      "value" : 2,
    },
    {
      "id": "Geborand",
      "group": 1,
      "value" : 3,
    }
  ],
  "links": [
    {"source": "Napoleon", "target": "Myriel", "value": 1},
    {"source": "Mlle.Baptistine", "target": "Napoleon", "value": 8},
    {"source": "CountessdeLo", "target": "Myriel", "value": 1},
    {"source": "Geborand", "target": "CountessdeLo", "value": 1}
  ]
}



var nodeRadiusScale = d3.scaleSqrt().domain([0, 50]).range([10, 50]);

var color = function() {
  var scale = d3.scaleOrdinal(d3.schemeCategory10);
  return d => scale(d.group);
}

var drag = simulation => {
  
  function dragstarted(d) {
    if (!d3.event.active) simulation.alphaTarget(0.3).restart();
    d.fx = d.x;
    d.fy = d.y;
  }
  
  function dragged(d) {
    d.fx = d3.event.x;
    d.fy = d3.event.y;
  }
  
  function dragended(d) {
    if (!d3.event.active) simulation.alphaTarget(0);
    d.fx = null;
    d.fy = null;
  }
  
  return d3.drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended);
}

function drawChildNodes(nodeElement, parentIds, options) {
  if(!parentIds.childNodes) {
    return
  }


  const nodeColor = options.nodeColor
  const borderColor = options.borderColor
  const nodeTextColor = options.nodeTextColor
  const width = options.width
  const height = options.height
  const data = getData(parentIds, width * 2, height * 2);
  const nodeData = nodeElement.selectAll("g").data(data)

  var childNodeRadius = 5;

  const nodesEnter = nodeData
    .enter()
    .append("g")
    .attr("id", (d, i) => {
      return "node-group-" + d.data.id
    })
    .attr('class', 'child-node')
    .attr("transform", (d) => `translate(${d.x - width},${d.y - height})`)
    .attr('cx', (d) => d.x)
    .attr('cy', (d) => d.y)
  
  nodesEnter
    .filter((d) => d.height === 0)
    .append("circle")
    .attr("class", "node pie")
    .attr("r", (d) => childNodeRadius)
    .attr("stroke", borderColor)
    .attr("stroke-width", 1)
    .attr("fill", "white")

  /*nodesEnter
    .filter((d) => d.height === 0)
    .append("text")
    .style("fill", "black")
    .attr("font-size", "0.8em")
    .attr("text-anchor", "middle")
    .attr("alignment-baseline", "middle")
    .attr("dy", -7)
    .text(d=>d.data.id)*/
    if(!parentIds.childLink) {
      return;
    }
  
  const linkData = nodeElement.selectAll("line").data(parentIds.childLink);

  const linksEnter = linkData
    .enter()
    .append("line")
    .attr("class", "node line")
    .attr('id', (d) => d.source + '->' + d.target)
    .attr("x1", (d,i) => data.find(el => el.data.id === d.source).x - width)
    .attr("y1", (d,i) => data.find(el=>el.data.id === d.source).y - height)
    .attr("x2", (d,i) => data.find(el=>el.data.id === d.target).x - width)
    .attr("y2", (d,i) => data.find(el=>el.data.id === d.target).y - height)
    .attr("stroke", 'red')
    .attr("stroke-width", 1)
    .attr("fill", "none")

}

function getData(parentIDs, width, height) {
  var rawData = []
  rawData.push({ id: "root" })
  rawData.push({
    id: parentIDs.key,
    size: parentIDs.values,
    parentId: "root"
  })

  parentIDs.childNodes.forEach((el) => {
    rawData.push({
      id: el.id,
      parentId: parentIDs.key,
      size: el.value
    })
  })
  
  const vData = d3.stratify()(rawData)
  const vLayout = d3.pack().size([width, height]).padding(10)
  const vRoot = d3.hierarchy(vData).sum(function (d) {
    return d.data.size
  })
  const vNodes = vLayout(vRoot)
  const data = vNodes.descendants().slice(1)

  return data
}
var svg = d3.select("svg"),
        width = +svg.attr("width"),
        height = +svg.attr("height");

var links = data.links.map(d => Object.create(d));
var nodes = data.nodes.map(d => Object.create(d));

var simulation = d3.forceSimulation(nodes)
                    .force("link", d3.forceLink(links).id(d => d.id).distance(200))
                    .force("charge", d3.forceManyBody().strength(0,15))
                    .force("collide", d3.forceCollide(function (d) { 
                        return 100;
                        //return nodeRadiusScale(d.value) 
                    }))
                    .force("center", d3.forceCenter(width / 2, height / 2));

var link = svg.append("g")
            .attr("stroke", "#999")
            .attr("stroke-opacity", 0.6)
            .selectAll("line")
            .data(links)
            .enter()
            .append('line')
            .attr("stroke-width", d => Math.sqrt(d.value));

function zoom(focus) {
    const transition = svg.transition()
    .duration(750)
    .attr("transform", function(){
      clicked = !clicked
      if(clicked){
        return `translate(${-(focus.x-width/2)*k},${-(focus.y-height/2)*k})scale(${k})`
      } else {
        return `translate(${0},${0})})scale(1)`
      }      
    });
}

var nodeG = svg.append("g")
    .selectAll("g")
      .data(nodes)
      .enter()
      .append('g')
      .call(drag(simulation))
      .on("click", d => (zoom(d), d3.event.stopPropagation()));


nodeG.append('circle')
    .attr("r", d => nodeRadiusScale(d.value * 2))
    .attr("fill", color);

nodeG.append('text')
    .style("fill", "black")
    .attr("font-size", "0.8em")
    .attr("text-anchor", "middle")
    .attr("alignment-baseline", "middle")
    .attr("dy", d => -nodeRadiusScale(d.value * 2)- 10)
    .text(d=>d.id);

nodeG.append('g')
    .each(function (d) {
    drawChildNodes(
      d3.select(this),
      { key: d.id, values: d.value, childNodes: d.childNode, childLink: d.links },
      {
        width: nodeRadiusScale(d.value),
        height: nodeRadiusScale(d.value),
        nodeColor: 'white',
        borderColor: 'black',
        nodeTextColor: 'black',
      }
    )
  });

simulation.on("tick", () => {
    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);

    nodeG.attr("transform", d => `translate(${d.x}, ${d.y})`)
  });

</script>
<body>

I want to achieve something here in the image:

End goal is to achieve something shown in below image

Also can we have added functionality like collapsing and expanding the circle if the outer circle is having children (may be if any link is there between child and outer nodes then probably we can shift the line to parent and remove the links between children of the collapsed circle, if possible don't want to change the circle position of collapsed/expanded circle.)

ranjeet kumar
  • 251
  • 2
  • 10

1 Answers1

2

Here is an example with collapsing/expandind nodes. The sizes and margins should be adjusted according to your requirements. Suggest to see the snippet in a full-page mode:

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>
Michael Rovinsky
  • 6,807
  • 7
  • 15
  • 30
  • I want to move link to parent if child is being hidden because of parent. If we can do this also it will be a good lead for me. – ranjeet kumar May 21 '21 at 07:29
  • I understand... It's quite a problem because links are separated and not contained in their nodes, so they need to be recalculated in a difficult way each time we expand/collapse a node. I did not see a single exaple of a circle packing with links but I can try, maybe tomorrow. Can you post another question with this requirement? – Michael Rovinsky May 21 '21 at 07:35
  • I have written the same in the description of the question. if we can achieve the same and could provide the solution here itself then it will be good otherwise I will create another question. Thanks again for helping me. Will wait for the updated answer. – ranjeet kumar May 21 '21 at 07:41
  • You can copy my snippet to your next question and maybe someone else will provide you a more comprehensive solution. Good luck :) – Michael Rovinsky May 21 '21 at 07:53
  • 1
    Anyway I have created another question here https://stackoverflow.com/questions/67633143/need-to-shift-link-of-child-node-to-parent-once-parent-collapsed-in-d3-graph. plz help me if u have any solution for the same. – ranjeet kumar May 21 '21 at 08:19
  • Excellent. Please mark the current answer as correct and upvote. I will do my best to solve the new one – Michael Rovinsky May 21 '21 at 08:42