我正在try 动态地将 node 添加到D3力有向图中.我相信我接近了,但我不理解一些与D3中的 Select 如何工作相关的关键概念.

如果我直接调用我的addData函数,并一次添加所有 node ,一切都按预期工作.

但是,如果我使用addDataInChunks,则会添加前两个 node 和第一个链接,但在这之后,图形中不会出现其他 node .此外,一旦将第二个块添加到图形中,就会失go 拖动 node 的能力.

我做错了什么?我的代码需要如何更改才能正常工作?

    const nodeData = [
      {
        id: 'node 1',
        added: false,
      },
      {
        id: 'node 2',
        added: false,
      },
      {
        id: 'node 3',
        added: false,
      },
      {
        id: 'node 4',
        added: false,
      },
    ]

    const linkData = [
      {
        linkID: 'link 1',
        added: false,
        source: 'node 1',
        target: 'node 2',
      },
      {
        linkID: 'link 2',
        added: false,
        source: 'node 1',
        target: 'node 3',
      },
      {
        linkID: 'link 3',
        added: false,
        source: 'node 3',
        target: 'node 4',
      },
    ]

    //
    //
    //

    let svg = null
    let node = null
    let link = null
    let simulation = null

    const width = 750
    const height = 400
    const nodeCount = 10
    const svgNS = d3.namespace('svg:text').space

    function setupGraph() {
      svg = d3.select('#chart').call(d3.zoom().on('zoom', zoomed)).append('g')
      node = svg.append('g').attr('stroke', '#fff').attr('stroke-width', 1.5)
      link = svg.append('g').attr('stroke', '#999').attr('stroke-opacity', 0.6)
    }

    const simulationNodes = []
    const simulationLinks = []

    function addData(addNodes, addLinks) {
      const links = addLinks.map((d) => ({ ...d }))
      const nodes = addNodes.map((d, index) => ({ ...d }))

      console.log(`🚀 ~ nodes:`, nodes)
      console.log(`🚀 ~ links:`, links)

      simulationNodes.push(...nodes)
      simulationLinks.push(...links)

      simulation = d3
        .forceSimulation(simulationNodes)
        .force(
          'link',
          d3
            .forceLink(simulationLinks)
            .id((d) => d.id)
            .distance((d) => 50)
        )
        .force('charge', d3.forceManyBody().strength(-400))
        .force('x', d3.forceX())
        .force('y', d3.forceY())
        .on('tick', ticked)

      node = node
        .selectAll()
        .data(nodes)
        .join((enter) => {
          return enter.append((d) => {
            const circleElement = document.createElementNS(svgNS, 'circle')

            circleElement.setAttribute('r', 16)
            circleElement.setAttribute('fill', '#318631')
            circleElement.setAttribute('stroke', '#7CC07C')
            circleElement.setAttribute('stroke-width', '3')

            return circleElement
          })
        })

      node.append('title').text((d) => `hello ${d.id}`)
      node.call(d3.drag().on('start', dragstarted).on('drag', dragged).on('end', dragended))

      link = link.selectAll().data(links).join('line').attr('stroke-width', 1)
    }

    setupGraph()

    // addData(nodeData, linkData)

    async function addDataInChunks(allNodes, allLinks) {
      const timeout = 7000

      let nodeChunk = allNodes.slice(0, 2)
      let linkChunk = allLinks.slice(0, 1)

      addData(nodeChunk, linkChunk)
      await new Promise((r) => setTimeout(r, timeout))
      //
      nodeChunk = allNodes.slice(2, 3)
      linkChunk = allLinks.slice(1, 2)

      addData(nodeChunk, linkChunk)
      await new Promise((r) => setTimeout(r, timeout))
      //
      nodeChunk = allNodes.slice(3, 4)
      linkChunk = allLinks.slice(2, 3)

      addData(nodeChunk, linkChunk)
      await new Promise((r) => setTimeout(r, timeout))

      console.log('addDataInChunks finished')
    }

    addDataInChunks(nodeData, linkData)

    //
    // Misc Functions
    //

    function zoomed(transform) {
      const t = transform.transform

      const container = this.getElementsByTagNameNS(svgNS, 'g')[0]
      const transformString = 'translate(' + t.x + ',' + t.y + ') scale(' + t.k + ')'

      container.setAttribute('transform', transformString)
    }

    function ticked() {
      link
        .attr('x1', (d) => d.source.x)
        .attr('y1', (d) => d.source.y)
        .attr('x2', (d) => d.target.x)
        .attr('y2', (d) => d.target.y)

      node.each(function (d) {
        this.setAttribute('cx', d.x)
        this.setAttribute('cy', d.y)
      })
    }

    function dragstarted(event) {
      if (!event.active) simulation.alphaTarget(0.3).restart()
      event.subject.fx = event.subject.x
      event.subject.fy = event.subject.y
    }

    function dragged(event) {
      event.subject.fx = event.x
      event.subject.fy = event.y
    }

    function dragended(event) {
      if (!event.active) simulation.alphaTarget(0)
      event.subject.fx = null
      event.subject.fy = null
    }
.graph {
      width: 750px;
      height: 400px;
    }
<script src="https://d3js.org/d3.v7.min.js" charset="utf-8"></script>
<svg ref="chart" id="chart" class="graph" style="background-color: #141621"></svg>

推荐答案

以下是我的更改摘要:

首先,你需要对一个"父"元素进行selectAll.您正在 Select 没有意义的 Select .这在第一个循环中起作用,因为所有数据都是"进入"的,但随后d3将找不到已经存在的绑定数据.您还应该显式地对元素或类使用selectAll.

其次,当您追加和跟踪simulationNodessimulationLinks时,您需要将相同的追加数据传递给 Select .否则,d3无法计算输入数据与更新数据.

第三,您需要管理模拟的生命周期.这意味着您需要停止它,并在修改其数据后重新启动它.

第四,我清理了您的数据连接.当与d3混合使用时,我发现使用原生SVG方法很奇怪.我还修改了它们,使其具有显式的Enter、UPDATE和Exit方法.您没有使用出口,但为了完整起见,我将其包括在内.

最后,我做了一些一般性的清理工作,比如更清晰的变量名称,而不是在每次数据修改时重新创建模拟,并将图形移到SVG的中心.

<!DOCTYPE html>

<html>
  <head>
    <style>
      .graph {
        width: 750px;
        height: 400px;
      }
    </style>
  </head>

  <body>
    <script src="https://d3js.org/d3.v7.min.js" charset="utf-8"></script>
    <svg
      ref="chart"
      id="chart"
      class="graph"
      style="background-color: #141621"
    ></svg>
    <script>
      const nodeData = [
        {
          id: 'node 1',
          added: false,
        },
        {
          id: 'node 2',
          added: false,
        },
        {
          id: 'node 3',
          added: false,
        },
        {
          id: 'node 4',
          added: false,
        },
      ];

      const linkData = [
        {
          linkID: 'link 1',
          added: false,
          source: 'node 1',
          target: 'node 2',
        },
        {
          linkID: 'link 2',
          added: false,
          source: 'node 1',
          target: 'node 3',
        },
        {
          linkID: 'link 3',
          added: false,
          source: 'node 3',
          target: 'node 4',
        },
      ];

      //
      //
      //

      let svg = null;

      const width = 750;
      const height = 400;
      const nodeCount = 10;

      function setupGraph() {
        svg = d3
          .select('#chart')
          .call(d3.zoom().on('zoom', zoomed))
          .append('g');
        nodeGroup = svg
          .append('g')
          .attr('stroke', '#fff')
          .attr('stroke-width', 1.5);
        linkGroup = svg
          .append('g')
          .attr('stroke', '#999')
          .attr('stroke-opacity', 0.6);
      }

      // nodes and links are the d3 selections
      let nodes = null;
      let links = null;
      // currentNodes and currentLinks is the current data
      const currentNodes = [];
      const currentLinks = [];

      // the link simulation
      const linkSim = d3
        .forceLink()
        .id((d) => d.id)
        .distance((d) => 50);

      // overall simulation
      const simulation = d3
        .forceSimulation()
        .force('link', linkSim)
        .force('charge', d3.forceManyBody().strength(-400))
        .force('x', d3.forceX())
        .force('y', d3.forceY())
        .force('center', d3.forceCenter(width / 2, height / 2))
        .on('tick', ticked);

      function addData(addNodes, addLinks) {
        addNodes = addNodes.map((d, index) => ({ ...d }));
        addLinks = addLinks.map((d) => ({ ...d }));

        currentNodes.push(...addNodes);
        currentLinks.push(...addLinks);

        nodes = nodeGroup
          .selectAll('circle')
          .data(currentNodes, (d) => d)
          .join(
            (enter) => {
              let e = enter
                .append('circle')
                .attr('r', 16)
                .attr('fill', '#318631')
                .attr('stroke', '#7CC07C')
                .attr('stroke-width', '3');
              e
                .append('title')
                .text((d) => `hello ${d.id}`);
                
              return e;
            },
            (update) => update,
            (exit) => exit.remove()
          );

        nodes.call(
          d3
            .drag()
            .on('start', dragstarted)
            .on('drag', dragged)
            .on('end', dragended)
        );

        links = linkGroup
          .selectAll('line')
          .data(currentLinks)
          .join(
            (enter) => enter.append('line').attr('stroke-width', 1),
            (update) => update,
            (exit) => exit.remove()
          );

        // stop and start the simulation
        simulation.stop();
        simulation.nodes(currentNodes);
        linkSim.links(currentLinks);
        simulation.alpha(0.3).restart();
      }

      setupGraph();

      async function addDataInChunks(allNodes, allLinks) {
        const timeout = 7000;

        let nodeChunk = allNodes.slice(0, 2);
        let linkChunk = allLinks.slice(0, 1);

        addData(nodeChunk, linkChunk);
        await new Promise((r) => setTimeout(r, timeout));
        //
        nodeChunk = allNodes.slice(2, 3);
        linkChunk = allLinks.slice(1, 2);

        addData(nodeChunk, linkChunk);
        await new Promise((r) => setTimeout(r, timeout));
        //
        nodeChunk = allNodes.slice(3, 4);
        linkChunk = allLinks.slice(2, 3);

        addData(nodeChunk, linkChunk);
        await new Promise((r) => setTimeout(r, timeout));

        console.log('addDataInChunks finished');
      }

      addDataInChunks(nodeData, linkData);

      //
      // Misc Functions
      //

      function zoomed(transform) {
        const t = transform.transform;

        const container = this.getElementsByTagNameNS(svgNS, 'g')[0];
        const transformString =
          'translate(' + t.x + ',' + t.y + ') scale(' + t.k + ')';

        container.setAttribute('transform', transformString);
      }

      function ticked() {
        links
          .attr('x1', (d) => d.source.x)
          .attr('y1', (d) => d.source.y)
          .attr('x2', (d) => d.target.x)
          .attr('y2', (d) => d.target.y);

        nodes.each(function (d) {
          this.setAttribute('cx', d.x);
          this.setAttribute('cy', d.y);
        });
      }

      function dragstarted(event) {
        if (!event.active) simulation.alphaTarget(0.3).restart();
        event.subject.fx = event.subject.x;
        event.subject.fy = event.subject.y;
      }

      function dragged(event) {
        event.subject.fx = event.x;
        event.subject.fy = event.y;
      }

      function dragended(event) {
        if (!event.active) simulation.alphaTarget(0);
        event.subject.fx = null;
        event.subject.fy = null;
      }
    </script>
  </body>
</html>

Javascript相关问答推荐

通过在页面上滚动来移动滚动条

单击子元素时关闭父元素(JS)

react/redux中的formData在expressjs中返回未定义的req.params.id

被CSS优先级所迷惑

使搜索栏更改语言

如何从一个列表中创建一个2列的表?

PrivateRoute不是路由组件错误

如何解决useState错误—setSelect Image不是函数''

如何使用JavaScript拆分带空格的单词

用JS从平面文件构建树形 struct 的JSON

确保函数签名中的类型安全:匹配值

如何将数组用作复合函数参数?

Angular 形式,从DOM中删除不会删除指定索引处的内容,但会删除最后一项

Rxjs流中生成IMMER不能在对象上操作

从逗号和破折号分隔的给定字符串中查找所有有效的星期几

构建器模式与参数对象输入

Reaction useState和useLoaderData的组合使用引发无限循环错误

如何使用[ModelJSON,ArrayBuffer]调用tf.loadGraphModelSync

如何使用Reaction路由导航测试挂钩?

当S点击按钮时,我如何才能改变它的样式?