🕌

vue-flowを使ってツリー構造を描画する

に公開

本業で購買部門にいたころ、企業のレジリエンスを強化するという某スタートアップ企業のサービスを導入しましたが、2年間経ってもロクに動きませんでした。(そんなサービスに2年間も毎月数十万払い続けている弊社も弊社です)

おまけにそこの社長が何言ってもいい加減な対応ばかりで、愛想を尽かして自分でサプライチェーンを管理するシステムを作りました。

私のオリジナルアプリでは、サプライチェーンを図示するためにvue-flowを使っています。
vue-flowを使うと、こんな感じで、サプライチェーンやBOMをツリー構造で可視化できます

※ ReactバージョンのReact-flowもあります。というかReact-flowが本家です。

vue-flowは通常は組織図を表示するためのものなので、上図のようにサプライチェーンやBOMを水平方向に表示させるにはちょっとしたコツが必要です。

vue-flowのインストール

"@vue-flow/background": "^1.2.0",
"@vue-flow/controls": "^1.1.1",
"@vue-flow/core": "^1.30.1",
"@vue-flow/minimap": "^1.4.0",
"@vue-flow/node-toolbar": "^1.1.0",

データ構造

  • 以下のように、それぞれのレコードで親子関係を示します。
クリックで展開
interface Construction {
    親品目: string
    子品目: string
}

const constructions: Construction[] = [
    { 親品目: 'root',            子品目: 'マルチビタミン30日分'},
    { 親品目: 'マルチビタミン30日分',子品目: 'マルチビタミン錠'},
    { 親品目: 'マルチビタミン30日分',子品目: 'アルミ袋'},
    { 親品目: 'ビタミンC',         子品目: 'HPC'},
    { 親品目: 'マルチビタミン30日分',子品目: '外箱'},
    { 親品目: 'マルチビタミン錠',    子品目: '還元麦芽糖水飴'},
    { 親品目: 'マルチビタミン錠',    子品目: 'ビタミンC'},
    { 親品目: 'マルチビタミン錠',    子品目: 'ビタミンB1'},
    { 親品目: 'ビタミンC',         子品目: 'アスコルビン酸'},
    { 親品目: 'ビタミンC',         子品目: 'アスコルビン酸Na'},
]

スクリプト部分

import { h, ref, computed } from 'vue'
import { Background } from '@vue-flow/background'
import { Controls } from '@vue-flow/controls'
import { MiniMap } from '@vue-flow/minimap'
import { VueFlow, useVueFlow, type Node, type Edge, Position } from '@vue-flow/core'

const { onConnect, addEdges } = useVueFlow()

const res = computed(() => {
    const _nodes: Node[] = []
    const _edges: Edge[] = []

    let childCount = 0

    function setChildren(parentNode: Node) {

        const children: Construction[] = constructions.filter(item => item.親品目 === parentNode.id)

        children.forEach((child,i, arr) => {
            const newNode: Node = {
                id: child.子品目,
                type: 'custom',
                label: child.子品目,
                position: { x: 200, y: (childCount+i)*50 },
                targetPosition: Position.Left,
                sourcePosition: Position.Right,
                parentNode: parentNode.id,
            }
            _nodes.push(newNode)
            _edges.push({
                id: child.子品目,
                source: parentNode.id,
                target: newNode.id,
                type: 'step',
            })

            // 末子まで行ったら、次の階層のノードを作成するために、子の数を足す
            if(i===arr.length-1){
                childCount += arr.length-1
            }

            setChildren(newNode)
        })

    }
    const topItem = constructions.find(item => item.親品目 === 'root')
    const topNode:Node = {
        id: topItem.子品目,
        type: 'input',
        label: topItem.子品目,
        position: { x: 0, y: 0 },
        targetPosition: Position.Left,
        sourcePosition: Position.Right,
    }
    _nodes.push(topNode)
    setChildren(topNode)

    return {nodes:_nodes,edges:_edges}
})

テンプレート部分

  <div class="h-[500px]">
    <VueFlow
      v-model:nodes="res.nodes"
      v-model:edges="res.edges"

      class="border"
      :default-zoom="0.2"
      :min-zoom="0.2"
      :max-zoom="4"
      :snap-to-grid="true"
      :snap-grid="[10, 10]"
      :fit-view-on-init="true"
    >
      <Background pattern-color="#aaa" :gap="16" />
      <Controls />

    </VueFlow>
  </div>

Discussion