examples: add math example (#1461)

* examples: add math example

* docs: add math example
This commit is contained in:
Braks
2024-06-08 20:35:18 +02:00
committed by GitHub
parent f74be96b2d
commit a8ba97331f
23 changed files with 748 additions and 11 deletions
+10
View File
@@ -19,6 +19,7 @@ import { SnapToHandleApp, SnappableConnectionLine } from './connection-radius'
import { NodeResizerApp, ResizableNode } from './node-resizer' import { NodeResizerApp, ResizableNode } from './node-resizer'
import { ToolbarApp, ToolbarNode } from './node-toolbar' import { ToolbarApp, ToolbarNode } from './node-toolbar'
import { LayoutApp, LayoutEdge, LayoutElements, LayoutIcon, LayoutNode, useLayout, useRunProcess, useShuffle } from './layout' import { LayoutApp, LayoutEdge, LayoutElements, LayoutIcon, LayoutNode, useLayout, useRunProcess, useShuffle } from './layout'
import { MathApp, MathCSS, MathElements, MathIcon, MathOperatorNode, MathResultNode, MathValueNode } from './math'
export const exampleImports = { export const exampleImports = {
basic: { basic: {
@@ -140,4 +141,13 @@ export const exampleImports = {
'@dagrejs/dagre': 'https://cdn.jsdelivr.net/npm/@dagrejs/dagre@1.1.2/+esm', '@dagrejs/dagre': 'https://cdn.jsdelivr.net/npm/@dagrejs/dagre@1.1.2/+esm',
}, },
}, },
math: {
'App.vue': MathApp,
'ValueNode.vue': MathValueNode,
'OperatorNode.vue': MathOperatorNode,
'ResultNode.vue': MathResultNode,
'Icon.vue': MathIcon,
'style.css': MathCSS,
'initial-elements.js': MathElements,
},
} }
+31
View File
@@ -0,0 +1,31 @@
<script setup>
import { ref } from 'vue'
import { VueFlow } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import { initialEdges, initialNodes } from './initial-elements.js'
import ValueNode from './ValueNode.vue'
import OperatorNode from './OperatorNode.vue'
import ResultNode from './ResultNode.vue'
const nodes = ref(initialNodes)
const edges = ref(initialEdges)
</script>
<template>
<VueFlow class="math-flow" :nodes="nodes" :edges="edges" fit-view-on-init>
<template #node-value="props">
<ValueNode :id="props.id" :data="props.data" />
</template>
<template #node-operator="props">
<OperatorNode :id="props.id" :data="props.data" />
</template>
<template #node-result="props">
<ResultNode :id="props.id" />
</template>
<Background />
</VueFlow>
</template>
+31
View File
@@ -0,0 +1,31 @@
<script setup>
defineProps(['name'])
</script>
<template>
<svg v-if="name === '+'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6z" />
</svg>
<svg v-else-if="name === '-'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="M19 13H5v-2h14v2z" />
</svg>
<svg v-else-if="name === '*'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 12L6 6m6 6l6 6m-6-6l6-6m-6 6l-6 6"
/>
</svg>
<svg v-else-if="name === '/'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="currentColor"
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"
/>
</svg>
</template>
+27
View File
@@ -0,0 +1,27 @@
<script setup>
import { Handle, Position, useVueFlow } from '@vue-flow/core'
import Icon from './Icon.vue'
const props = defineProps(['id', 'data'])
const operators = ['+', '-', '*', '/']
const { updateNodeData } = useVueFlow()
</script>
<template>
<div class="buttons nodrag">
<button
v-for="operator of operators"
:key="`${id}-${operator}-operator`"
:class="{ selected: data.operator === operator }"
@click="updateNodeData(props.id, { operator })"
>
<Icon :name="operator" />
</button>
</div>
<Handle type="source" :position="Position.Right" :connectable="false" />
<Handle id="target-a" type="target" :position="Position.Left" :connectable="false" />
<Handle id="target-b" type="target" :position="Position.Left" :connectable="false" />
</template>
+77
View File
@@ -0,0 +1,77 @@
<script setup>
import { computed } from 'vue'
import { Handle, Position, useHandleConnections, useNodesData, useVueFlow } from '@vue-flow/core'
defineProps(['id'])
const mathFunctions = {
'+': (a, b) => a + b,
'-': (a, b) => a - b,
'*': (a, b) => a * b,
'/': (a, b) => a / b,
}
const { getConnectedEdges } = useVueFlow()
// Get the source connections of the result node. In this example it's only one operator node.
const sourceConnections = useHandleConnections({
// type target means all connections where *this* node is the target
// that means we go backwards in the graph to find the source of the connection(s)
type: 'target',
})
// Get the source connections of the operator node
const operatorSourceConnections = computed(() =>
getConnectedEdges(sourceConnections.value[0].source).filter((e) => e.source !== sourceConnections.value[0].source),
)
const operatorData = useNodesData(() => sourceConnections.value.map((connection) => connection.source))
const valueData = useNodesData(() => operatorSourceConnections.value.map((connection) => connection.source))
const result = computed(() => {
const currResult = operatorData.value.reduce((acc, { data }) => {
const operator = data?.operator
if (operator) {
const [a, b] = valueData.value.map(({ data }) => data?.value)
if (a && b) {
return mathFunctions[operator](a, b)
}
}
return acc
}, 0)
// Round to 2 decimal places
return Math.round(currResult * 100) / 100
})
</script>
<template>
<div class="calculation">
<template v-for="(value, i) in valueData" :key="`${value.id}-${value.data}`">
<span>
{{ value.data?.value }}
</span>
<span v-if="i !== valueData.length - 1">
{{ operatorData[0].data?.operator }}
</span>
</template>
</div>
<span> = </span>
<span class="counter" :style="{ color: result > 0 ? '#5EC697' : '#f15a16' }">
{{ result }}
</span>
<Handle
type="target"
:position="Position.Left"
:connectable="false"
:style="{ background: result > 0 ? '#5EC697' : '#f15a16' }"
/>
</template>
+22
View File
@@ -0,0 +1,22 @@
<script setup>
import { Handle, Position, useVueFlow } from '@vue-flow/core'
const props = defineProps(['id', 'data'])
const { updateNodeData } = useVueFlow()
function onChange(event) {
const value = Number.parseFloat(event.target.value)
if (!Number.isNaN(value)) {
updateNodeData(props.id, { value })
}
}
</script>
<template>
<label :for="`${id}-input`"> </label>
<input :id="`${id}-input`" :value="data.value" type="number" class="nodrag" @change="onChange" />
<Handle type="source" :position="Position.Right" :connectable="false" />
</template>
+7
View File
@@ -0,0 +1,7 @@
export { default as MathApp } from './App.vue?raw'
export { default as MathElements } from './initial-elements.js?raw'
export { default as MathValueNode } from './ValueNode.vue?raw'
export { default as MathOperatorNode } from './OperatorNode.vue?raw'
export { default as MathResultNode } from './ResultNode.vue?raw'
export { default as MathIcon } from './Icon.vue?raw'
export { default as MathCSS } from './style.css?inline'
+31
View File
@@ -0,0 +1,31 @@
export const initialNodes = [
{
id: '1',
position: { x: 0, y: 0 },
type: 'value',
data: { value: 10 },
},
{
id: '2',
position: { x: 0, y: 100 },
type: 'value',
data: { value: 30 },
},
{
id: '3',
position: { x: 300, y: 35 },
type: 'operator',
data: { operator: '+' },
},
{
id: '4',
position: { x: 650, y: 15 },
type: 'result',
},
]
export const initialEdges = [
{ id: 'e1-3', source: '1', target: '3', animated: true, targetHandle: 'target-a' },
{ id: 'e2-3', source: '2', target: '3', animated: true, targetHandle: 'target-b' },
{ id: 'e3-4', source: '3', target: '4', animated: true },
]
+123
View File
@@ -0,0 +1,123 @@
.math-flow {
background-color: #edf2f7;
height: 100%;
width: 100%;
}
.vue-flow__handle {
height: 24px;
width: 10px;
background: #aaa;
border-radius: 4px
}
.vue-flow__edges path {
stroke-width: 3;
}
.vue-flow__node-value {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background-color: #f3f4f6;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
.vue-flow__node-value.selected {
box-shadow: 0 0 0 2px #ec4899;
}
.vue-flow__node-value input {
flex: 1;
padding: 8px;
border: none;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.vue-flow__node-value input:focus {
outline: none;
}
.vue-flow__node-value .vue-flow__handle {
background-color: #ec4899;
}
.vue-flow__node-operator {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px 24px;
background-color: #f3f4f6;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
.vue-flow__node-operator.selected {
box-shadow: 0 0 0 2px #0EA5E9;
}
.vue-flow__node-operator .buttons {
display: flex;
gap: 8px;
}
.vue-flow__node-operator button {
border: none;
cursor: pointer;
background-color: #4a5568;
border-radius: 8px;
color: white;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
width: 40px;
height: 40px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.vue-flow__node-operator button:hover {
background-color: #0EA5E9;
transition: background-color 0.2s;
}
.vue-flow__node-operator button.selected {
background-color: #0EA5E9;
}
.vue-flow__node-operator .vue-flow__handle[data-handleid="target-a"] {
top: 25%;
}
.vue-flow__node-operator .vue-flow__handle[data-handleid="target-b"] {
top: 75%;
}
.vue-flow__node-operator .vue-flow__handle {
background-color: #0EA5E9;
}
.vue-flow__node-result {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px 24px;
background-color: #f3f4f6;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
.vue-flow__node-result.selected {
box-shadow: 0 0 0 2px #5EC697;
}
.vue-flow__node-result .calculation {
display: flex;
gap: 8px;
font-size: 24px;
}
+1
View File
@@ -206,6 +206,7 @@ export default defineConfigWithTheme<DefaultTheme.Config>({
{ text: 'Drag & Drop', link: '/examples/dnd' }, { text: 'Drag & Drop', link: '/examples/dnd' },
{ text: 'Interactions', link: '/examples/interaction' }, { text: 'Interactions', link: '/examples/interaction' },
{ text: 'Save & Restore', link: '/examples/save' }, { text: 'Save & Restore', link: '/examples/save' },
{ text: 'Math Operation Flow', link: '/examples/math' },
{ text: 'Screenshot', link: '/examples/screenshot' }, { text: 'Screenshot', link: '/examples/screenshot' },
{ text: 'Node Visibility', link: '/examples/hidden' }, { text: 'Node Visibility', link: '/examples/hidden' },
{ text: 'Node Intersections', link: '/examples/intersection' }, { text: 'Node Intersections', link: '/examples/intersection' },
+9
View File
@@ -0,0 +1,9 @@
# Math Operation
This example demonstrates how to use the different composables like `useHandleConnections` or `useNodesData` to create a data flow between nodes.
In this example we use nodes to create a simple math operation, where the user can input two numbers and select an operation to perform.
<div class="mt-6">
<Repl example="math"></Repl>
</div>
+1 -6
View File
@@ -1,4 +1,3 @@
# Stress # Stress
Vue Flow is built to be fast but there are limits. Vue Flow is built to be fast but there are limits.
@@ -6,9 +5,5 @@ Vue Flow is built to be fast but there are limits.
Try out how many nodes you can get to render before your browser crashes. Try out how many nodes you can get to render before your browser crashes.
<div class="mt-6"> <div class="mt-6">
<ClientOnly> <Repl example="stress"></Repl>
<Suspense>
<Repl example="stress"></Repl>
</Suspense>
</ClientOnly>
</div> </div>
+1 -5
View File
@@ -4,9 +4,5 @@
Teleport your nodes to another dom element using Vue 3 [`Teleport`](https://vuejs.org/guide/built-ins/teleport.html). Teleport your nodes to another dom element using Vue 3 [`Teleport`](https://vuejs.org/guide/built-ins/teleport.html).
<div class="mt-6"> <div class="mt-6">
<ClientOnly> <Repl example="teleport"></Repl>
<Suspense>
<Repl example="teleport"></Repl>
</Suspense>
</ClientOnly>
</div> </div>
+4
View File
@@ -18,6 +18,10 @@ export const routes: RouterOptions['routes'] = [
path: '/snap-handle', path: '/snap-handle',
component: () => import('./src/SnapHandle/SnapHandleExample.vue'), component: () => import('./src/SnapHandle/SnapHandleExample.vue'),
}, },
{
path: '/math',
component: () => import('./src/Math/MathExample.vue'),
},
{ {
path: '/node-resizer', path: '/node-resizer',
component: () => import('./src/NodeResizer/NodeResizerExample.vue'), component: () => import('./src/NodeResizer/NodeResizerExample.vue'),
+33
View File
@@ -0,0 +1,33 @@
<script lang="ts" setup>
defineProps<{
name: '+' | '-' | '*' | '/'
}>()
</script>
<template>
<svg v-if="name === '+'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6z" />
</svg>
<svg v-else-if="name === '-'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="M19 13H5v-2h14v2z" />
</svg>
<svg v-else-if="name === '*'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 12L6 6m6 6l6 6m-6-6l6-6m-6 6l-6 6"
/>
</svg>
<svg v-else-if="name === '/'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="currentColor"
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"
/>
</svg>
</template>
+31
View File
@@ -0,0 +1,31 @@
<script setup lang="ts">
import { VueFlow } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import { initialEdges, initialNodes } from './initialElements'
import ValueNode from './ValueNode.vue'
import OperatorNode from './OperatorNode.vue'
import ResultNode from './ResultNode.vue'
import './styles.css'
const nodes = ref(initialNodes)
const edges = ref(initialEdges)
</script>
<template>
<VueFlow class="math-flow" :nodes="nodes" :edges="edges" fit-view-on-init>
<template #node-value="props">
<ValueNode :id="props.id" :data="props.data" />
</template>
<template #node-operator="props">
<OperatorNode :id="props.id" :data="props.data" />
</template>
<template #node-result="props">
<ResultNode :id="props.id" />
</template>
<Background />
</VueFlow>
</template>
+31
View File
@@ -0,0 +1,31 @@
<script setup lang="ts">
import type { NodeProps } from '@vue-flow/core'
import { Handle, Position, useVueFlow } from '@vue-flow/core'
import type { Operator, OperatorNodeData } from './types'
import Icon from './Icon.vue'
const props = defineProps<Pick<NodeProps<OperatorNodeData>, 'id' | 'data'>>()
const operators: Operator[] = ['+', '-', '*', '/']
const { updateNodeData } = useVueFlow()
</script>
<template>
<label :for="`${id}-operator`">Operator</label>
<div class="buttons nodrag">
<button
v-for="operator of operators"
:key="`${id}-${operator}-operator`"
:class="{ selected: data.operator === operator }"
@click="updateNodeData(props.id, { operator })"
>
<Icon :name="operator" />
</button>
</div>
<Handle type="source" :position="Position.Right" :connectable="false" />
<Handle id="target-a" type="target" :position="Position.Left" :connectable="false" />
<Handle id="target-b" type="target" :position="Position.Left" :connectable="false" />
</template>
+76
View File
@@ -0,0 +1,76 @@
<script setup lang="ts">
import type { GraphNode } from '@vue-flow/core'
import { Handle, Position, useHandleConnections, useNodesData, useVueFlow } from '@vue-flow/core'
import type { OperatorNodeData, ValueNodeData } from './types'
import { mathFunctions } from './utils'
defineProps<{ id: string }>()
const { getConnectedEdges } = useVueFlow()
// Get the source connections of the result node. In this example it's only one operator node.
const sourceConnections = useHandleConnections({
// type target means all connections where *this* node is the target
// that means we go backwards in the graph to find the source of the connection(s)
type: 'target',
})
// Get the source connections of the operator node
const operatorSourceConnections = computed(() =>
getConnectedEdges(sourceConnections.value[0].source).filter((e) => e.source !== sourceConnections.value[0].source),
)
const operatorData = useNodesData<GraphNode<OperatorNodeData>>(() =>
sourceConnections.value.map((connection) => connection.source),
)
const valueData = useNodesData<GraphNode<ValueNodeData>>(() =>
operatorSourceConnections.value.map((connection) => connection.source),
)
const result = computed(() => {
const currResult = operatorData.value.reduce((acc, { data }) => {
const operator = data?.operator
if (operator) {
const [a, b] = valueData.value.map(({ data }) => data?.value)
if (a && b) {
return mathFunctions[operator](a, b)
}
}
return acc
}, 0)
// Round to 2 decimal places
return Math.round(currResult * 100) / 100
})
</script>
<template>
<div class="calculation">
<template v-for="(value, i) in valueData" :key="`${value.id}-${value.data}`">
<span>
{{ value.data?.value }}
</span>
<span v-if="i !== valueData.length - 1">
{{ operatorData[0].data?.operator }}
</span>
</template>
</div>
<span> = </span>
<span class="counter" :style="{ color: result > 0 ? '#5EC697' : '#f15a16' }">
{{ result }}
</span>
<Handle
type="target"
:position="Position.Left"
:connectable="false"
:style="{ background: result > 0 ? '#5EC697' : '#f15a16' }"
/>
</template>
+28
View File
@@ -0,0 +1,28 @@
<script setup lang="ts">
import type { NodeProps } from '@vue-flow/core'
import { Handle, Position, useVueFlow } from '@vue-flow/core'
import type { ValueNodeData } from './types'
const props = defineProps<Pick<NodeProps<ValueNodeData>, 'id' | 'data'>>()
const { updateNodeData } = useVueFlow()
function onChange(event: Event) {
const evt = event as InputEvent
const target = evt.target as HTMLInputElement
const value = Number.parseFloat(target.value)
if (!Number.isNaN(value)) {
updateNodeData(props.id, { value })
}
}
</script>
<template>
<label :for="`${id}-input`">Value</label>
<input :id="`${id}-input`" :value="data.value" type="number" class="nodrag" @change="onChange" />
<Handle type="source" :position="Position.Right" :connectable="false" />
</template>
+34
View File
@@ -0,0 +1,34 @@
import type { Edge, Node } from '@vue-flow/core'
// these are some math nodes, inputs and outputs
export const initialNodes: Node[] = [
{
id: '1',
position: { x: 0, y: 0 },
type: 'value',
data: { value: 10 },
},
{
id: '2',
position: { x: 0, y: 100 },
type: 'value',
data: { value: 30 },
},
{
id: '3',
position: { x: 400, y: 35 },
type: 'operator',
data: { operator: '+' },
},
{
id: '4',
position: { x: 700, y: 40 },
type: 'result',
},
]
export const initialEdges: Edge[] = [
{ id: 'e1-3', source: '1', target: '3', animated: true, targetHandle: 'target-a' },
{ id: 'e2-3', source: '2', target: '3', animated: true, targetHandle: 'target-b' },
{ id: 'e3-4', source: '3', target: '4', animated: true },
]
+123
View File
@@ -0,0 +1,123 @@
.math-flow {
background-color: #edf2f7;
height: 100%;
width: 100%;
}
.vue-flow__handle {
height: 24px;
width: 10px;
background: #aaa;
border-radius: 4px
}
.vue-flow__edges path {
stroke-width: 3;
}
.vue-flow__node-value {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background-color: #f3f4f6;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
.vue-flow__node-value.selected {
box-shadow: 0 0 0 2px #ec4899;
}
.vue-flow__node-value input {
flex: 1;
padding: 8px;
border: none;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.vue-flow__node-value input:focus {
outline: none;
}
.vue-flow__node-value .vue-flow__handle {
background-color: #ec4899;
}
.vue-flow__node-operator {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 8px 16px;
background-color: #f3f4f6;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
.vue-flow__node-operator.selected {
box-shadow: 0 0 0 2px #0EA5E9;
}
.vue-flow__node-operator .buttons {
display: flex;
gap: 8px;
}
.vue-flow__node-operator button {
border: none;
cursor: pointer;
background-color: #4a5568;
border-radius: 8px;
color: white;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
width: 40px;
height: 40px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.vue-flow__node-operator button:hover {
background-color: #0EA5E9;
transition: background-color 0.2s;
}
.vue-flow__node-operator button.selected {
background-color: #0EA5E9;
}
.vue-flow__node-operator .vue-flow__handle[data-handleid="target-a"] {
top: 25%;
}
.vue-flow__node-operator .vue-flow__handle[data-handleid="target-b"] {
top: 75%;
}
.vue-flow__node-operator .vue-flow__handle {
background-color: #0EA5E9;
}
.vue-flow__node-result {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 8px 16px;
background-color: #f3f4f6;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
.vue-flow__node-result.selected {
box-shadow: 0 0 0 2px #5EC697;
}
.vue-flow__node-result .calculation {
display: flex;
gap: 8px;
font-size: 24px;
}
+9
View File
@@ -0,0 +1,9 @@
export interface ValueNodeData {
value: number
}
export type Operator = '+' | '-' | '*' | '/'
export interface OperatorNodeData {
operator: Operator
}
+8
View File
@@ -0,0 +1,8 @@
import type { Operator } from './types'
export const mathFunctions: Record<Operator, (a: number, b: number) => number> = {
'+': (a: number, b: number) => a + b,
'-': (a: number, b: number) => a - b,
'*': (a: number, b: number) => a * b,
'/': (a: number, b: number) => a / b,
}