docs(examples): add simple layout example (#1678)

* docs(examples): cleanup animated layout example

Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com>

* chore(docs,deps): update deps

Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com>

* docs(examples): add animated and simple layout examples

Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com>

---------

Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com>
This commit is contained in:
Braks
2024-11-05 15:53:16 +03:00
committed by GitHub
parent f3075bc246
commit 61f4b0d447
13 changed files with 959 additions and 363 deletions

View File

@@ -19,6 +19,8 @@ import { SnapToHandleApp, SnappableConnectionLine } from './connection-radius'
import { NodeResizerApp, ResizableNode } from './node-resizer'
import { ToolbarApp, ToolbarNode } from './node-toolbar'
import { LayoutApp, LayoutEdge, LayoutElements, LayoutIcon, LayoutNode, useLayout, useRunProcess, useShuffle } from './layout'
import { SimpleLayoutApp, SimpleLayoutElements, SimpleLayoutIcon, useSimpleLayout } from './layout-simple'
import { MathApp, MathCSS, MathElements, MathIcon, MathOperatorNode, MathResultNode, MathValueNode } from './math'
import { ConfirmApp, ConfirmDialog, useDialog } from './confirm-delete'
@@ -140,6 +142,15 @@ export const exampleImports = {
'@dagrejs/dagre': 'https://cdn.jsdelivr.net/npm/@dagrejs/dagre@1.1.2/+esm',
},
},
layoutSimple: {
'App.vue': SimpleLayoutApp,
'initial-elements.js': SimpleLayoutElements,
'useLayout.js': useSimpleLayout,
'Icon.vue': SimpleLayoutIcon,
'additionalImports': {
'@dagrejs/dagre': 'https://cdn.jsdelivr.net/npm/@dagrejs/dagre@1.1.2/+esm',
},
},
math: {
'App.vue': MathApp,
'ValueNode.vue': MathValueNode,

View File

@@ -0,0 +1,133 @@
<script setup>
import { nextTick, ref } from 'vue'
import { Panel, VueFlow, useVueFlow } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import Icon from './Icon.vue'
import { initialEdges, initialNodes } from './initial-elements.js'
import { useLayout } from './useLayout'
const nodes = ref(initialNodes)
const edges = ref(initialEdges)
const { layout } = useLayout()
const { fitView } = useVueFlow()
async function layoutGraph(direction) {
nodes.value = layout(nodes.value, edges.value, direction)
nextTick(() => {
fitView()
})
}
</script>
<template>
<div class="layout-flow">
<VueFlow :nodes="nodes" :edges="edges" @nodes-initialized="layoutGraph('LR')">
<Background />
<Panel class="process-panel" position="top-right">
<div class="layout-panel">
<button title="set horizontal layout" @click="layoutGraph('LR')">
<Icon name="horizontal" />
</button>
<button title="set vertical layout" @click="layoutGraph('TB')">
<Icon name="vertical" />
</button>
</div>
</Panel>
</VueFlow>
</div>
</template>
<style>
.layout-flow {
background-color: #1a192b;
height: 100%;
width: 100%;
}
.process-panel,
.layout-panel {
display: flex;
gap: 10px;
}
.process-panel {
background-color: #2d3748;
padding: 10px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
}
.process-panel button {
border: none;
cursor: pointer;
background-color: #4a5568;
border-radius: 8px;
color: white;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}
.process-panel button {
font-size: 16px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.checkbox-panel {
display: flex;
align-items: center;
gap: 10px;
}
.process-panel button:hover,
.layout-panel button:hover {
background-color: #2563eb;
transition: background-color 0.2s;
}
.process-panel label {
color: white;
font-size: 12px;
}
.stop-btn svg {
display: none;
}
.stop-btn:hover svg {
display: block;
}
.stop-btn:hover .spinner {
display: none;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #2563eb;
border-radius: 50%;
width: 10px;
height: 10px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,40 @@
<script setup>
defineProps({
name: {
type: String,
required: true,
},
})
</script>
<template>
<svg v-if="name === 'play'" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M8 5v14l11-7z" fill="currentColor" />
</svg>
<svg v-else-if="name === 'stop'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M8 16h8V8H8zm4 6q-2.075 0-3.9-.788t-3.175-2.137q-1.35-1.35-2.137-3.175T2 12q0-2.075.788-3.9t2.137-3.175q1.35-1.35 3.175-2.137T12 2q2.075 0 3.9.788t3.175 2.137q1.35 1.35 2.138 3.175T22 12q0 2.075-.788 3.9t-2.137 3.175q-1.35 1.35-3.175 2.138T12 22"
/>
</svg>
<svg v-else-if="name === 'horizontal'" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M2,12 L22,12" stroke="currentColor" stroke-width="2" />
<path d="M7,7 L2,12 L7,17" stroke="currentColor" stroke-width="2" fill="none" />
<path d="M17,7 L22,12 L17,17" stroke="currentColor" stroke-width="2" fill="none" />
</svg>
<svg v-else-if="name === 'vertical'" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12,2 L12,22" stroke="currentColor" stroke-width="2" />
<path d="M7,7 L12,2 L17,7" stroke="currentColor" stroke-width="2" fill="none" />
<path d="M7,17 L12,22 L17,17" stroke="currentColor" stroke-width="2" fill="none" />
</svg>
<svg v-else-if="name === 'shuffle'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M14 20v-2h2.6l-3.175-3.175L14.85 13.4L18 16.55V14h2v6zm-8.6 0L4 18.6L16.6 6H14V4h6v6h-2V7.4zm3.775-9.425L4 5.4L5.4 4l5.175 5.175z"
/>
</svg>
</template>

View File

@@ -0,0 +1,4 @@
export { default as SimpleLayoutApp } from './App.vue?raw'
export { default as SimpleLayoutElements } from './initial-elements.js?raw'
export { default as useSimpleLayout } from './useLayout.js?raw'
export { default as SimpleLayoutIcon } from './Icon.vue?raw'

View File

@@ -0,0 +1,94 @@
const position = { x: 0, y: 0 }
export const initialNodes = [
{
id: '1',
position,
data: {
label: 'Node 1',
},
},
{
id: '2',
position,
data: {
label: 'Node 2',
},
},
{
id: '2a',
position,
data: {
label: 'Node 2a',
},
},
{
id: '2b',
position,
data: {
label: 'Node 2b',
},
},
{
id: '2c',
position,
data: {
label: 'Node 2c',
},
},
{
id: '2d',
position,
data: {
label: 'Node 2d',
},
},
{
id: '3',
position,
data: {
label: 'Node 3',
},
},
{
id: '4',
position,
data: {
label: 'Node 4',
},
},
{
id: '5',
position,
data: {
label: 'Node 5',
},
},
{
id: '6',
position,
data: {
label: 'Node 6',
},
},
{
id: '7',
position,
data: {
label: 'Node 7',
},
},
]
export const initialEdges = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e2-2a', source: '2', target: '2a' },
{ id: 'e2-2b', source: '2', target: '2b' },
{ id: 'e2-2c', source: '2', target: '2c' },
{ id: 'e2c-2d', source: '2c', target: '2d' },
{ id: 'e3-7', source: '3', target: '4' },
{ id: 'e4-5', source: '4', target: '5' },
{ id: 'e5-6', source: '5', target: '6' },
{ id: 'e5-7', source: '5', target: '7' },
]

View File

@@ -0,0 +1,56 @@
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 || 150, height: graphNode.dimensions.height || 50 })
}
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 },
}
})
}
return { graph, layout, previousDirection }
}

View File

@@ -52,7 +52,12 @@ async function layoutGraph(direction) {
<template>
<div class="layout-flow">
<VueFlow :nodes="nodes" :edges="edges" @nodes-initialized="layoutGraph('LR')">
<VueFlow
:nodes="nodes"
:edges="edges"
:default-edge-options="{ type: 'animation', animated: true }"
@nodes-initialized="layoutGraph('LR')"
>
<template #node-process="props">
<ProcessNode :data="props.data" :source-position="props.sourcePosition" :target-position="props.targetPosition" />
</template>

View File

@@ -1,6 +1,5 @@
const position = { x: 0, y: 0 }
const nodeType = 'process'
const edgeType = 'animation'
export const initialNodes = [
{
@@ -61,14 +60,14 @@ export const initialNodes = [
]
export const initialEdges = [
{ id: 'e1-2', source: '1', target: '2', type: edgeType, animated: true },
{ id: 'e1-3', source: '1', target: '3', type: edgeType, animated: true },
{ id: 'e2-2a', source: '2', target: '2a', type: edgeType, animated: true },
{ id: 'e2-2b', source: '2', target: '2b', type: edgeType, animated: true },
{ id: 'e2-2c', source: '2', target: '2c', type: edgeType, animated: true },
{ id: 'e2c-2d', source: '2c', target: '2d', type: edgeType, animated: true },
{ id: 'e3-7', source: '3', target: '4', type: edgeType, animated: true },
{ id: 'e4-5', source: '4', target: '5', type: edgeType, animated: true },
{ id: 'e5-6', source: '5', target: '6', type: edgeType, animated: true },
{ id: 'e5-7', source: '5', target: '7', type: edgeType, animated: true },
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e2-2a', source: '2', target: '2a' },
{ id: 'e2-2b', source: '2', target: '2b' },
{ id: 'e2-2c', source: '2', target: '2c' },
{ id: 'e2c-2d', source: '2c', target: '2d' },
{ id: 'e3-7', source: '3', target: '4' },
{ id: 'e4-5', source: '4', target: '5' },
{ id: 'e5-6', source: '5', target: '6' },
{ id: 'e5-7', source: '5', target: '7' },
]

View File

@@ -11,10 +11,10 @@
"typedocs": "typedoc --options ./typedoc.json"
},
"dependencies": {
"@algolia/client-search": "^5.7.0",
"@algolia/client-search": "^5.12.0",
"@stackblitz/sdk": "^1.11.0",
"@vercel/analytics": "^1.3.1",
"@vercel/speed-insights": "^1.0.12",
"@vercel/analytics": "^1.3.2",
"@vercel/speed-insights": "^1.0.14",
"@vue-flow/background": "workspace:*",
"@vue-flow/controls": "workspace:*",
"@vue-flow/core": "workspace:*",
@@ -23,24 +23,24 @@
"@vue-flow/node-toolbar": "workspace:*",
"@vue/repl": "3.4.0",
"blobity": "^0.2.3",
"vue": "^3.5.11",
"web-vitals": "^4.2.3"
"vue": "^3.5.12",
"web-vitals": "^4.2.4"
},
"devDependencies": {
"@iconify/json": "^2.2.256",
"@iconify/json": "^2.2.268",
"@tooling/eslint-config": "workspace:*",
"@tooling/tsconfig": "workspace:*",
"@windicss/plugin-scrollbar": "^1.2.3",
"dotenv": "^16.4.5",
"ohmyfetch": "^0.4.21",
"typedoc": "^0.26.7",
"typedoc-plugin-markdown": "^4.2.9",
"typedoc-plugin-merge-modules": "^6.0.1",
"typedoc": "^0.26.11",
"typedoc-plugin-markdown": "^4.2.10",
"typedoc-plugin-merge-modules": "^6.0.3",
"unplugin-auto-import": "^0.18.3",
"unplugin-icons": "^0.19.3",
"unplugin-icons": "^0.20.0",
"unplugin-vue-components": "^0.27.4",
"vite-plugin-windicss": "^1.9.3",
"vitepress": "^1.3.4",
"vitepress": "^1.5.0",
"windicss": "^3.5.6"
}
}

View File

@@ -202,7 +202,6 @@ export default defineConfigWithTheme<DefaultTheme.Config>({
collapsed: false,
items: [
{ text: 'Basic', link: '/examples/' },
{ text: 'Layouting', link: '/examples/layout' },
{ text: 'Drag & Drop', link: '/examples/dnd' },
{ text: 'Interactions', link: '/examples/interaction' },
{ text: 'Save & Restore', link: '/examples/save' },
@@ -217,6 +216,14 @@ export default defineConfigWithTheme<DefaultTheme.Config>({
{ text: 'Stress Test', link: '/examples/stress' },
],
},
{
text: 'Layout',
collapsed: false,
items: [
{ text: 'Simple Layout', link: '/examples/layout/simple' },
{ text: 'Animation & Layout', link: '/examples/layout/animated' },
],
},
{
text: 'Nodes',
collapsed: false,

View File

@@ -0,0 +1,10 @@
# Animation and Layout
In this example we are going to use [dagre](https://github.com/dagrejs/dagre) to layout our nodes and also implement
some animations
to demonstrate execution from one node to another in a sequence.
<div class="mt-6">
<Repl example="layout"></Repl>
</div>

View File

@@ -1,4 +1,4 @@
# Layouting
# Layout
A lot of the time, you'll want to layout your nodes automatically instead of assigning their positions manually.
Vue Flow does *not* have a built-in layouting system, but it's easy to use a third-party library to achieve this.
@@ -6,5 +6,5 @@ Vue Flow does *not* have a built-in layouting system, but it's easy to use a thi
In this example we are going to use [dagre](https://github.com/dagrejs/dagre) to layout our nodes.
<div class="mt-6">
<Repl example="layout"></Repl>
<Repl example="layoutSimple"></Repl>
</div>

911
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff