Post

8. auto layout

ROS2에 대해 알아보자

8. auto layout

Auto Layout

  • vue-flow내 생성된 node/edge 정보를 토대로 수직 또는 수평으로 Layout Align을 맞추는 기능

사용 방법

  • Version1, 중첩된 노드를 고려하지 않음(Nested Node auto layout 지원 x)
  • Node/Edge의 경우 dagre 라이브러리를 사용하여 그래프 객체를 사로 만들고, useVueFlow fitView action을 사용하여 auto layout 수행 ( Node/Edge position을 잡기위해 dagre를 사용하는 것으로 보이나 동작 원리는 추가 분석 필요 )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# useLayout.js
import dagre from '@dagrejs/dagre'
import { Position, useVueFlow } from '@vue-flow/core'
import { ref } from 'vue'

/**
 * Composable to run the layout algorithm on the graph.
 * It uses the `dagre` library to calculate the layout of the nodes and edges.
 */
export function useLayout() {
  const { findNode } = useVueFlow()

  const graph = ref(new dagre.graphlib.Graph())

  const previousDirection = ref('LR')

  function layout(nodes, edges, direction) {
    // we create a new graph instance, in case some nodes/edges were removed, otherwise dagre would act as if they were still there
    const dagreGraph = new dagre.graphlib.Graph()

    graph.value = dagreGraph

    dagreGraph.setDefaultEdgeLabel(() => ({}))

    const isHorizontal = direction === 'LR'
    dagreGraph.setGraph({ rankdir: direction })

    previousDirection.value = direction

    for (const node of nodes) {
      // if you need width+height of nodes for your layout, you can use the dimensions property of the internal node (`GraphNode` type)
      const graphNode = findNode(node.id)

      dagreGraph.setNode(node.id, {
        width: graphNode.dimensions.width,
        height: graphNode.dimensions.height,
      })
      console.log(graphNode)
      console.log(dagreGraph)
    }

    for (const edge of edges) {
      dagreGraph.setEdge(edge.source, edge.target)
    }

    dagre.layout(dagreGraph)

    // set nodes with updated positions
    return nodes.map((node) => {
      const nodeWithPosition = dagreGraph.node(node.id)

      return {
        ...node,
        targetPosition: isHorizontal ? Position.Left : Position.Top,
        sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
        position: { x: nodeWithPosition.x, y: nodeWithPosition.y },
      }
    })
  }

  function arrangeNode(nodes, edges) {
    // we create a new graph instance, in case some nodes/edges were removed, otherwise dagre would act as if they were still there
    const dagreGraph = new dagre.graphlib.Graph()

    graph.value = dagreGraph

    dagreGraph.setDefaultEdgeLabel(() => ({}))
    dagreGraph.setGraph({ rankdir: 'TB' })

    for (const node of nodes) {
      // if you need width+height of nodes for your layout, you can use the dimensions property of the internal node (`GraphNode` type)
      const graphNode = findNode(node.id)

      dagreGraph.setNode(node.id, {
        width: graphNode.dimensions.width || 150,
        height: graphNode.dimensions.height || 50,
      })
    }

    for (const edge of edges) {
      dagreGraph.setEdge(edge.source, edge.target)
    }

    // set nodes with updated positions
    return nodes.map((node) => {
      return {
        ...node,
      }
    })
  }

  return { graph, layout, previousDirection, arrangeNode }
}


# main
async function layoutGraph(direction) {
  await stop()

  nodes.value = layout(nodes.value, edges.value, direction)
  nextTick(() => {
    fitView()
  })
}

Version2, 중첩된 노드를 고려한 Layout 정렬

  • Nested Node인 경우를 고려한 Auto Layout
  • Nested Node의 경우 노드 좌표는 부모 노드의 상대좌표로 환산 되어야함

x = child.x - parent.x; y = child.y - parent.y

Ref. https://github.com/xyflow/xyflow/discussions/2968

  • 부모-자식 노드 관계를 고려하여 Tree를 만들고 재귀호출 탐색(DFS)으로 layout도 정렬이 가능하지만, dagre-d3의 cluster 기능을 활용하여 x/y/width/height 값을 받아오고,
  • 자식 노드일 경우는 부모 노드 좌표로 환산하여 업데이트 수행
# useLayout.js
import dagre from '@dagrejs/dagre'
import * as dagreD3 from 'dagre-d3'
import { Position, useVueFlow } from '@vue-flow/core'
import { ref } from 'vue'

export function useLayout() {
  const { findNode } = useVueFlow()

  const graph = ref(new dagre.graphlib.Graph())

  const previousDirection = ref('LR')

  function layout(nodes, edges, direction) {
    const dagreGraph = new dagreD3.graphlib.Graph({ compound: true })
      .setGraph({})
      .setDefaultEdgeLabel(function () {
        return {}
      })

    graph.value = dagreGraph

    dagreGraph.setDefaultEdgeLabel(() => ({}))

    const isHorizontal = direction === 'LR'
    dagreGraph.setGraph({ rankdir: direction, ranksep: 10 })

    previousDirection.value = direction

    for (const node of nodes) {
      const graphNode = findNode(node.id)

      dagreGraph.setNode(node.id, {
        width: graphNode.dimensions.width,
        height: graphNode.dimensions.height,
      })
    }

    for (const edge of edges) {
      dagreGraph.setEdge(edge.source, edge.target)
    }

    const groupNodes = nodes.filter((node) => node.parentNode)
    for (const groupNode of groupNodes) {
      dagreGraph.setParent(groupNode.id, groupNode.parentNode)
    }

    dagre.layout(dagreGraph)

    return nodes.map((node) => {
      const nodeWithPosition = dagreGraph.node(node.id)

      if (node.parentNode) {
        const parentNode = dagreGraph.node(node.parentNode)
        return {
          ...node,
          targetPosition: isHorizontal ? Position.Left : Position.Top,
          sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
          computedPosition: {
            x: nodeWithPosition.x - nodeWithPosition.width / 2,
            y: nodeWithPosition.y - nodeWithPosition.height / 2,
          },
          position: {
            x:
              nodeWithPosition.x - nodeWithPosition.width / 2 - parentNode.x + parentNode.width / 2,
            y:
              nodeWithPosition.y -
              nodeWithPosition.height / 2 -
              parentNode.y +
              parentNode.height / 2,
          },
          style: {
            width: `${nodeWithPosition.width}px`,
            height: `${nodeWithPosition.height}px`,
          },
        }
      } else {
        return {
          ...node,
          targetPosition: isHorizontal ? Position.Left : Position.Top,
          sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
          computedPosition: {
            x: nodeWithPosition.x - nodeWithPosition.width / 2,
            y: nodeWithPosition.y - nodeWithPosition.height / 2,
          },
          position: {
            x: nodeWithPosition.x - nodeWithPosition.width / 2,
            y: nodeWithPosition.y - nodeWithPosition.height / 2,
          },
          style: {
            width: `${nodeWithPosition.width}px`,
            height: `${nodeWithPosition.height}px`,
          },
        }
      }
    })
  }

  function arrangeNode(nodes, edges) {
    const dagreGraph = new dagre.graphlib.Graph()

    graph.value = dagreGraph

    dagreGraph.setDefaultEdgeLabel(() => ({}))
    dagreGraph.setGraph({ rankdir: 'TB' })

    for (const node of nodes) {
      const graphNode = findNode(node.id)

      dagreGraph.setNode(node.id, {
        width: graphNode.dimensions.width || 150,
        height: graphNode.dimensions.height || 50,
      })
    }

    for (const edge of edges) {
      dagreGraph.setEdge(edge.source, edge.target)
    }

    return nodes.map((node) => {
      return {
        ...node,
      }
    })
  }

  return { graph, layout, previousDirection, arrangeNode }
}

This post is licensed under CC BY 4.0 by the author.