d3强制布局:d3是否要求节点中的某些顺序才能正常工作?

时间:2019-01-14 19:54:21

标签: javascript d3.js

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

最近几天,我一直在为一个问题打招呼。我给出了相同的图形,当描述为边缘列表时,它行得通...

Imgur

,当被描述为邻接列表时,它给出以下内容:

Imgur

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

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 ...,它可以正常工作...

Imgur

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

1 个答案:

答案 0 :(得分:0)

好的,所以我的图表在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函数内部,将selectAll更改为select

  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)
  }