D3 圆形包布局鼠标悬停事件被多次触发

2024-04-16

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

我已分配mouseover and mouseout屏幕截图中的圆圈上有事件,但我无法弄清楚为什么mouseover内圈事件被多次触发(例如 A1、B1 等)?

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>

每个内圆都是一个<g> with a <circle> and <text>在其中。即使您将鼠标事件附加到<g>, the mouseover当您经过时事件会触发<text>元素位于圆圈中间,导致工具提示随着鼠标移动而移动。

你可以加.attr('pointer-events', 'none') to the <text>防止这种情况发生的要素:

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>
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

D3 圆形包布局鼠标悬停事件被多次触发 的相关文章

  • 让用户渲染自己的 SVG 文件的安全隐患

    我计划让网站用户上传他们自己的 SVG 文档并使用inkscape or svg2pdf 用户要么未经身份验证 要么经历一个简单的注册过程 所以我预计会有一些黑客尝试 我可以采取哪些过滤措施来最大程度地减少安全威胁 Inkscape 似乎并
  • 使用selenium IDE提取部分文本并将其放入变量中

    有人可以告诉我应该使用哪个命令来使用 Selenium Ide 从文本中仅提取数字 694575 并将其放入变量中以供进一步使用 这是带有文本的 div div class loginBoxTitle Edit Exhibition Cen
  • 如何理解 Angular JS 中的控制台错误消息?有什么工具吗?

    我是 Angular JS 的新手 我的第一个问题是如何理解 Angular JS 中控制台的错误消息 我编写了这段用于匹配密码的代码片段 它在控制台上抛出错误 但它工作正常 它是有线的 我无法从这些控制台消息中理解任何内容 谁能指出我为什
  • 响应式网格布局框架[关闭]

    就目前情况而言 这个问题不太适合我们的问答形式 我们希望答案得到事实 参考资料或专业知识的支持 但这个问题可能会引发辩论 争论 民意调查或扩展讨论 如果您觉得这个问题可以改进并可能重新开放 访问帮助中心 help reopen questi
  • 保存/导出Chrome的JavaScript控制台输入历史记录

    无论如何 我可以保存或导出 JavaScript 控制台的历史记录吗 input 控制台历史记录 在 Google Chrome 中 我不想保存输出或错误 因此将鼠标悬停在控制台框上 右键单击并选择Save as 不是解决方案 我不想每次都
  • v-file-input .click() 不是函数

    我试图以编程方式触发 v file input 的 click 事件 因为它在 Vuetify 的文档中 但它显示一个错误this refs imagePicker click is not a function我在这里错过了什么吗 代码重
  • KeyboardEvent.keyCode 已弃用。这在实践中意味着什么?

    根据 MDN 我们绝对应该not正在使用 keyCode财产 它已被弃用 https developer mozilla org en US docs Web API KeyboardEvent keyCode https develope
  • 如何从顺序键盘导航中删除 Vuetify 附加图标

    在带有 Vuetify 的 Vue js 应用程序中 我有一组用v text field并且其中有一个append icon为了切换文本可见性 如下所示
  • 如何滚动到div内的元素?

    我有一个滚动的div我想在点击它时发生一个事件 它会强制执行此操作div滚动以查看内部元素 我写的JavasCript是这样的 document getElementById chr scrollIntoView true 但这会在滚动时滚
  • 如何访问另一个 mobx 商店中的 mobx 商店?

    假设以下结构 stores RouterStore js UserStore js index js each of Store jsfiles 是一个 mobx 存储类 包含 observable and action index js只
  • 选中复选框时提交表单

    有没有办法在选中复选框时提交表单
  • React无限滚动scrollableTarget动态获取id?

    我在我的项目中使用react infinite scroll component 如何让scrollableTarget动态获取item id 我试过这样scrollableTarget item id 但它不起作用 必须与该 div 具有
  • Keycloak javascript 适配器 `keycloak.init` 加载 404 iframe

    我正在尝试使用 javascript 适配器将 Keycloak 集成到我的客户端应用程序keycloak js 但是 我似乎无法让它发挥作用 这是我的代码 const keycloak new Keycloak realm my real
  • Jquery,清除/清空 tbody 元素的所有内容?

    我认为这会相当简单 但似乎空方法无法清除我拥有的 tbody 如果有人知道执行此操作的正确方法 我将不胜感激 我只想删除 tbody 中包含的所有内容 到目前为止我正在尝试 tbodyid empty HTML table tbody tr
  • 加载另一个 JS 脚本后加载

    这是我的代码 very big js file lots of html stuff 问题是 这些是异步加载的 有没有办法等待第二个脚本直到第一个脚本加载 如果您使用 jQuery 有一个非常简单的方法可以通过获取脚本 https api
  • 如何获取使用 .map 渲染的第一个元素的 ref?

    我需要在几行中显示视频 卡片 的缩略图 并重点关注第一个缩略图 我使用嵌套地图进行了显示 该代码基本上迭代视频数组并返回多行视频 我们如何关注第一个渲染的元素 我认为我们需要获得第一个要聚焦的元素的引用 但是我们如何在这里设置 ref 并在
  • 您如何看待引导模式触发器的相应回调?

    On 引导模态 http getbootstrap com javascript modals 我们知道我们可以为触发器绑定事件 例如show or hide using show shown hide hidden 但此事件绑定仅适用于一
  • React Native - 跨屏幕传递数据

    我遇到了一些麻烦react native应用程序 我不知道如何跨屏幕传递数据 我意识到还有其他类似的问题在 SO 上得到了回答 但是这些解决方案对我来说不起作用 我正在使用StackNavigator 这是我的设置App js file e
  • 如何在 SVG 元素上使用箭头标记?

    我需要在 d3 js 中创建一个箭头 但我找到的只是带有节点图的示例 我需要的是简单地制作一个从 A 点到 B 点的箭头 我尝试实现以下示例中的部分代码 http bl ocks org 1153292 http bl ocks org 1
  • 我可以使用 jQuery 动态创建文件(及其内容)吗? [关闭]

    很难说出这里问的是什么 这个问题是含糊的 模糊的 不完整的 过于宽泛的或修辞性的 无法以目前的形式得到合理的回答 如需帮助澄清此问题以便重新打开 访问帮助中心 help reopen questions 这是我的 HTML 代码 ul li

随机推荐