如何在 d3.js 中同时自动移动节点和链接

Posted

技术标签:

【中文标题】如何在 d3.js 中同时自动移动节点和链接【英文标题】:How to automatically move nodes and links at the same time in d3.js 【发布时间】:2022-01-15 00:35:28 【问题描述】:

我正在 d3.js(v7) 中尝试force-directed example。 在这个示例中,当我拖动一个节点时,链接和其他节点会同步移动。 我希望所有节点始终随机移动,并且我希望其他链接和节点与它们同步移动,就像我在拖动它们一样。 代码如下。 json 文件与示例相同。当我运行此代码时,节点会移动,但链接不会跟随移动并保持静止。

function ForceGraph(
    nodes, // an iterable of node objects (typically [id, …])
    links, // an iterable of link objects (typically [source, target, …])
  , 
    nodeId = d => d.id, // given d in nodes, returns a unique identifier (string)
    nodeGroup, // given d in nodes, returns an (ordinal) value for color
    nodeGroups, // an array of ordinal values representing the node groups
    nodeStrength,
    linkSource = (source) => source, // given d in links, returns a node identifier string
    linkTarget = (target) => target, // given d in links, returns a node identifier string
    linkStrokeWidth = 10, // given d in links, returns a stroke width in pixels
    linkStrength = 0.55,
    colors = d3.schemeTableau10, // an array of color strings, for the node groups
    width = 640, // outer width, in pixels
    height = 400, // outer height, in pixels
    invalidation // when this promise resolves, stop the simulation
   = ) 
    // Compute values.
    const N = d3.map(nodes, nodeId).map(intern);
    const LS = d3.map(links, linkSource).map(intern);
    const LT = d3.map(links, linkTarget).map(intern);
  
    if (nodeTitle === undefined) nodeTitle = (_, i) => N[i];
    const T = nodeTitle == null ? null : d3.map(nodes, nodeTitle);
    const G = nodeGroup == null ? null : d3.map(nodes, nodeGroup).map(intern);
    const W = typeof linkStrokeWidth !== "function" ? null : d3.map(links, linkStrokeWidth);
  
    // Replace the input nodes and links with mutable objects for the simulation.
    nodes = d3.map(nodes, (_, i) => (id: N[i], type: NODETYPES[i], tag: parsed_NODETAGS[i], texts: X[i]));
    links = d3.map(links, (_, i) => (source: LS[i], target: LT[i]));
  
    // Compute default domains.
    if (G && nodeGroups === undefined) nodeGroups = d3.sort(G);
  
    // Construct the scales.
    const color = nodeGroup == null ? null : d3.scaleOrdinal(nodeGroups, colors);
  
    // Construct the forces.
    const forceLink = d3.forceLink(links).id((index: i) => N[i]);
    if (nodeStrength !== undefined) forceNode.strength(nodeStrength);
    if (linkStrength !== undefined) forceLink.strength(linkStrength);
  
    const zoom = d3.zoom()
        .scaleExtent([1, 40])
        .on("zoom", zoomed);
  
    const svg = d3.create("svg")
        .attr("viewBox", [-width / 2, -height / 2.5, width, height])
        .on("click", reset)
        .attr("style", "max-width: 100%; height: auto; height: intrinsic;");
    
    svg.call(zoom);
    const g = svg.append("g");
  
    const link = g.append("g")
      .selectAll("line")
      .data(links)
      .join("line");
  
    const simulation = d3.forceSimulation(nodes)
      .force("link", forceLink)
      .force("charge", d3.forceManyBody())
      .force("center",  d3.forceCenter())
      .on("tick", ticked);
    
    const node = g.append("g")
        .attr("class", "nodes")
        .style("opacity", 1.0)
      .selectAll("circle")
      .data(nodes)
      .join("circle")
        .attr("r", 5)
        .call(drag(simulation));
  
    if (W) link.attr("stroke-width", (index: i) => W[i]);
    if (G) node.attr("fill", (index: i) => color(G[i]));

    function random()
        node
          .transition()
          .duration(2000)
          .attr("cx", function(d)
            return d.x + Math.random()*80 - 40;
          )
          .attr("cy", function(d)
            return d.y + Math.random()*80 - 40;
          );
      
    setInterval(random, 800);
    if (invalidation != null) invalidation.then(() => simulation.stop());
  
    function intern(value) 
      return value !== null && typeof value === "object" ? value.valueOf() : value;
    
  
    function ticked() 
      node
        .attr("cx", d => d.x)
        .attr("cy", d => d.y);
  
      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);
    

    function drag(simulation)     
      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;
      
      return d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended);
    

    function zoomed(transform) 
      g.attr("transform", transform);
    
  
    return Object.assign(svg.node(), );

【问题讨论】:

如果您可以创建代码的minimal reproducible example,最好在runnable stack snippet 中使用一些样本/虚拟数据来帮助我们理解问题,这将大有帮助。这样,我们可以更轻松地回答您的问题,您也更有可能得到一个好的答案! 只是添加更多代码和minimal reproducible example不一样,你需要让它可运行和可调试,并删除所有不必要的功能(如拖动) 【参考方案1】:

在您的函数random() 中,您不会更改基础数据,您只会更改它的表示方式。每个circle 都包含对nodes 数组中一个元素的引用,但是您在random() 中设置了cxcy,您不会更新基础数据d.xd.y。即便如此,圈子对cxcy 的值也没有反应。也就是说,当d.xd.y 发生变化时,它们不会被重新评估。

所以我会拆分你的代码。有一个函数 random() 每 800 毫秒调用一次,并通过更改 d.xd.y 来稍微调整节点。然后simulation 负责实际绘制圆圈和链接——它似乎已经这样做了。

const size = 500;

const nodes = [
    id: 'A',
    x: 150,
    y: 150
  ,
  
    id: 'B',
    x: 250,
    y: 250
  ,
  
    id: 'C',
    x: 350,
    y: 350
  
];
const links = [
    source: nodes[0],
    target: nodes[1]
  ,
  
    source: nodes[0],
    target: nodes[2]
  
];

const svg = d3.select('body')
  .append('svg')
  .attr('width', size)
  .attr('height', size)
  .attr('border', 'solid 1px red')
const g = svg.append('g');

const node = g.append("g")
  .attr("class", "nodes")
  .selectAll("circle")
  .data(nodes)
  .join("circle")
  .attr("r", 5);

const link = g.append("g")
  .attr("class", "links")
  .selectAll("line")
  .data(links)
  .join("line");

const simulation = d3.forceSimulation(nodes)
  .force("link", d3.forceLink(links).strength(2))
  .force("charge", d3.forceManyBody().strength(2))
  .force("center", d3.forceCenter(size / 2, size / 2).strength(0.05))
  .on("tick", ticked);

function ticked() 
  node
    .attr("cx", d => d.x)
    .attr("cy", d => d.y);
  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);


function random() 
  simulation.stop();
  nodes.forEach(d => 
    d.x += Math.random() * 80 - 40;
    d.y += Math.random() * 80 - 40;
  );

  node
    .transition(1000)
    .attr("cx", d => d.x)
    .attr("cy", d => d.y);
  link
    .transition(1000)
    .attr("x1", d => d.source.x)
    .attr("y1", d => d.source.y)
    .attr("x2", d => d.target.x)
    .attr("y2", d => d.target.y)
    .on('end', () =>  simulation.restart(); );

setInterval(random, 2000);
.links>line 
  stroke: black;
  stroke-width: 2px;
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.1.1/d3.min.js"></script>

【讨论】:

以上是关于如何在 d3.js 中同时自动移动节点和链接的主要内容,如果未能解决你的问题,请参考以下文章

如何检查没有链接的节点的d3 js力图并删除它们?

如何在 d3.js 中创建家谱?

d3.js 使用 exit() 和 enter() 进行子转换

如何防止 d3.js 强制布局在恢复/重启时脉动/弹跳

D3.js 网络图使用力导向布局和矩形节点

在 d3.js 中将节点和链接转换为分层树