选择性连线与悬停标签.onNodeHover、.onNodeClick在 Vasturiano的 3d-force-graph中

前端开发 2026-07-09

我在使用Vasco Asturiano的惊人作品 3d-force-graph,在为下面的问题设计一个合适的开关命令(或if/else语句)时遇到了困难:

在下面的最小可复现示例(MRE)中,你会看到.onNodeHover会对每个节点输出文本,即使文本没有意义(我只是想要对人名显示悬停文本,而不是标签或学科)。我希望鼠标悬停文本仅在.json文件中存在链接时才出现。

https://jsfiddle.net/oan6zwcm/

我接着想保持相机重新定位 .onNodeClick,这简直酷到爆,但想用右键单击来打开链接。可以做到吗?目前我只能选择一个函数,或另一个:

.onNodeClick(node => window.open(node.link))
.onNodeClick(node => {
  // Aim at node from outside it
  const distance = 80;
  const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z);
  const newPos = node.x || node.y || node.z
     ? {x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio}
     : {x: 0, y: 0, z: distance}; // special case if node is in (0,0,0)

Graph.cameraPosition(
     newPos, // new position
     node, // lookAt ({ x, y, z })
     3000  // ms transition duration
  );
   })

右键点击函数在别处定义,但我还没想清楚如何创建正确的条件,将右键点击与 window.open(node.link)) 连接起来。

function doSomething(e) {
    var rightclick;
    if (!e) var e = window.event;
    if (e.which) rightclick = (e.which == 3);
    else if (e.button) rightclick = (e.button == 2);
    alert('Rightclick: ' + rightclick); // true or false
}

我的当前构建在这里: https://github.com/greg-gan/JTC_Oak-visualization/blob/main/index.html

以及可视化:https://greg-gan.github.io/JTC_Oak-visualization/public/

我最近在这里发了我的第一个问题,它帮了我非常多!@kikon给出的if/else语句很可能就是破解难题的钥匙。无论如何,非常感谢你的帮助。

解决方案

我理解你的意思是在悬停时你只想要带有链接的节点,在右键时你想从某个特定节点打开链接……希望理解正确。

试试这个思路

.nodeLabel(node => {
  if (node.link) {
    return `${node.label}<br>${node.link}`;
  }
  return '';
})

逐个检查每个节点是否存在链接:若存在,就返回tooltip;若不存在,就返回一个空字符串。

至于第二个问题,为什么不使用.onNodeRightClick?

https://github.com/vasturiano/3d-force-graph#interaction

.onNodeRightClick(node => {
    if (node.link) {
    window.open(node.link, '_blank', 'noopener,noreferrer');
    }
})
<div id="3d-graph"></div>

<script type="importmap">
  {
    "imports": {
      "three": "https://esm.sh/[email protected]",
      "three-spritetext": "https://esm.sh/[email protected]",
      "3d-force-graph": "https://esm.sh/[email protected]"
    }
  }
</script>
<script type="module">
import SpriteText from "three-spritetext";
import * as THREE from "three";
import ForceGraph3D from "3d-force-graph";

function main(data) {
  const elem = document.getElementById("3d-graph");

  const Graph = new ForceGraph3D(elem)
    .graphData(data)
  //v
    .nodeLabel(node => {
      if (node.link) {
        return `${node.label}<br>${node.link}`;
      }
      return '';
    })
  //^hover
    .onNodeClick(node => {
      const distance = 40;
      const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z);
      const newPos = node.x || node.y || node.z
        ? { x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }
        : { x: 0, y: 0, z: distance };

      Graph.cameraPosition(newPos, node, 3000);
    })
    //v
    .onNodeRightClick(node => {
      if (node.link) {
        window.open(node.link, '_blank', 'noopener,noreferrer');
      }
    })
  //^rc
    .nodeThreeObjectExtend(true)
    .nodeThreeObject(node => {
      const textSprite = new SpriteText(node.label ?? node.id);
      textSprite.material.depthWrite = false;
      textSprite.color = node.color;
      textSprite.center.y = -1.2;

      if (node.type === "keywords") {
        const imgTexture = new THREE.TextureLoader().load(
          "https://raw.githubusercontent.com/greg-gan/JTC_Oak_visualization_trial/refs/heads/main/imgs/hashtag.png"
        );
        imgTexture.colorSpace = THREE.SRGBColorSpace;
        const material = new THREE.SpriteMaterial({ map: imgTexture });
        const sprite = new THREE.Sprite(material);
        sprite.scale.set(8, 8);
        textSprite.textHeight = 4 / 8;
        sprite.add(textSprite);
        node.color = "rgba(0,0,0,0)";
        return sprite;
      }

      if (node.type === "discipline") {
        const imgTexture = new THREE.TextureLoader().load(
          "https://raw.githubusercontent.com/greg-gan/JTC_Oak_visualization_trial/refs/heads/main/imgs/discipline.png"
        );
        imgTexture.colorSpace = THREE.SRGBColorSpace;
        const material = new THREE.SpriteMaterial({ map: imgTexture });
        const sprite = new THREE.Sprite(material);
        sprite.scale.set(14, 14);
        textSprite.textHeight = 4 / 14;
        sprite.add(textSprite);
        node.color = "rgba(0,0,0,0)";
        return sprite;
      }

      textSprite.textHeight = 4;
      return textSprite;
    });

  Graph.d3Force("charge").strength(-120);
}

const data = {
  nodes: [
    {
      label: "Interdisciplinary",
      x: 1072,
      y: -498,
      id: "Interdisciplinary research",
      type: "keywords",
      color: "rgb(0,189,148)",
      size: 33
    },
    {
      label: "Prof. Dr. J. H.",
      link: "https://www.archive.org",
      x: -1448,
      y: -4560,
      id: "Prof. Dr. J. H.",
      type: "scholar",
      color: "rgb(248,189,0)",
      size: 30
    },
    {
      label: "Benjamin St.",
      link: "https://www.wikipedia.org",
      x: -2228,
      y: 13,
      id: "Benjamin St.",
      color: "rgb(190,129,248)",
      size: 30.0
    },
    {
      label: "#journalism",
      x: -15396,
      y: 16305,
      id: "#journalism",
      type: "discipline",
      color: "rgb(192,192,192)",
      size: 31
    }
  ],
  links: [
    {
      source: "Prof. Dr. J. H.",
      target: "Interdisciplinary research",
      id: "988",
      color: "rgb(0,189,148)",
      size: 1.0
    },
    {
      source: "Prof. Dr. J. H.",
      target: "#journalism",
      id: "555",
      color: "rgb(192,192,192)",
      size: 1.0
    },
    {
      source: "Benjamin St.",
      target: "#journalism",
      id: "1555",
      color: "rgb(92,192,192)",
      size: 1.0
    }
  ]
};

main(data);
</script>
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章