代码之家  ›  专栏  ›  技术社区  ›  Mehdi Saffar

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

  •  1
  • Mehdi Saffar  · 技术社区  · 6 年前

    我正在做一个图形可视化玩具项目。我希望能够给它一个边列表或邻接列表作为输入(在文本区域中),并在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 回复  |  直到 6 年前
        1
  •  0
  •   Mehdi Saffar    6 年前

    好吧,所以我的图表是为了巧合而在埃格利斯特案例中工作的,我相信。

    我好像在用 selectAll 用于我的 tick 我本该使用的功能 select 相反。 我的 getAdjacencyListFromText 有时也会提供重复的节点/链接,我删除了这个bug。

    结果:

    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 功能,已更改 精选的 选择 :

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