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

Michael Rovinsky
  • 6,807
  • 7
  • 15
  • 30
ranjeet kumar
  • 251
  • 2
  • 10
  • Can you post your data? – Michael Rovinsky May 20 '21 at 07:24
  • Its hardcoded in the sample codebase. I am not getting a way to defined the outer connection so I am not able to define in node object definition. If there is a simpler way to add it then I am ok with that as well. @MichaelRovinsky – ranjeet kumar May 20 '21 at 09:33
  • In this example something like connecting child1 with child4. bot of these are not part of the same parent (circle). I need to find a way to define the relationship also which d3 supports. – ranjeet kumar May 20 '21 at 09:57
  • If I get it right, you have N groups of nodes, where each group contains nodes of the same color and the nodes are packed as a circle. Each node has a certain size and may be linked to one or more other nodes in any group. Is it correct? – Michael Rovinsky May 20 '21 at 10:20
  • yes. that is my use case. – ranjeet kumar May 20 '21 at 10:22
  • @MichaelRovinsky I have added one more screen for the reference. – ranjeet kumar May 20 '21 at 11:15
  • All 3 screenshots are completely different... I thought the second is correct (with blue, orange and green circles). Is it? – Michael Rovinsky May 20 '21 at 11:19
  • The first one with bigger circle having smaller circles and with the red background. @MichaelRovinsky – ranjeet kumar May 20 '21 at 12:10
  • I suggest to remove all the irrelevant screenshots and rewrite your question clearly. I think your code sample is irrelevant here, just keep the screenshot and try to describe the functionality you want to achieve (and your data if you have any). – Michael Rovinsky May 20 '21 at 12:38

1 Answers1

2

Here is a snippet using D3 circle packing (V6):

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 svg = d3.select("svg");

const pack = data => d3.pack()
    .size([400, 400])
    .padding(20)
  (d3.hierarchy(data)
    .sum(d => d.value * 2.5)
    .sort((a, b) => b.value - a.value));

const root = pack(data);    
    
const nodes = root.descendants().slice(1);    
console.log(nodes);

const container = svg.append('g')
    .attr('transform', 'translate(0,-50)')

const nodeElements = container
    .selectAll("circle")
    .data(nodes);
    
nodeElements.enter()
    .append("circle")
    .attr('cx', d => d.x)
    .attr('cy', d => d.y)
    .attr('r', d => d.value)
    .attr("fill", d => d.children ? "#ffe0e0" : "#ffefef")
    .attr('stroke', 'black')

const labelElements = container
    .selectAll("text")
    .data(nodes);
    
labelElements.enter()
    .append("text")
    .text(d => d.data.name)
    .attr('x', d => d.x)
    .attr('y', d => d.children ? d.y + d.value + 10 : d.y)
    .attr('text-anchor', 'middle')
    .attr('alignment-baseline', 'middle')
    .style('fill', 'black')

const linkElements = container.selectAll('path.link')
    .data(data.links);
  
const linkPath = d => {
 const from = nodes.find(n => n.data.name === d.from);
 const to = nodes.find(n => n.data.name === d.to);
 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}`; 
};  
  
linkElements.enter()
    .append('path')
  .classed('link', true)
  .attr('d', linkPath)
  .attr('marker-start', 'url(#arrowhead-from)')
  .attr('marker-end', 'url(#arrowhead-to)');
  
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="400" height="400">
  <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
  • 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). – ranjeet kumar May 21 '21 at 05:34
  • @ranjeetkumar I think it's doable, can you please post another question? I also will appreciate very much if you mark my answer as correct. – Michael Rovinsky May 21 '21 at 06:04
  • I have posted another question here https://stackoverflow.com/questions/67631648/need-to-connect-two-nodes-of-different-circle-packed-layout-in-d3-and-pack-layou. can u please help on this. – ranjeet kumar May 21 '21 at 06:14