0

I am working on this graph in d3. There is one problem, these circles are overlapping (A and F). As the data is dynamic, how can I make sure that no circles should overlap each other. Any leads would be appreciated.

enter image description here

Attaching the code below:

    const svgWidth = 1000;
    const svgHeight = 1000;

    const QUAD1 = 'quadrant1';
    const QUAD2 = 'quadrant2';
    const QUAD3 = 'quadrant3';
    const QUAD4 = 'quadrant4';
    let mouseEnterTimeout, mouseOutTimeout;
    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
        },
       {
          name: "F",
          children: [
            {name: "F1", value: 11}, {name: "F2", value: 7}, {name: "F3", value: 8},{name: "F4", value: 5}, {name: "F5", value: 3}, {name: "F6", value: 10}
          ]
        }
      ],
      links: [{from: "A5", to: "B3", colorCode: 'green'}, {from: "B3", to: "A5",  colorCode: 'blue'}, {from: "A3", to: "C",colorCode: 'blue'}, {from: "A2", to: "E", colorCode: 'blue'}, {from: "B1", to: "D", colorCode: 'red'}, {from: "B2", to: "B3", colorCode: 'blue'}, {from: "B1", to: "C", colorCode: 'red'}]
    };

    const cloneObj = item => {
      if (!item) { return item; } // null, undefined values check

      let types = [ Number, String, Boolean ],
        result;

      // normalizing primitives if someone did new String('aaa'), or new Number('444');
      types.forEach(function(type) {
        if (item instanceof type) {
          result = type( item );
        }
      });

      if (typeof result == "undefined") {
        if (Object.prototype.toString.call( item ) === "[object Array]") {
          result = [];
          item.forEach(function(child, index, array) {
            result[index] = cloneObj( child );
          });
        } else if (typeof item == "object") {
          // testing that this is DOM
          if (item.nodeType && typeof item.cloneNode == "function") {
            result = item.cloneNode( true );
          } else if (!item.prototype) { // check that this is a literal
            if (item instanceof Date) {
              result = new Date(item);
            } else {
              // it is an object literal
              result = {};
              for (let i in item) {
                result[i] = cloneObj( item[i] );
              }
            }
          } else {
            // depending what you would like here,
            // just keep the reference, or create new object
            if (false && item.constructor) {
              // would not advice to do that, reason? Read below
              result = new item.constructor();
            } else {
              result = item;
            }
          }
        } else {
          result = item;
        }
      }

      return result;
    }
    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;
    }

    let markerCount = 0;
    const marker  = (color, val)  => {
        val = val+(markerCount++);
        svg.append("svg:defs").selectAll("marker")
             .data([val])
             .enter().append("svg:marker")    
             .attr("id", String)
             .attr("viewBox", "0 -5 10 10")
             .attr("refX", 60)
             .attr("refY", 0)
             .attr("markerWidth", 6)
             .attr("markerHeight", 6)
             .attr("orient", "auto")
             .style("fill", color)
             .append("svg:path")
             .attr("d", "M0,-5L10,0L0,5");
        return "url(#" +val+ ")";
    }


    //Node click code

    const nodeColor = '#ffefef';

    const isNeighborLink = (node, link) => {
      return link.from.data.name === node.name || link.to.data.name === node.name
    }

    const getNodeColor = (node, neighbors,  count, isSelectEvent, prevOpacity) => {
      if(isSelectEvent) {
        if (neighbors.indexOf(node.name) < 0 && !node.selected && selectedNodeCount) {
          return 0.2;
        } 
      return 1;
      } else {
        if(selectedNodeCount) {
          if (neighbors.indexOf(node.name) > -1 && !node.selected) {
            return selectedNodeCount ? 0.2 : 1;
          }
          return prevOpacity;
        } else {
          return 1;
        }
      }
    }

    const getTextColor = (node, neighbors) => {
       if (neighbors.indexOf(node.name) < 0 ) {
        return 'gray';
      }
      return;
    }

    const getLinkColor = (node, link) => {
      return isNeighborLink(node, link) ? 'green' : '#E5E5E5';
    }

    const getNeighbors = (node) => {
        return data.links.reduce((neighbors, link) => {
          if (link.to === node.name) {
            neighbors.push(link.from)
          } else if (link.from === node.name) {
            neighbors.push(link.to)
          }
          return neighbors
        }, [node.name])
      }
      

    const svg = d3.select("svg");
    // This is for tooltip


    const findPopoverQuad = (popoverNode, x, y, offset) => {
      const viewPortH = window.innerHeight;
      const viewPortW = window.innerWidth;
      const nodeDimensions = popoverNode.getBoundingClientRect();
      if (x >= viewPortW/2 && y >= viewPortH/2) {
        return {
              x: -(nodeDimensions.width-offset),
              y: -(nodeDimensions.height-offset)
            };
      } else if (x >= viewPortW/2 && y < viewPortH/2) {    
        return {
          x: -(nodeDimensions.width-offset),
          y: offset
        };
      } else if( x < viewPortW/2 && y >= viewPortH/2) {
        return {
          x: offset,
          y: -(nodeDimensions.height-offset)
        };
      } else {
        return {
          x: offset,
          y: offset
        }
      }
    }

  const closePopOver = () => {
    const popoverNode = d3.select('#popover').node();
    popoverNode.innerHTML = "";
    d3.select('#popover')
      .style('visibility', 'hidden') 
  }
  
  const onMouseEnter = (e,d )=> {
    console.log('circle mouse enter')
    e.stopPropagation();
    if (mouseEnterTimeout) {
       clearTimeout(mouseEnterTimeout)
     }
     mouseEnterTimeout = setTimeout(function() {
       
     const popoverNode = d3.select('#popover').node();
     const node = findNode(data, d.data.name);

     const children = (node.children || node._children) || [];
                children.forEach((c) => {
       const p = document.createElement("p")
       p.innerHTML = `${c.name} : ${c.value}`;
         popoverNode.append(p)
     });  
     
     if(children.length === 0 ) {
       popoverNode.append('No children')
     }
     
     let offset = 0;           
     const transformValues = findPopoverQuad(popoverNode, e.x, e.y, offset);
     d3.select('#popover')
      .style('visibility', 'visible')
      .on('mouseenter', function(e)  {
          console.log('popover mouse enter')
          if(mouseOutTimeout) {
            clearTimeout(mouseOutTimeout);
          }
      })
      .on('mouseleave', (e) => {
         closePopOver()  
      })
      .transition()
      .duration(500)
      .style('transform', `
        translate(${e.x+transformValues.x}px, 
          ${e.y+transformValues.y}px)
      `)
     }, 100)
  }

  const onMouseLeave = (e,d ) => {
    console.log('circle mouse leave')
    if(mouseOutTimeout) {
      clearTimeout(mouseOutTimeout);
    }
    mouseOutTimeout = setTimeout(function() {
    const popoverNode = d3.select('#popover').node();
    popoverNode.innerHTML = "";
    d3.select('#popover')
      .style('visibility', 'hidden')   
    }, 100) 
  }

  const onSelectNode = (node, ref) => {
   if(node) {
    let isPerviousSelectedNode = node['selected'];
    node['selected'] = !isPerviousSelectedNode;
    // node['selected'] = node['selected'] ?  false:  true;
    isPerviousSelectedNode ? selectedNodeCount-- : selectedNodeCount++;
    if(selectedNodeCount < 0 ) {
      selectedNodeCount = 0;
    } 
    d3.select(ref.firstChild).style('fill',  node.selected ? '#fb7777': nodeColor);
    const neighbors = getNeighbors(node);
    circleElements
        .transition()
        .duration(200)
        .style('opacity', function(node) {
          let prevOpacity = d3.select(this).style('opacity'); 
          return getNodeColor(node.data, neighbors, selectedNodeCount, !isPerviousSelectedNode, prevOpacity);
        })          

    linkElements
      .transition()
      .duration(200)
      .style('stroke', link => getLinkColor(node, link))
      .attr("marker-end", link => marker(getLinkColor(node, link), link.from.data.name + link.to.data.name+ '_click'))

    textElements
    .transition()
    .duration(200)
      .style('fill', node => getTextColor(node, neighbors))
    }
  }

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

    let circleElements, textElements, linkElements;
    let selectedNodeCount = 0;
      
    const onClickNode = (e, d, ref) => {
      e.stopPropagation();
      e.preventDefault();
      
      const node = findNode(data, d.data.name);
      if( d.depth == 2 || (node && !(node.children || node._children))) {
        onSelectNode(node, ref);
      }
      if(node.children && !node._children) {
        /*node._children = node.children;*/
        node._children = cloneObj(node.children);
        node.children = undefined;
        node.value = 20;
        updateGraph(data);
      } else {
        if (node._children && !node.children) {
            //node.children = node._children;
            node.children = cloneObj(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', function (e, d) {onClickNode(e, d, this)})
        .on('mouseenter',(e, d) => onMouseEnter(e, d))
        .on('mouseleave', (e, d) => onMouseLeave(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})`);
        
        circleElements = mergedNodes.select('circle')
        .attr("fill", d => d.children ? "#ffe0e0" : "#ffefef")
        .attr('r', d => d.value);

        textElements = mergedNodes.select('text')
        .attr('dy', d => d.children ? d.value + 10 : 0)
        .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 => {
            let length = Math.hypot(d.from.x - d.to.x, d.from.y - d.to.y);
            if(length == 0 ) {
              return ''; // This means its a connection inside collapsed node
            }
            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)
          }
        }
        let colorCode = link.colorCode;
        return {from, to, colorCode};
      });


      const linkElem = container.selectAll('path.link')
        .data(links.filter(l => l.from && l.to));
      
      linkElements = linkElem.enter()
        .append('path')
        .classed('link', true)
        .attr("stroke", function(d){
          return d.colorCode;
        })
        .attr("marker-end", function(d){ 
            return marker(d.colorCode, d.from.data.name+d.to.data.name);
        })

      linkElements.merge(linkElem)
            .style('visibility', 'hidden')
        .transition()
        .delay(750)
        .attr('d', linkPath)
            .style('visibility', 'visible')
      }  

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

          .link {
            fill: none;
          }

          .popover {
            position: absolute;
            top: 0; left: 0;
            visibility: hidden;
            width :150px;
            max-height:300px;
            background: white;
            border: 1px solid blue;
            padding: 10px;
          }
<!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">
     
      <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
  </head>
  <body>
      <svg width="1000" height="1000">
        <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>

      <div id='popover' class='popover'>
      </div>
  </body>
</html>
CJ Yetman
  • 8,373
  • 2
  • 24
  • 56
monica
  • 1,454
  • 14
  • 28
  • 1
    You set `svgWidth = 1000` and also `svgHeight = 1000` but `.pack.size([600, 600])`. Have you considered changing that configuration ? – Robin Mackenzie May 25 '21 at 12:08
  • 1
    You can just set all the coordinates hard-coded in JSON instead of computing them by `d3.pack` – Michael Rovinsky May 25 '21 at 12:31
  • You can use `d3-force` and simulate a few ticks before drawing the result. d3-force is used to avoid any overlapping objects – Ruben Helsloot May 25 '21 at 13:26
  • @MichaelRovinsky can't hard-code any coordinates as data will be too dynamic. – monica May 25 '21 at 13:49
  • @RubenHelsloot Can you please apply in that example and show? – monica May 25 '21 at 13:50
  • @RobinMackenzie Change to what? There is a possibility of any number of circles and any number of circles inside. How can I make sure they don't collide? – monica May 25 '21 at 13:51
  • @monica D3 circle pack has very limited functionality ... Maybe it's a better idea to write a simple layout algorithm of your own – Michael Rovinsky May 25 '21 at 13:58
  • @RubenHelsloot I don't think the force layout can help here – Michael Rovinsky May 25 '21 at 13:59
  • 1
    You are using `d.value` instead of `d.r` to plot the circles' radii. You cannot use d.value to plot radius because areas (or radii) cannot be proportional to d.value across different generations of parent/children or between cousins except in extremely limited circumstances in a proper circle pack, see [here](https://stackoverflow.com/q/50894731/7106086) – Andrew Reid May 25 '21 at 21:34

0 Answers0