ranjeet kumar
ranjeet kumar

Reputation: 271

D3 circle pack layout mouseover event is getting triggered multiple times

I have this circle pack layout using D3:

actual requirement

I have assigned mouseover and mouseout event on the circles in the screenshot, but am not able to figure out why mouseover event is being triggered multiple times for inner circle (for example A1, B1, etc..) ?

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: "A5", to: "B3"}, {from: "A3", to: "C"}, {from: "A2", to: "E"}, {from: "B1", to: "D"}, {from: "B2", to: "B3"}, {from: "B1", to: "C"}]
    };

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

    const svg = d3.select("svg");
    // This is for tooltip
    const Tooltip = d3.select("body").append("div")
                      .attr("class", "tooltip-menu")
                      .style("opacity", 0);

  const onMouseover = (e,d )=> {
    console.log('d -->>', d);
    e.stopPropagation();
    Tooltip.style("opacity", 1);
    let html = `<span>
                  Hi
                </span>`;

    Tooltip.html(html)
            .style("left", (e.pageX + 10) + "px")
            .style("top", (e.pageY - 15) + "px");
  }

  const onMouseout = (e,d ) => {
    Tooltip.style("opacity", 0)
  }

    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 = 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', (e, d) => onClickNode(e, d))
        .on('mouseover',(e, d) => onMouseover(e, d))
        .on('mouseout', (e, d) => onMouseout(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 => {
            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)
          }
        }
        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;
          }

          div.tooltip-menu {
             position: absolute;
             text-align: center;
             padding: .5rem;
             background: #FFFFFF;
             color: #313639;
             border: 1px solid #313639;
             border-radius: 8px;
             pointer-events: none;
            font-size: 1.3rem;
          }
<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>

Upvotes: 3

Views: 456

Answers (1)

Robin Mackenzie
Robin Mackenzie

Reputation: 19319

Each inner circle is a <g> with a <circle> and <text> within it. Even though you attach the mouse events to the <g>, the mouseover event fires as you pass over the <text> element in the middle of the circle, causing the tooltip to move as your mouse around.

You can add .attr('pointer-events', 'none') to the <text> elements to prevent this:

addedNodes.append("text")
  .text(d => d.data.name)
  .attr('text-anchor', 'middle')
  .attr('alignment-baseline', 'middle')
  .attr('pointer-events', 'none') // <---- HERE
  .style('visibility', 'hidden')
  .style('fill', 'black');

Example below:

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: "A5", to: "B3"}, {from: "A3", to: "C"}, {from: "A2", to: "E"}, {from: "B1", to: "D"}, {from: "B2", to: "B3"}, {from: "B1", to: "C"}]
    };

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

    const svg = d3.select("svg");
    // This is for tooltip
    const Tooltip = d3.select("body").append("div")
                      .attr("class", "tooltip-menu")
                      .style("opacity", 0);

  const onMouseover = (e,d )=> {
    console.log('d -->>', d);
    e.stopPropagation();
    Tooltip.style("opacity", 1);
    let html = `<span>
                  Hi ${d.data.name}
                </span>`;

    Tooltip.html(html)
            .style("left", (e.pageX + 10) + "px")
            .style("top", (e.pageY - 15) + "px");
  }

  const onMouseout = (e,d ) => {
    Tooltip.style("opacity", 0)
  }

    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 = 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', (e, d) => onClickNode(e, d))
        .on('mouseover',(e, d) => onMouseover(e, d))
        .on('mouseout', (e, d) => onMouseout(e, d));
        
      addedNodes.append('circle')
        .attr('stroke', 'black')
      
      addedNodes.append("text")
        .text(d => d.data.name)
        .attr('text-anchor', 'middle')
        .attr('alignment-baseline', 'middle')
        .attr('pointer-events', 'none')
        .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 => {
            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)
          }
        }
        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;
          }

          div.tooltip-menu {
             position: absolute;
             text-align: center;
             padding: .5rem;
             background: #FFFFFF;
             color: #313639;
             border: 1px solid #313639;
             border-radius: 8px;
             pointer-events: none;
            font-size: 1.3rem;
          }
<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>

Upvotes: 2

Related Questions