如何将数据块添加到 d3 力定向图?

问题描述 投票:0回答:1

我正在尝试动态地将节点添加到 D3 力定向图。我相信我已经很接近了,但不理解一些与 D3 中的选择如何工作相关的关键概念。

如果我直接调用我的

addData
函数并立即添加所有节点,一切都会按预期进行。

但是,如果我使用

addDataInChunks
,则会添加前两个节点和第一个链接,但之后图形中不会出现更多节点。此外,一旦将第二个块添加到图表中,拖动节点的能力就会丢失。

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

    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>

javascript d3.js d3-force-directed
1个回答
0
投票

以下是我的更改摘要:

首先,您需要

selectAll
位于元素的“父级”上。您正在选择没有意义的选择。这在第一次循环中有效,因为所有数据都在“输入”,但随后 d3 将找不到已经存在的绑定数据。您还应该在元素或类上明确
selectAll

其次,当您附加并跟踪

simulationNodes
simulationLinks
时,您需要将相同的附加数据传递给选择。否则,d3无法计算输入和更新数据。

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

第四,我清理了您的数据连接。我发现与

d3
混合使用原生 svg 方法很奇怪。我还修改了它们,使其具有显式的输入、更新和退出方法。您没有使用出口,但为了完整性我将其包括在内。

最后,我做了一些常规清理,例如更清晰的变量名称,而不是在每次数据修改时重新创建模拟并将图形移动到 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) =>
              enter
                .append('circle')
                .attr('r', 16)
                .attr('fill', '#318631')
                .attr('stroke', '#7CC07C')
                .attr('stroke-width', '3'),
            (update) => update,
            (exit) => exit.remove()
          );

        nodes.append('title').text((d) => `hello ${d.id}`);
        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>

© www.soinside.com 2019 - 2024. All rights reserved.