Skip to content

Commit a8ba973

Browse files
authored
examples: add math example (#1461)
* examples: add math example * docs: add math example
1 parent f74be96 commit a8ba973

23 files changed

+748
-11
lines changed

docs/examples/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { SnapToHandleApp, SnappableConnectionLine } from './connection-radius'
1919
import { NodeResizerApp, ResizableNode } from './node-resizer'
2020
import { ToolbarApp, ToolbarNode } from './node-toolbar'
2121
import { LayoutApp, LayoutEdge, LayoutElements, LayoutIcon, LayoutNode, useLayout, useRunProcess, useShuffle } from './layout'
22+
import { MathApp, MathCSS, MathElements, MathIcon, MathOperatorNode, MathResultNode, MathValueNode } from './math'
2223

2324
export const exampleImports = {
2425
basic: {
@@ -140,4 +141,13 @@ export const exampleImports = {
140141
'@dagrejs/dagre': 'https://cdn.jsdelivr.net/npm/@dagrejs/[email protected]/+esm',
141142
},
142143
},
144+
math: {
145+
'App.vue': MathApp,
146+
'ValueNode.vue': MathValueNode,
147+
'OperatorNode.vue': MathOperatorNode,
148+
'ResultNode.vue': MathResultNode,
149+
'Icon.vue': MathIcon,
150+
'style.css': MathCSS,
151+
'initial-elements.js': MathElements,
152+
},
143153
}

docs/examples/math/App.vue

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<script setup>
2+
import { ref } from 'vue'
3+
import { VueFlow } from '@vue-flow/core'
4+
import { Background } from '@vue-flow/background'
5+
import { initialEdges, initialNodes } from './initial-elements.js'
6+
import ValueNode from './ValueNode.vue'
7+
import OperatorNode from './OperatorNode.vue'
8+
import ResultNode from './ResultNode.vue'
9+
10+
const nodes = ref(initialNodes)
11+
12+
const edges = ref(initialEdges)
13+
</script>
14+
15+
<template>
16+
<VueFlow class="math-flow" :nodes="nodes" :edges="edges" fit-view-on-init>
17+
<template #node-value="props">
18+
<ValueNode :id="props.id" :data="props.data" />
19+
</template>
20+
21+
<template #node-operator="props">
22+
<OperatorNode :id="props.id" :data="props.data" />
23+
</template>
24+
25+
<template #node-result="props">
26+
<ResultNode :id="props.id" />
27+
</template>
28+
29+
<Background />
30+
</VueFlow>
31+
</template>

docs/examples/math/Icon.vue

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<script setup>
2+
defineProps(['name'])
3+
</script>
4+
5+
<template>
6+
<svg v-if="name === '+'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
7+
<path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6z" />
8+
</svg>
9+
10+
<svg v-else-if="name === '-'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
11+
<path fill="currentColor" d="M19 13H5v-2h14v2z" />
12+
</svg>
13+
14+
<svg v-else-if="name === '*'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
15+
<path
16+
fill="none"
17+
stroke="currentColor"
18+
stroke-linecap="round"
19+
stroke-linejoin="round"
20+
stroke-width="2"
21+
d="M12 12L6 6m6 6l6 6m-6-6l6-6m-6 6l-6 6"
22+
/>
23+
</svg>
24+
25+
<svg v-else-if="name === '/'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
26+
<path
27+
fill="currentColor"
28+
d="M19 13H5v-2h14zm-7-8a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0 10a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2"
29+
/>
30+
</svg>
31+
</template>

docs/examples/math/OperatorNode.vue

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<script setup>
2+
import { Handle, Position, useVueFlow } from '@vue-flow/core'
3+
import Icon from './Icon.vue'
4+
5+
const props = defineProps(['id', 'data'])
6+
7+
const operators = ['+', '-', '*', '/']
8+
9+
const { updateNodeData } = useVueFlow()
10+
</script>
11+
12+
<template>
13+
<div class="buttons nodrag">
14+
<button
15+
v-for="operator of operators"
16+
:key="`${id}-${operator}-operator`"
17+
:class="{ selected: data.operator === operator }"
18+
@click="updateNodeData(props.id, { operator })"
19+
>
20+
<Icon :name="operator" />
21+
</button>
22+
</div>
23+
24+
<Handle type="source" :position="Position.Right" :connectable="false" />
25+
<Handle id="target-a" type="target" :position="Position.Left" :connectable="false" />
26+
<Handle id="target-b" type="target" :position="Position.Left" :connectable="false" />
27+
</template>

docs/examples/math/ResultNode.vue

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<script setup>
2+
import { computed } from 'vue'
3+
import { Handle, Position, useHandleConnections, useNodesData, useVueFlow } from '@vue-flow/core'
4+
5+
defineProps(['id'])
6+
7+
const mathFunctions = {
8+
'+': (a, b) => a + b,
9+
'-': (a, b) => a - b,
10+
'*': (a, b) => a * b,
11+
'/': (a, b) => a / b,
12+
}
13+
14+
const { getConnectedEdges } = useVueFlow()
15+
16+
// Get the source connections of the result node. In this example it's only one operator node.
17+
const sourceConnections = useHandleConnections({
18+
// type target means all connections where *this* node is the target
19+
// that means we go backwards in the graph to find the source of the connection(s)
20+
type: 'target',
21+
})
22+
23+
// Get the source connections of the operator node
24+
const operatorSourceConnections = computed(() =>
25+
getConnectedEdges(sourceConnections.value[0].source).filter((e) => e.source !== sourceConnections.value[0].source),
26+
)
27+
28+
const operatorData = useNodesData(() => sourceConnections.value.map((connection) => connection.source))
29+
30+
const valueData = useNodesData(() => operatorSourceConnections.value.map((connection) => connection.source))
31+
32+
const result = computed(() => {
33+
const currResult = operatorData.value.reduce((acc, { data }) => {
34+
const operator = data?.operator
35+
36+
if (operator) {
37+
const [a, b] = valueData.value.map(({ data }) => data?.value)
38+
39+
if (a && b) {
40+
return mathFunctions[operator](a, b)
41+
}
42+
}
43+
44+
return acc
45+
}, 0)
46+
47+
// Round to 2 decimal places
48+
return Math.round(currResult * 100) / 100
49+
})
50+
</script>
51+
52+
<template>
53+
<div class="calculation">
54+
<template v-for="(value, i) in valueData" :key="`${value.id}-${value.data}`">
55+
<span>
56+
{{ value.data?.value }}
57+
</span>
58+
59+
<span v-if="i !== valueData.length - 1">
60+
{{ operatorData[0].data?.operator }}
61+
</span>
62+
</template>
63+
</div>
64+
65+
<span> = </span>
66+
67+
<span class="counter" :style="{ color: result > 0 ? '#5EC697' : '#f15a16' }">
68+
{{ result }}
69+
</span>
70+
71+
<Handle
72+
type="target"
73+
:position="Position.Left"
74+
:connectable="false"
75+
:style="{ background: result > 0 ? '#5EC697' : '#f15a16' }"
76+
/>
77+
</template>

docs/examples/math/ValueNode.vue

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script setup>
2+
import { Handle, Position, useVueFlow } from '@vue-flow/core'
3+
4+
const props = defineProps(['id', 'data'])
5+
6+
const { updateNodeData } = useVueFlow()
7+
8+
function onChange(event) {
9+
const value = Number.parseFloat(event.target.value)
10+
11+
if (!Number.isNaN(value)) {
12+
updateNodeData(props.id, { value })
13+
}
14+
}
15+
</script>
16+
17+
<template>
18+
<label :for="`${id}-input`"> </label>
19+
<input :id="`${id}-input`" :value="data.value" type="number" class="nodrag" @change="onChange" />
20+
21+
<Handle type="source" :position="Position.Right" :connectable="false" />
22+
</template>

docs/examples/math/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export { default as MathApp } from './App.vue?raw'
2+
export { default as MathElements } from './initial-elements.js?raw'
3+
export { default as MathValueNode } from './ValueNode.vue?raw'
4+
export { default as MathOperatorNode } from './OperatorNode.vue?raw'
5+
export { default as MathResultNode } from './ResultNode.vue?raw'
6+
export { default as MathIcon } from './Icon.vue?raw'
7+
export { default as MathCSS } from './style.css?inline'
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export const initialNodes = [
2+
{
3+
id: '1',
4+
position: { x: 0, y: 0 },
5+
type: 'value',
6+
data: { value: 10 },
7+
},
8+
{
9+
id: '2',
10+
position: { x: 0, y: 100 },
11+
type: 'value',
12+
data: { value: 30 },
13+
},
14+
{
15+
id: '3',
16+
position: { x: 300, y: 35 },
17+
type: 'operator',
18+
data: { operator: '+' },
19+
},
20+
{
21+
id: '4',
22+
position: { x: 650, y: 15 },
23+
type: 'result',
24+
},
25+
]
26+
27+
export const initialEdges = [
28+
{ id: 'e1-3', source: '1', target: '3', animated: true, targetHandle: 'target-a' },
29+
{ id: 'e2-3', source: '2', target: '3', animated: true, targetHandle: 'target-b' },
30+
{ id: 'e3-4', source: '3', target: '4', animated: true },
31+
]

docs/examples/math/style.css

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
.math-flow {
2+
background-color: #edf2f7;
3+
height: 100%;
4+
width: 100%;
5+
}
6+
7+
.vue-flow__handle {
8+
height: 24px;
9+
width: 10px;
10+
background: #aaa;
11+
border-radius: 4px
12+
}
13+
14+
.vue-flow__edges path {
15+
stroke-width: 3;
16+
}
17+
18+
.vue-flow__node-value {
19+
display: flex;
20+
align-items: center;
21+
gap: 8px;
22+
padding: 8px 16px;
23+
background-color: #f3f4f6;
24+
border-radius: 8px;
25+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
26+
}
27+
28+
.vue-flow__node-value.selected {
29+
box-shadow: 0 0 0 2px #ec4899;
30+
}
31+
32+
.vue-flow__node-value input {
33+
flex: 1;
34+
padding: 8px;
35+
border: none;
36+
border-radius: 8px;
37+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
38+
}
39+
40+
.vue-flow__node-value input:focus {
41+
outline: none;
42+
}
43+
44+
.vue-flow__node-value .vue-flow__handle {
45+
background-color: #ec4899;
46+
}
47+
48+
.vue-flow__node-operator {
49+
display: flex;
50+
flex-direction: column;
51+
align-items: center;
52+
gap: 8px;
53+
padding: 16px 24px;
54+
background-color: #f3f4f6;
55+
border-radius: 8px;
56+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
57+
}
58+
59+
.vue-flow__node-operator.selected {
60+
box-shadow: 0 0 0 2px #0EA5E9;
61+
}
62+
63+
.vue-flow__node-operator .buttons {
64+
display: flex;
65+
gap: 8px;
66+
}
67+
68+
.vue-flow__node-operator button {
69+
border: none;
70+
cursor: pointer;
71+
background-color: #4a5568;
72+
border-radius: 8px;
73+
color: white;
74+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
75+
width: 40px;
76+
height: 40px;
77+
font-size: 16px;
78+
display: flex;
79+
align-items: center;
80+
justify-content: center;
81+
}
82+
83+
.vue-flow__node-operator button:hover {
84+
background-color: #0EA5E9;
85+
transition: background-color 0.2s;
86+
}
87+
88+
.vue-flow__node-operator button.selected {
89+
background-color: #0EA5E9;
90+
}
91+
92+
.vue-flow__node-operator .vue-flow__handle[data-handleid="target-a"] {
93+
top: 25%;
94+
}
95+
96+
.vue-flow__node-operator .vue-flow__handle[data-handleid="target-b"] {
97+
top: 75%;
98+
}
99+
100+
.vue-flow__node-operator .vue-flow__handle {
101+
background-color: #0EA5E9;
102+
}
103+
104+
.vue-flow__node-result {
105+
display: flex;
106+
flex-direction: column;
107+
align-items: center;
108+
gap: 8px;
109+
padding: 16px 24px;
110+
background-color: #f3f4f6;
111+
border-radius: 8px;
112+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
113+
}
114+
115+
.vue-flow__node-result.selected {
116+
box-shadow: 0 0 0 2px #5EC697;
117+
}
118+
119+
.vue-flow__node-result .calculation {
120+
display: flex;
121+
gap: 8px;
122+
font-size: 24px;
123+
}

docs/src/.vitepress/config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ export default defineConfigWithTheme<DefaultTheme.Config>({
206206
{ text: 'Drag & Drop', link: '/examples/dnd' },
207207
{ text: 'Interactions', link: '/examples/interaction' },
208208
{ text: 'Save & Restore', link: '/examples/save' },
209+
{ text: 'Math Operation Flow', link: '/examples/math' },
209210
{ text: 'Screenshot', link: '/examples/screenshot' },
210211
{ text: 'Node Visibility', link: '/examples/hidden' },
211212
{ text: 'Node Intersections', link: '/examples/intersection' },

0 commit comments

Comments
 (0)