【问题标题】:D3 circle pack layout mouseover event is getting triggered multiple timesD3圆形包布局鼠标悬停事件被多次触发
【发布时间】:2021-05-24 12:06:40
【问题描述】:

我有这个使用 D3 的圆形包布局:

我在屏幕截图中的圆圈上分配了mouseovermouseout 事件,但我无法弄清楚为什么内圈(例如A1、B1 等)会多次触发mouseover 事件。 .) ?

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>

【问题讨论】:

    标签: javascript svg d3.js


    【解决方案1】:

    每个内圈都是一个&lt;g&gt;,其中包含一个&lt;circle&gt;&lt;text&gt;。即使您将鼠标事件附加到 &lt;g&gt;,当您越过圆圈中间的 &lt;text&gt; 元素时,mouseover 事件也会触发,从而导致工具提示随着鼠标四处移动。

    您可以将.attr('pointer-events', 'none') 添加到&lt;text&gt; 元素以防止这种情况发生:

    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');
    

    以下示例:

    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>

    【讨论】:

      猜你喜欢
      • 2013-09-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-01-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多