首页 > 解决方案 > d3 强制布局:d3 是否需要节点中的某些顺序才能正常工作?

问题描述

我正在制作一个图形可视化玩具项目。我希望能够给它一个边列表或邻接列表作为输入(在文本区域中),并在 svg 元素中向我展示相应的图形绘制。描述为边缘列表的图有效,但邻接列表以某种方式为所有节点提供相同的索引。

在过去的几天里,我一直在为一个问题头疼。我给出了相同的图表,当被描述为边缘列表时它工作正常......

伊姆古尔

,当被描述为邻接列表时,它给出了:

伊姆古尔

这是接受文本输入并将其转换为邻接/边缘列表的部分:

export const getAdjacencyListFromText = (text: string) => {
  const tokenizedLines = getLines(text)
    .map(line => getTokensFromLine(line).map(token => Number(token)))
    .filter(tokens => tokens.length >= 2)

  const adjacencyList = tokenizedLines.reduce<AdjacencyList>((acc: any, current: any) => {
    const source = current[0]
    const neighbors = current.slice(1)
    acc[source] = neighbors
    return acc
  }, [])

  return adjacencyList
}
export const getEdgeListFromText = (text: string) =>
  getLines(text)
    .map(line => getTokensFromLine(line).map(e => Number(e)))
    .filter(lineTokens => lineTokens.length >= 2)
    .map(tokens => {
      const source = tokens[0]
      const target = tokens[1]
      const weight = tokens[2]
      if (tokens.length === 2) return { source, target }
      else if (tokens.length === 3) return { source, target, weight }
    }) as EdgeList

创建图形对象的部分:

export const getGraphFromAdjacencyList = (adjacencyList: AdjacencyList) => {
  const nodes = adjacencyList.reduce<G.Node>((acc: G.Node[], current: AdjacencyListEntry, index: number) => {
    current.forEach(node => acc.push({ id: node }))
    if (!acc.some(el => el.id === index)) acc.push({ id: index })
    return acc
  }, []) as G.Node[]

  const links = nodes.reduce<G.Link>((acc: G.Link[], currentNode: G.Node) => {
    const neighbors = adjacencyList[currentNode.id]
    if (neighbors === undefined) return acc
    const newLinks = neighbors
      .map(neighborId => ({ source: currentNode.id, target: neighborId }))
      .filter(link => !acc.some(l => l.source === link.source && l.target === link.target))
    return acc = acc.concat(newLinks)
  }, [])

  return { nodes, links }
}

export const getGraphFromEdgeList = (edgeList: EdgeList) => {
  const nodes = edgeList.reduce<G.Node>((acc: G.Node[], currentEdge) => {
    const { source, target } = currentEdge
    if (!acc.some(n => n.id === source)) {
      acc.push({ id: source })
    }
    if (!acc.some(n => n.id === target)) {
      acc.push({ id: target })
    }
    return acc
  }, [])

  return {
    links: edgeList,
    nodes,
  }
}

以及我将该图提供给 d3 的部分:

  componentDidUpdate() {
    const { links: edgeData, nodes: nodeData } = this.props.graph

    // newNodes and newLinks is done so that old nodes and links keep their position
    // goal is better visuals
    const previousNodes = this.simulation.nodes()
    const newNodes = nodeData.map(node => {
      const existingNode = _(previousNodes).find((n: any) => n.id === node.id)
      if (existingNode !== undefined) {
        return existingNode
      } else {
        return node
      }
    })

    const previousLinks = this.simulation.force("link").links()
    const newLinks = edgeData.map(edge => {
      const existingLink = _(previousLinks).find((l: any) => l.source.id === edge.source && l.target.id === edge.target)
      if (existingLink !== undefined) {
        return existingLink
      } else {
        return edge
      }
    })

    const line = this.edgeLayer.selectAll("g").data(newLinks)
    const node = this.nodeLayer.selectAll("g").data(newNodes)

    node.exit().remove()
    line.exit().remove()

    this.node = this.createNode(node)
    this.edge = this.createEdge(line)

    this.simulation = this.simulation.nodes(newNodes)
    this.simulation.force("link").links(newLinks).id(d => d.id)
    this.simulation.on("tick", this.simulationTick)
    this.simulation.alphaTarget(1)
    this.simulation.restart()
  }

邻接列表中的图形对象是这样的:

{ 
     nodes: [ { id: 2 }, { id: 1 }, { id: 3 } ],
     links: [ { source: 2, target: 3 }, { source: 1, target: 2 } ] }
}

边缘列表中的图形对象是这样的:

{     
     nodes: [ { id: 1 }, { id: 2 }, { id: 3 } ],
     links: [ { source: 1, target: 2 }, { source: 1, target: 3 } ]
}

唯一的区别是节点数组中节点的顺序。因此,在为我的边缘列表和邻接列表实用程序函数编写了一整个下午的测试用例之后,我决定在将节点提供给 d3 之前对其进行排序......并且它可以工作......

伊姆古尔

为什么顺序很重要?我去查看文档,并且没有任何地方(我到目前为止看到的)提到与节点顺序相关的任何重要性。我很困惑。

标签: javascriptd3.js

解决方案


好的,我相信我的图表是出于巧合在 EdgeList 案例中工作的。

似乎我应该selectAll在我的函数中使用节点和链接,而tick我应该使用它select。我getAdjacencyListFromText有时也会给出重复的节点/链接,我删除了那个错误。

结果:

simulationTick

  simulationTick = () => {
    const { width, height } = this.props
    const keepBounded = (p: Point) => ({
      ...p,
      x: Math.max(0, Math.min(p.x, width)),
      y: Math.max(0, Math.min(p.y, height)),
    })

    const processPoint = (p: Point) => keepBounded(p)

    const setEdgeAttributes = edge =>
      edge
        .attr("x1", d => processPoint(d.source).x)
        .attr("y1", d => processPoint(d.source).y)
        .attr("x2", d => processPoint(d.target).x)
        .attr("y2", d => processPoint(d.target).y)

    const setNodeAttributes = node =>
      node.attr("transform", d => {
        const p = processPoint(d)
        return "translate(" + p.x + ", " + p.y + ")"
      })

    setEdgeAttributes(this.edge.select("line"))
    setNodeAttributes(this.node)
  }

内部createNode函数,更改selectAllselect

  createNode = node => {
    const nodeGroup = node

    const nodeGroupEnter = nodeGroup.enter().append("g")

    // here
    const nodeCircle = nodeGroup.select("circle")
    const nodeLabel = nodeGroup.select("text")
    // here

    const nodeCircleEnter = nodeGroupEnter.append("circle")
    nodeCircleEnter(...)

    const nodeLabelEnter = nodeGroupEnter.append("text")
    nodeLabelEnter(...)

    return nodeGroupEnter.merge(nodeGroup).attr("id", d => d.id)
  }

相同的createEdge

  createEdge = edge => {
    const edgeGroup = edge

    const edgeGroupEnter = edgeGroup.enter().append("g")

    // here
    const edgeLine = edgeGroup.select("line")
    const edgeLabel = edgeGroup.select("text")
    // here

    const edgeLineEnter = edgeGroupEnter.append("line")
    edgeLineEnter (...)

    const edgeLabelEnter = edgeGroupEnter.append("text")
    edgeLabelEnter (...)
    return edgeGroupEnter.merge(edgeGroup)
  }

推荐阅读