Miscellaneous
Get the content as SVG data
You can obtain the displayed content as SVG text data.
In this example, the SVG data is downloaded.
<script setup lang="ts">
import { reactive, ref } from "vue"
import * as vNG from "v-network-graph"
import { Download } from "@element-plus/icons"
import data from "./data"
const nodes = reactive<vNG.Nodes>({ ...data.nodes })
const edges = reactive<vNG.Edges>({ ...data.edges })
const nextNodeIndex = ref(Object.keys(nodes).length + 1)
const nextEdgeIndex = ref(Object.keys(edges).length + 1)
const selectedNodes = ref<string[]>([])
const selectedEdges = ref<string[]>([])
// ref="graph"
const graph = ref<vNG.Instance>()
async function downloadAsSvg() {
if (!graph.value) return
const text = await graph.value.exportAsSvgText()
const url = URL.createObjectURL(new Blob([text], { type: "octet/stream" }))
const a = document.createElement("a")
a.href = url
a.download = "network-graph.svg" // filename to download
a.click()
window.URL.revokeObjectURL(url)
}
function addNode() {
const nodeId = `node${nextNodeIndex.value}`
const name = `N${nextNodeIndex.value}`
nodes[nodeId] = { name }
nextNodeIndex.value++
}
function removeNode() {
for (const nodeId of selectedNodes.value) {
delete nodes[nodeId]
}
}
function addEdge() {
if (selectedNodes.value.length !== 2) return
const [source, target] = selectedNodes.value
const edgeId = `edge${nextEdgeIndex.value++}`
edges[edgeId] = { source, target }
}
function removeEdge() {
for (const edgeId of selectedEdges.value) {
delete edges[edgeId]
}
}
</script>
<template>
<div class="demo-control-panel">
<el-button type="primary" @click="downloadAsSvg">
<el-icon><download /></el-icon>
Download SVG
</el-button>
<label>Node:</label>
<el-button @click="addNode">add</el-button>
<el-button
:disabled="selectedNodes.length == 0"
@click="removeNode"
>remove</el-button>
<label>Edge:</label>
<el-button :disabled="selectedNodes.length != 2" @click="addEdge">add</el-button>
<el-button
:disabled="selectedEdges.length == 0"
@click="removeEdge"
>remove</el-button>
</div>
<v-network-graph
ref="graph"
v-model:selected-nodes="selectedNodes"
v-model:selected-edges="selectedEdges"
:nodes="nodes"
:edges="edges"
:layouts="data.layouts"
:configs="data.configs"
/>
</template>
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
import { Nodes, Edges, Layouts, defineConfigs } from "v-network-graph"
const nodes: Nodes = {
node1: { name: "N1" },
node2: { name: "N2" },
node3: { name: "N3" },
}
const edges: Edges = {
edge1: { source: "node1", target: "node2" },
edge2: { source: "node2", target: "node3" },
}
const layouts: Layouts = {
nodes: {
node1: { x: 50, y: 0 },
node2: { x: 0, y: 75 },
node3: { x: 100, y: 75 },
},
}
const configs = defineConfigs({
node: {
selectable: 2, // up to 2 nodes
},
edge: {
selectable: true,
normal: {
width: 3,
},
},
})
export default {
nodes,
edges,
layouts,
configs,
}
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
The above example saves the XML data as it is drawn as SVG.
If the SVG contains image elements (<image />
tag), the URLs specified will be still included. If relative URLs are specified, it would be no longer valid.
As measures to address this problem, by passing the { embedImages: true }
argument to the exportAsSvgText()
function, the URLs in the <image />
elements can be converted to the data-url(base64) format and embed them in the SVG document. However, whether or not this format can be displayed depends on the software that opens the exported SVG file. (Google Chrome can display it).
Below is an example of exporting with embedded images. An example using images also shown in Appearance Customization.
<script setup lang="ts">
import { ref } from "vue"
import * as vNG from "v-network-graph"
import { Download } from "@element-plus/icons"
import data from "./data"
// photo generated by https://generated.photos/
const nodes: vNG.Nodes = {
node1: { name: "User 1", face: "0023286.png" },
node2: { name: "User 2", face: "0700202.png" },
node3: { name: "User 3", face: "0037725.png" },
node4: { name: "User 4", face: "0062138.png" },
node5: { name: "User 5", face: "0103755.png" },
node6: { name: "User 6", face: "0092035.png" },
}
// ref="graph"
const graph = ref<vNG.Instance>()
async function downloadAsSvg() {
if (!graph.value) return
const text = await graph.value.exportAsSvgText({ embedImages: true })
const url = URL.createObjectURL(new Blob([text], { type: "octet/stream" }))
const a = document.createElement("a")
a.href = url
a.download = "network-graph.svg" // filename to download
a.click()
window.URL.revokeObjectURL(url)
}
</script>
<template>
<div class="demo-control-panel">
<el-button type="primary" @click="downloadAsSvg">
<el-icon><download /></el-icon>
Download SVG with Images
</el-button>
</div>
<v-network-graph
ref="graph"
:nodes="nodes"
:edges="data.edges"
:layouts="data.layouts"
:configs="data.configs"
>
<defs>
<!--
Define the path for clipping the face image.
To change the size of the applied node as it changes,
add the `clipPathUnits="objectBoundingBox"` attribute
and specify the relative size (0.0~1.0).
-->
<clipPath id="faceCircle" clipPathUnits="objectBoundingBox">
<circle cx="0.5" cy="0.5" r="0.5" />
</clipPath>
</defs>
<!-- Replace the node component -->
<template #override-node="{ nodeId, scale, config, ...slotProps }">
<!-- circle for filling background -->
<circle
class="face-circle"
:r="config.radius * scale"
fill="#ffffff"
v-bind="slotProps"
/>
<!--
The base position of the <image /> is top left. The node's
center should be (0,0), so slide it by specifying x and y.
-->
<image
class="face-picture"
:x="-config.radius * scale"
:y="-config.radius * scale"
:width="config.radius * scale * 2"
:height="config.radius * scale * 2"
:xlink:href="`./faces/${nodes[nodeId].face}`"
clip-path="url(#faceCircle)"
/>
<!-- circle for drawing stroke -->
<circle
class="face-circle"
:r="config.radius * scale"
fill="none"
stroke="#808080"
:stroke-width="1 * scale"
v-bind="slotProps"
/>
</template>
</v-network-graph>
</template>
<style lang="scss" scoped>
// transitions when scaling on mouseover.
.face-circle,
.face-picture {
transition: all 0.1s linear;
}
// suppress image events so that mouse events are received
// by the background circle.
.face-picture {
pointer-events: none;
}
</style>
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
106
import { defineConfigs, Edges, Layouts } from "v-network-graph"
const edges: Edges = {
edge1: { source: "node1", target: "node2" },
edge2: { source: "node2", target: "node3" },
edge3: { source: "node2", target: "node4" },
edge4: { source: "node4", target: "node5" },
edge5: { source: "node4", target: "node6" },
}
const layouts: Layouts = {
nodes: {
node1: { x: 0, y: 0 },
node2: { x: 80, y: 80 },
node3: { x: 0, y: 160 },
node4: { x: 240, y: 80 },
node5: { x: 320, y: 0 },
node6: { x: 320, y: 160 },
},
}
const configs = defineConfigs({
node: {
selectable: true,
normal: {
radius: 20,
},
hover: {
radius: 22,
},
},
})
export default {
edges,
layouts,
configs,
}
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
TIP
Performing exportAsSvgData()
instead of exportAsSvgText()
will get a DOM object. After manipulating the DOM (e.g., removing unwanted elements or rewriting attributes), you can retrieve the SVG string from outerHTML
.
Grid
<script setup lang="ts">
import { reactive } from "vue"
import * as vNG from "v-network-graph"
import data from "./data"
const configs = reactive(
vNG.defineConfigs({
view: {
grid: {
visible: true,
interval: 10,
thickIncrements: 5,
line: {
color: "#e0e0e0",
width: 1,
dasharray: 1,
},
thick: {
color: "#cccccc",
width: 1,
dasharray: 0,
},
},
layoutHandler: new vNG.GridLayout({ grid: 10 }),
},
})
)
</script>
<template>
<div class="demo-control-panel">
<demo-grid-panel
v-model:visible="configs.view.grid.visible"
v-model:interval="configs.view.grid.interval"
v-model:thickIncrements="configs.view.grid.thickIncrements"
v-model:normalColor="configs.view.grid.line.color"
v-model:normalWidth="configs.view.grid.line.width"
v-model:normalDasharray="configs.view.grid.line.dasharray"
v-model:thickColor="configs.view.grid.thick.color"
v-model:thickWidth="configs.view.grid.thick.width"
v-model:thickDasharray="configs.view.grid.thick.dasharray"
/>
</div>
<v-network-graph
:nodes="data.nodes"
:edges="data.edges"
:layouts="data.layouts"
:configs="configs"
/>
</template>
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
import { Nodes, Edges, Layouts } from "v-network-graph"
const nodes: Nodes = {
node1: { name: "Node 1" },
node2: { name: "Node 2" },
node3: { name: "Node 3" },
node4: { name: "Node 4" },
node5: { name: "Node 5" },
}
const edges: Edges = {
edge1: { source: "node1", target: "node2" },
edge2: { source: "node2", target: "node3" },
edge3: { source: "node3", target: "node4" },
edge4: { source: "node3", target: "node4" },
edge5: { source: "node4", target: "node5" },
edge6: { source: "node4", target: "node5" },
edge7: { source: "node4", target: "node5" },
edge8: { source: "node4", target: "node5" },
edge9: { source: "node4", target: "node5" },
edge10: { source: "node4", target: "node5" },
edge11: { source: "node4", target: "node5" },
edge12: { source: "node4", target: "node5" },
}
const layouts: Layouts = {
nodes: {
node1: { x: 0, y: 0 },
node2: { x: 80, y: 80 },
node3: { x: 160, y: 0 },
node4: { x: 240, y: 80 },
node5: { x: 320, y: 0 },
},
}
export default {
nodes,
edges,
layouts,
}
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
Coordinates translation
Mutual coordinate translation between the DOM and SVG is provided by the methods of the v-network-graph component.
As an example of DOM to SVG coordinate translation, the process of adding a new node at the position clicked by the user is shown below.
<script setup lang="ts">
import { reactive, ref } from "vue"
import * as vNG from "v-network-graph"
import data from "./data"
// ref="graph"
const graph = ref<vNG.Instance>()
const nodes = reactive({ ...data.nodes })
const layouts = ref({ ...data.layouts })
let nextNodeIndex = Object.keys(nodes).length + 1
const eventHandlers: vNG.EventHandlers = {
"view:click": ({ event }) => {
if (!graph.value) return
const point = { x: event.offsetX, y: event.offsetY }
// translate coordinates: DOM -> SVG
const svgPoint = graph.value.translateFromDomToSvgCoordinates(point)
// add node and its position
const nodeId = `node${nextNodeIndex}`
const name = `N${nextNodeIndex}`
layouts.value.nodes[nodeId] = svgPoint
nodes[nodeId] = { name }
nextNodeIndex++
},
}
</script>
<template>
<v-network-graph
ref="graph"
v-model:nodes="nodes"
:edges="data.edges"
v-model:layouts="layouts"
:event-handlers="eventHandlers"
/>
</template>
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
import { Nodes, Edges, Layouts } from "v-network-graph"
const nodes: Nodes = {
node1: { name: "N1" },
node2: { name: "N2" },
}
const edges: Edges = {
edge1: { source: "node1", target: "node2" },
}
const layouts: Layouts = {
nodes: {
node1: { x: 50, y: 0 },
node2: { x: 0, y: 75 },
},
}
export default {
nodes,
edges,
layouts,
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
For an example of SVG to DOM coordinate translation, please see the Tooltip example in Event Handling section.
Centering on Load
This example pans centered on the specified coordinates when loaded.
By default, the content is centered according to the content on load. If you want to pan to any position on load, disable this with the configs.view.autoPanAndZoomOnLoad
config.
<script setup lang="ts">
import { reactive, ref } from "vue"
import * as vNG from "v-network-graph"
import data from "./data"
const graph = ref<vNG.Instance>()
const targetNode = "node4"
const selectedNodes = ref([targetNode])
const eventHandlers: vNG.EventHandlers = {
"view:load": () => {
if (!graph.value) return
// Pan the target node position to the center.
const sizes = graph.value.getSizes()
graph.value.panTo({
x: sizes.width / 2 - data.layouts.nodes[targetNode].x,
y: sizes.height / 2 - data.layouts.nodes[targetNode].y,
})
},
}
const configs = reactive(
vNG.defineConfigs({
view: {
autoPanAndZoomOnLoad: false,
},
node: {
selectable: true,
},
})
)
</script>
<template>
<v-network-graph
ref="graph"
v-model:selected-nodes="selectedNodes"
:nodes="data.nodes"
:edges="data.edges"
:layouts="data.layouts"
:configs="configs"
:event-handlers="eventHandlers"
/>
</template>
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
import { Nodes, Edges, Layouts } from "v-network-graph"
const nodes: Nodes = {
node1: { name: "Node 1" },
node2: { name: "Node 2" },
node3: { name: "Node 3" },
node4: { name: "Node 4" },
}
const edges: Edges = {
edge1: { source: "node1", target: "node2" },
edge2: { source: "node2", target: "node3" },
edge3: { source: "node3", target: "node4" },
}
const layouts: Layouts = {
nodes: {
node1: { x: 0, y: 0 },
node2: { x: 100, y: 100 },
node3: { x: 200, y: 0 },
node4: { x: 300, y: 100 },
},
}
export default {
nodes,
edges,
layouts,
}
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
Auto panning on resize
By default, it automatically pans to keep the display area centered when it is resized. To disable this behavior, set configs.view.autoPanOnResize
to false.
<script setup lang="ts">
import { reactive } from "vue"
import * as vNG from "v-network-graph"
import data from "./data"
const configs = reactive(
vNG.defineConfigs({
view: {
autoPanOnResize: true,
},
})
)
</script>
<template>
<div class="demo-control-panel">
<el-checkbox v-model="configs.view.autoPanOnResize"
>Auto panning on resize</el-checkbox
>
</div>
<div class="outer-box">
<div class="resizable">
<div class="handle"></div>
<v-network-graph
ref="graph"
:nodes="data.nodes"
:edges="data.edges"
:layouts="data.layouts"
:configs="configs"
/>
</div>
</div>
</template>
<style scoped>
.outer-box {
margin: 12px;
width: calc(100% - 12px * 2);
height: 400px;
position: relative;
background-color: #aaaaaa;
}
.resizable {
position: relative;
resize: both;
overflow: hidden;
min-width: 100px;
min-height: 100px;
max-width: 100%;
max-height: 100%;
width: 90%;
height: 90%;
border: 1px solid #444444;
background-color: #ffffff;
}
.handle {
position: absolute;
bottom: 0;
right: 0;
width: 0;
height: 0;
border-top: 8px solid transparent;
border-left: 8px solid transparent;
border-right: 8px solid #ff8f8f;
border-bottom: 8px solid #ff8f8f;
pointer-events: none;
}
</style>
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
import { Nodes, Edges, Layouts } from "v-network-graph"
const nodes: Nodes = {
node1: { name: "Node 1" },
node2: { name: "Node 2" },
node3: { name: "Node 3" },
node4: { name: "Node 4" },
}
const edges: Edges = {
edge1: { source: "node1", target: "node2" },
edge2: { source: "node2", target: "node3" },
edge3: { source: "node3", target: "node4" },
}
const layouts: Layouts = {
nodes: {
node1: { x: 0, y: 0 },
node2: { x: 100, y: 100 },
node3: { x: 200, y: 0 },
node4: { x: 300, y: 100 },
},
}
export default {
nodes,
edges,
layouts,
}
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
Get and set the viewing area
The coordinates of the currently displayed area can be get and set. When setting, the zoom and pan to display the specified area will be automatically calculated.
If the aspect ratio of the area to be set differs from that of the <v-network-graph>
element size on the screen, the position will be adjusted so that the center position remains the same.
By using this function, the display position at a certain point in time can be restored later.
<script setup lang="ts">
import { ref, reactive } from "vue"
import * as vNG from "v-network-graph"
import data from "./data"
const configs = reactive(
vNG.defineConfigs({
node: {
normal: { radius: 20 },
label: { visible: false },
},
})
)
const graph = ref<vNG.VNetworkGraphInstance>()
function setViewBoxToRedZone() {
graph.value?.setViewBox({
left: 50,
top: 50,
right: 450,
bottom: 250,
})
}
const savedViewBox = ref<vNG.Box>()
function saveViewBox() {
savedViewBox.value = graph.value?.getViewBox()
}
function restoreViewBox() {
if (!savedViewBox.value) return
graph.value?.setViewBox(savedViewBox.value)
}
</script>
<template>
<div class="demo-control-panel">
<el-button @click="setViewBoxToRedZone">Set viewing area to RED zone</el-button>
<el-button @click="saveViewBox">Save viewing box</el-button>
<template v-if="savedViewBox">
<el-button @click="restoreViewBox">Restore</el-button>
<div class="box-data">
{ top: {{ Math.floor(savedViewBox.top) }}, bottom:
{{ Math.floor(savedViewBox.bottom) }}, left:
{{ Math.floor(savedViewBox.left) }}, right:
{{ Math.floor(savedViewBox.right) }} }
</div>
</template>
</div>
<v-network-graph
ref="graph"
:nodes="data.nodes"
:edges="data.edges"
:layouts="data.layouts"
:configs="configs"
:layers="{ redZone: 'base' }"
>
<!-- Draw a rectangle with a red background -->
<defs>
<component :is="'style'">
<!-- prettier-ignore -->
.point { fill: #ff000080; }
.coords { font-size: 11px; fill: #444; }
.lt { text-anchor: start; dominant-baseline: hanging; transform: translate(5px, 5px); }
.rt { text-anchor: end; dominant-baseline: hanging; transform: translate(-5px, 5px); }
.lb { text-anchor: start; dominant-baseline: ideographic; transform: translate(5px, -5px); }
.rb { text-anchor: end; dominant-baseline: ideographic; transform: translate(-5px, -5px); }
</component>
</defs>
<template #redZone>
<rect x="50" y="50" width="400" height="200" fill="#ff000030" />
<circle cx="50" cy="50" r="5" class="point" />
<circle cx="450" cy="50" r="5" class="point" />
<circle cx="50" cy="250" r="5" class="point" />
<circle cx="450" cy="250" r="5" class="point" />
<text x="50" y="50" class="coords lt">(50, 50)</text>
<text x="450" y="50" class="coords rt">(450, 50)</text>
<text x="50" y="250" class="coords lb">(50, 450)</text>
<text x="450" y="250" class="coords rb">(450, 450)</text>
</template>
</v-network-graph>
</template>
<style scoped>
.box-data {
margin-left: 10px;
font-size: 90%;
color: #444;
}
</style>
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
import { Nodes, Edges, Layouts } from "v-network-graph"
const nodes: Nodes = {
node1: { name: "Node 1" },
node2: { name: "Node 2" },
node3: { name: "Node 3" },
node4: { name: "Node 4" },
}
const edges: Edges = {
edge1: { source: "node1", target: "node2" },
edge2: { source: "node2", target: "node3" },
edge3: { source: "node3", target: "node4" },
}
const layouts: Layouts = {
nodes: {
node1: { x: 100, y: 100 },
node2: { x: 200, y: 200 },
node3: { x: 300, y: 100 },
node4: { x: 400, y: 200 },
},
}
export default {
nodes,
edges,
layouts,
}
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
Specify function in view config
The library does not provide an interface to specify a function for the value of configs.view.*
. However, it is possible to specify a function by using getter of JavaScript.
In the following example, the configuration specifies a function that changes whether panning/zooming is enabled depending on the the state of shift/ctrl key presses.
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue"
import * as vNG from "v-network-graph"
import data from "./data"
const holdingShift = ref(false)
const holdingCtrl = ref(false)
const onKeyDown = (e: KeyboardEvent) => {
if (e.shiftKey) holdingShift.value = true
if (e.ctrlKey) holdingCtrl.value = true
}
const onKeyUp = (e: KeyboardEvent) => {
if (!e.shiftKey) holdingShift.value = false
if (!e.ctrlKey) holdingCtrl.value = false
}
onMounted(() => {
document.addEventListener("keydown", onKeyDown)
document.addEventListener("keyup", onKeyUp)
})
onUnmounted(() => {
document.removeEventListener("keydown", onKeyDown)
document.removeEventListener("keyup", onKeyUp)
})
const configs = vNG.defineConfigs({
view: {
get panEnabled() {
return !holdingShift.value
},
get zoomEnabled() {
return holdingCtrl.value
},
},
})
</script>
<template>
<v-network-graph
:nodes="data.nodes"
:edges="data.edges"
:layouts="data.layouts"
:configs="configs"
/>
<div class="keys">
<div>
<div :class="{ holding: holdingShift }">Shift</div>
Disable panning while holding shift key
</div>
<div>
<div :class="{ holding: holdingCtrl }">Ctrl</div>
Enable zooming while holding ctrl key
</div>
</div>
</template>
<style scoped>
.keys {
position: absolute;
left: 6px;
bottom: 6px;
display: flex;
flex-direction: column;
pointer-events: none;
}
.keys > div {
margin-top: 3px;
display: flex;
flex-direction: row;
align-items: center;
font-size: 12px;
color: #666;
}
.keys > div > div {
margin-right: 4px;
text-align: center;
width: 50px;
padding: 2px;
border-radius: 4px;
border: 1px solid #ddd;
color: #bbb;
background-color: #eee;
}
.keys div.holding {
border-color: #888;
color: #444;
background-color: #ccc;
}
</style>
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
import { Nodes, Edges, Layouts } from "v-network-graph"
const nodes: Nodes = {
node1: { name: "Node 1" },
node2: { name: "Node 2" },
node3: { name: "Node 3" },
node4: { name: "Node 4" },
}
const edges: Edges = {
edge1: { source: "node1", target: "node2" },
edge2: { source: "node2", target: "node3" },
edge3: { source: "node3", target: "node4" },
}
const layouts: Layouts = {
nodes: {
node1: { x: 100, y: 100 },
node2: { x: 200, y: 200 },
node3: { x: 300, y: 100 },
node4: { x: 400, y: 200 },
},
}
export default {
nodes,
edges,
layouts,
}
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
Node expand/collapse
For the implementation of node expand and collapse, the structure of node relationships is a tree structure, which is a type of graph structure. This means that the parent-child relationship needs to be included in the data, and there are restrictions such as not circulating edges in the part of tree structure.
To keep the generality and simplicity of the library, please handle these things on the application.
The following is an example of creating a partially tree-structured graph and passing it to v-network-graph so that the parent node can collapse its child nodes.
<script setup lang="ts">
import { computed, reactive, ref } from "vue"
import * as vNG from "v-network-graph"
import { ForceLayout } from "v-network-graph/lib/force-layout"
import { TreeNodes, TreeNode } from "./data"
import * as data from "./data"
// Nodes containing parent-child relationships
const nodeTree = reactive(data.nodeTree)
// Flatten and hide collapsed nodes
const nodes = computed<TreeNodes>(() => {
const n: TreeNodes = {}
walkExpandedNodes(nodeTree, node => (n[node.id] = node))
return n
})
const edges = reactive(data.edges)
const layouts = reactive<vNG.Layouts>(data.layouts)
const layoutsBackup: vNG.NodePositions = {}
const zoomLevel = ref(1.0)
const configs = reactive(
vNG.defineConfigs<TreeNode>({
view: {
layoutHandler: new ForceLayout(),
},
node: {
normal: {
color: n => (n.children ? "#0000cc" : "#8888aa"),
},
},
})
)
const eventHandlers: vNG.EventHandlers = {
"node:click": ({ node }) => {
// Perform nodes expand/collapse
const children = nodes.value[node]?.children
const parentPos = layouts.nodes[node]
if (children && parentPos) {
// Toggle expand/collapse
nodes.value[node].collapse = !nodes.value[node].collapse
if (nodes.value[node].collapse) {
// Backup position relative to parent node
Object.values(children).forEach(n => {
const pos = layouts.nodes[n.id]
layoutsBackup[n.id] = {
x: pos ? pos.x - parentPos.x : 0,
y: pos ? pos.y - parentPos.y : 0,
}
})
} else {
// Restore position relative to parent node
const z = zoomLevel.value
Object.values(children).forEach((n, i) => {
const pos = layoutsBackup[n.id]
// If no previous position is available, place it at
// a shifted position from the parent's.
layouts.nodes[n.id] = {
x: pos ? pos.x + parentPos.x : parentPos.x + (30 * (i + 1)) / z,
y: pos ? pos.y + parentPos.y : parentPos.y + (30 * (i + 1)) / z,
}
delete layoutsBackup[n.id]
})
}
}
},
}
// Place +/- badge layer over nodes layer
const layers: vNG.Layers = { badge: "nodes" }
function walkExpandedNodes(nodes: TreeNodes, cb: (node: TreeNode) => void) {
for (const n of Object.values(nodes)) {
cb(n)
if (!n.collapse && n.children) {
walkExpandedNodes(n.children, cb)
}
}
}
</script>
<template>
<v-network-graph
class="graph"
:nodes="nodes"
:edges="edges"
:configs="configs"
:layers="layers"
:layouts="layouts"
:event-handlers="eventHandlers"
v-model:zoomLevel="zoomLevel"
>
<!-- +/- badge layer -->
<template #badge="{ scale }">
<template v-for="(pos, node) in layouts.nodes" :key="node">
<g
v-if="nodes[node]?.children"
class="collapse-badge"
:transform="`translate(${pos.x + 9 * scale}, ${pos.y - 9 * scale})`"
>
<circle
:cx="0"
:cy="0"
:r="7 * scale"
:fill="nodes[node].collapse ? '#00cc00' : '#ff5555'"
/>
<text text-anchor="middle" :transform="`scale(${scale})`">
<template v-if="nodes[node].collapse">+</template>
<template v-else>-</template>
</text>
</g>
</template>
</template>
</v-network-graph>
</template>
<style>
.collapse-badge {
pointer-events: none;
}
.collapse-badge text {
font-size: 14px;
stroke: #fff;
text-anchor: middle;
dominant-baseline: middle;
}
</style>
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import * as vNG from "v-network-graph"
export interface TreeNode extends vNG.Node {
id: string
name: string
collapse?: boolean
children?: Record<string, TreeNode>
}
export type TreeNodes = Record<string, TreeNode>
export const nodeTree: TreeNodes = {
groupA: {
id: "groupA",
name: "Group A",
collapse: false,
children: {
node1: {
id: "groupA/node1",
name: "Node 1",
},
node2: {
id: "groupA/node2",
name: "Node 2",
},
node3: {
id: "groupA/node3",
name: "Node 3",
},
},
},
groupB: {
id: "groupB",
name: "Group B",
collapse: false,
children: {
node1: {
id: "groupB/node1",
name: "Node 1",
},
node2: {
id: "groupB/node2",
name: "Node 2",
},
node3: {
id: "groupB/node3",
name: "Node 3",
},
},
},
groupC: {
id: "groupC",
name: "Group C",
collapse: false,
children: {
node1: {
id: "groupC/node1",
name: "Node 1",
},
node2: {
id: "groupC/node2",
name: "Node 2",
},
node3: {
id: "groupC/node3",
name: "Node 3",
},
},
},
}
export const edges: vNG.Edges = {
edge1: { source: "groupA", target: "groupB" },
edge2: { source: "groupB", target: "groupC" },
edge3: { source: "groupC", target: "groupA" },
// Group A
edgeA_1: { source: "groupA", target: "groupA/node1" },
edgeA_2: { source: "groupA", target: "groupA/node2" },
edgeA_3: { source: "groupA", target: "groupA/node3" },
// Group B
edgeB_1: { source: "groupB", target: "groupB/node1" },
edgeB_2: { source: "groupB", target: "groupB/node2" },
edgeB_3: { source: "groupB", target: "groupB/node3" },
// Group C
edgeC_1: { source: "groupC", target: "groupC/node1" },
edgeC_2: { source: "groupC", target: "groupC/node2" },
edgeC_3: { source: "groupC", target: "groupC/node3" },
}
export const layouts: vNG.Layouts = {
nodes: {
"groupA": { x: 0.0, y: -60.0 },
"groupA/node1": { x: -86.8, y: -115.5 },
"groupA/node2": { x: 0.8, y: -163.7 },
"groupA/node3": { x: 87.2, y: -113.3 },
"groupB": { x: -53.0, y: 32.0 },
"groupB/node1": { x: -143.1, y: -15.2 },
"groupB/node2": { x: -140.5, y: 84.7 },
"groupB/node3": { x: -53.4, y: 133.8 },
"groupC": { x: 53.0, y: 32.0 },
"groupC/node1": { x: 140.7, y: -22.1 },
"groupC/node2": { x: 144.3, y: 77.8 },
"groupC/node3": { x: 50.3, y: 132.2 },
},
}
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
Find the shortest path
v-network-graph is intended for drawing graphs, and does not contain any code for graph algorithms. On the other hand, it can be used with any graph algorithm.
The following is an example of drawing the shortest path using Dijkstra's algorithm. The edge label indicates the cost of that edge. v-network-graph's path drawing is directed by a list of edges, so it is converted from a list of nodes to a list of edges at the end.
<script setup lang="ts">
import { ref, watchEffect } from "vue"
import * as vNG from "v-network-graph"
import data from "./data"
// -------------------------------------------------------------
// Dijkstra route finder
// ref. https://github.com/andrewhayward/dijkstra
// -------------------------------------------------------------
type GraphMap = { [key: string]: { [key: string]: number } }
type EdgeMap = { [key: string]: { [key: string]: string } }
class Graph {
map: GraphMap
edgeMap: EdgeMap
_sorter = function (a: string, b: string) {
return parseFloat(a) - parseFloat(b)
}
constructor(edges: vNG.Edges) {
// var map = {a:{b:3,c:1},b:{a:2,c:1},c:{a:4,b:1}},
const map: GraphMap = {}
const edgeMap: EdgeMap = {}
Object.entries(edges).forEach(([edgeId, edge]) => {
const source = edge.source
const target = edge.target
const cost = edge?.cost ?? 1
if (!map[source]) map[source] = {}
if (!map[target]) map[target] = {}
if (!edgeMap[source]) edgeMap[source] = {}
if (!edgeMap[target]) edgeMap[target] = {}
if (map[source][target]) {
if (map[source][target] > cost) {
map[source][target] = cost
map[target][source] = cost
edgeMap[source][target] = edgeId
edgeMap[target][source] = edgeId
}
} else {
map[source][target] = cost
map[target][source] = cost
edgeMap[source][target] = edgeId
edgeMap[target][source] = edgeId
}
})
this.map = map
this.edgeMap = edgeMap
}
findShortestPath(viaNodes: string[]) {
return this._findShortestPath(this.map, viaNodes)
}
convertNodesToEdges(nodes: string[]): string[] {
const edges: string[] = []
if (nodes.length === 0) {
return []
}
let prev = nodes[0]
for (let i = 1; i < nodes.length; i++) {
const next = nodes[i]
edges.push(this.edgeMap[prev][next])
prev = next
}
return edges
}
_extractKeys(obj: object) {
const keys = []
let key
for (key in obj) {
Object.prototype.hasOwnProperty.call(obj, key) && keys.push(key)
}
return keys
}
_findPaths(map: GraphMap, start: string, end: string) {
const costs: { [key: string]: number } = {}
const open: { [key: string]: string[] } = { 0: [start] }
const predecessors: { [key: string]: string } = {}
let keys
const addToOpen = function (cost: number, vertex: string) {
const key = "" + cost
if (!open[key]) {
open[key] = []
}
open[key].push(vertex)
}
costs[start] = 0
// eslint-disable-next-line no-unmodified-loop-condition
while (open) {
if (!(keys = this._extractKeys(open)).length) {
break
}
keys.sort(this._sorter)
const key = keys[0]
const bucket = open[key]
const node = bucket.shift() || ""
const currentCost = parseFloat(key)
const adjacentNodes = map[node] || {}
if (!bucket.length) {
delete open[key]
}
for (const vertex in adjacentNodes) {
if (Object.prototype.hasOwnProperty.call(adjacentNodes, vertex)) {
const cost = adjacentNodes[vertex]
const totalCost = cost + currentCost
const vertexCost = costs[vertex]
if (vertexCost === undefined || vertexCost > totalCost) {
costs[vertex] = totalCost
addToOpen(totalCost, vertex)
predecessors[vertex] = node
}
}
}
}
if (costs[end] === undefined) {
return null
} else {
return predecessors
}
}
_extractShortest(predecessors: { [key: string]: string }, end: string) {
const nodes = []
let u = end
while (u !== undefined) {
nodes.push(u)
u = predecessors[u]
}
nodes.reverse()
return nodes
}
_findShortestPath(map: GraphMap, nodes: string[]) {
nodes = [...nodes] // copy
let start = nodes.shift() || ""
let end: string
let predecessors
const path: string[] = []
let shortest
while (nodes.length) {
end = nodes.shift() || ""
predecessors = this._findPaths(map, start, end)
if (predecessors) {
shortest = this._extractShortest(predecessors, end)
if (nodes.length) {
path.push.apply(path, shortest.slice(0, -1))
} else {
return path.concat(shortest)
}
} else {
return null
}
start = end
}
}
}
const paths = ref<vNG.Paths>({})
const targetNode = ref("node12")
watchEffect(() => {
const graph = new Graph(data.edges)
const routeOfNodes = graph.findShortestPath(["node1", targetNode.value])
if (routeOfNodes) {
const routeOfEdges = graph.convertNodesToEdges(routeOfNodes)
paths.value = { shortestPath: { edges: routeOfEdges } }
}
})
const eventHandlers: vNG.EventHandlers = {
"node:pointerover": ({ node }) => {
if (node === "node1") return
targetNode.value = node
},
}
</script>
<template>
<v-network-graph
:nodes="data.nodes"
:edges="data.edges"
:paths="paths"
:layouts="data.layouts"
:configs="data.configs"
:event-handlers="eventHandlers"
>
<template #edge-label="{ edge, ...slotProps }">
<v-edge-label
:text="`${edge.cost}`"
align="center"
vertical-align="below"
v-bind="slotProps"
/>
</template>
</v-network-graph>
</template>
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
import { Nodes, Edges, Layouts, defineConfigs } from "v-network-graph"
const nodes: Nodes = {
node1: { name: "Source" },
node2: { name: "" },
node3: { name: "" },
node4: { name: "" },
node5: { name: "" },
node6: { name: "" },
node7: { name: "" },
node8: { name: "" },
node9: { name: "" },
node10: { name: "" },
node11: { name: "" },
node12: { name: "" },
}
const edges: Edges = {
edge1: { source: "node1", target: "node2", cost: 10 },
edge2: { source: "node1", target: "node3", cost: 5 },
edge3: { source: "node2", target: "node4", cost: 10 },
edge4: { source: "node2", target: "node5", cost: 10 },
edge5: { source: "node3", target: "node5", cost: 5 },
edge6: { source: "node3", target: "node6", cost: 10 },
edge7: { source: "node4", target: "node7", cost: 10 },
edge8: { source: "node5", target: "node8", cost: 5 },
edge9: { source: "node6", target: "node9", cost: 10 },
edge10: { source: "node7", target: "node10", cost: 5 },
edge11: { source: "node8", target: "node10", cost: 5 },
edge12: { source: "node8", target: "node11", cost: 10 },
edge13: { source: "node9", target: "node11", cost: 10 },
edge14: { source: "node10", target: "node12", cost: 10 },
edge15: { source: "node11", target: "node12", cost: 10 },
}
const layouts: Layouts = {
nodes: {
node1: { x: 0, y: 100 },
node2: { x: 75, y: 50 },
node3: { x: 75, y: 150 },
node4: { x: 150, y: 0 },
node5: { x: 150, y: 100 },
node6: { x: 150, y: 200 },
node7: { x: 250, y: 0 },
node8: { x: 250, y: 100 },
node9: { x: 250, y: 200 },
node10: { x: 325, y: 50 },
node11: { x: 325, y: 150 },
node12: { x: 400, y: 100 },
},
}
const configs = defineConfigs({
node: {
label: {
visible: n => !!n.name,
},
},
path: {
visible: true,
curveInNode: true,
path: {
width: 14,
color: "#ff000088",
},
},
})
export default {
nodes,
edges,
layouts,
configs,
}
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