Graph
FluxFlowGraph places a branching flow for you. It reads the connectors written inside it as the graph they describe, lays the nodes out in layers, and hands each one its position, so a Node inside a graph carries no x and no y.
It is the branching counterpart of a Chain: a chain places a straight run, a graph places anything that forks and merges. The layout runs on the sizes the cards measured for themselves, so a 300px card and a pill in the same layer still line up.
Two shapes are on offer. By default the graph stacks in layers, which suits a pipeline: everything at the same depth sits on the same row. Name a trunk instead and those ids run straight down a column while everything hanging off them walks out to the right, which suits a numbered rule list, where each rule should stay with its own fan-out rather than share a row with the next rule.
TIP
Name the axis on the surrounding Flow that matches the graph's direction, so every connector runs with the flow instead of taking the shorter way out.
TIP
A graph reads its connectors before it lays anything out, so it also knows whether any of them carries a badge and leaves the layers further apart when one does, exactly as a Chain does for its run. Set layer-gap yourself to take that over.
TIP
A trunk places the nodes; the connectors are still yours. Give the ones that run sideways into a card a to-align="start" so they land on its icon rather than halfway down it, and give a branch a from-side="bottom" so it drops out of the card above rather than leaving its right edge and doubling back.
TIP
A node lands one layer past its furthest source, and every layer is centred against the widest one. An edge that would close a cycle is cut rather than followed, so a graph that is not a tree still lays out; give such a connector its own from-side and to-side to send it around the diagram.
Props
trunk?: string[]
The ids that form the trunk, in the order they run down it. Naming them lays the graph out as a trunk with branches instead of in layers, which is the shape of a rule list rather than of a pipeline. Without it the graph stacks in layers and `direction` applies.
direction?: 'horizontal' | 'vertical'
The direction the layers stack in. `vertical` runs top to bottom, `horizontal` runs left to right. Ignored by a trunk, which always runs down.
Default: vertical
indent?: number
Trunk only. The x, in pixels, a branch adds per level it hangs off the trunk. Without one the graph leaves 180px, or 210px as soon as any of its connectors carries a label or an icon, since a branch runs sideways and a badge on one needs room across rather than down.
x?: number
The x coordinate the layout starts from.
y?: number
The y coordinate the layout starts from.
layerGap?: number
The gap, in pixels, between two layers, or between a branch and the trunk node below it. Without one the graph leaves 60px, or in layers 105px (210px when horizontal) as soon as any of its connectors carries a label or an icon, since a badge needs room the standard gap does not have.
nodeGap?: number
The gap, in pixels, between two nodes inside one layer.
Default: 45
Slots
default
The nodes and connectors of the graph. The connectors are read as the wiring the layout runs on, so the nodes need no coordinates of their own.
Examples
Horizontal
Set direction to horizontal to stack the layers left to right, and match it with axis on the flow.
<template>
<div style="height: 300px">
<FluxFlow :padding="21" interactive axis="horizontal" background="dots">
<FluxFlowGraph direction="horizontal" :layer-gap="90">
<FluxFlowNode id="push">
<FluxFlowTriggerCard title="Push to main"/>
</FluxFlowNode>
<FluxFlowNode id="test">
<FluxFlowActionCard title="Run tests" icon="gauge-high"/>
</FluxFlowNode>
<FluxFlowNode id="build">
<FluxFlowActionCard title="Build image" icon="server"/>
</FluxFlowNode>
<FluxFlowNode id="ship">
<FluxFlowActionCard title="Ship" icon="circle-check" color="success"/>
</FluxFlowNode>
<FluxFlowConnection from="push" to="test"/>
<FluxFlowConnection from="test" to="build"/>
<FluxFlowConnection from="build" to="ship"/>
</FluxFlowGraph>
</FluxFlow>
</div>
</template>
<script
setup
lang="ts">
import { FluxFlow, FluxFlowActionCard, FluxFlowConnection, FluxFlowGraph, FluxFlowNode, FluxFlowTriggerCard } from '@flux-ui/flow';
</script>Fanning out and merging
Three checks run off one trigger and come back together on the step that needs all of them. The layer is laid out on what each node actually measures, so pills of three different widths sit evenly spaced and the layers above and below stay centred on them.
<template>
<div style="height: 450px">
<FluxFlow :padding="21" interactive axis="vertical" background="dots">
<FluxFlowGraph>
<FluxFlowNode id="push">
<FluxFlowTriggerCard title="Push to main" subtitle="Every commit"/>
</FluxFlowNode>
<FluxFlowNode id="lint">
<FluxFlowPill color="info" icon="code-branch" label="Lint"/>
</FluxFlowNode>
<FluxFlowNode id="test">
<FluxFlowPill color="info" icon="gauge-high" label="Unit tests"/>
</FluxFlowNode>
<FluxFlowNode id="audit">
<FluxFlowPill color="info" icon="file-lines" label="Audit"/>
</FluxFlowNode>
<FluxFlowNode id="build">
<FluxFlowActionCard title="Build image" icon="server"/>
</FluxFlowNode>
<FluxFlowNode id="ship">
<FluxFlowTerminal color="success" icon="circle-check" label="Shipped"/>
</FluxFlowNode>
<FluxFlowConnection from="push" to="lint"/>
<FluxFlowConnection from="push" to="test"/>
<FluxFlowConnection from="push" to="audit"/>
<FluxFlowConnection from="lint" to="build"/>
<FluxFlowConnection from="test" to="build"/>
<FluxFlowConnection from="audit" to="build"/>
<FluxFlowConnection from="build" to="ship"/>
</FluxFlowGraph>
</FluxFlow>
</div>
</template>
<script
setup
lang="ts">
import { FluxFlow, FluxFlowActionCard, FluxFlowConnection, FluxFlowGraph, FluxFlowNode, FluxFlowPill, FluxFlowTerminal, FluxFlowTriggerCard } from '@flux-ui/flow';
</script>Running back
A connector that would close a cycle is cut out of the layout rather than followed, so the three steps still stack in order. Give it a from-side and a to-side on the off axis and it loops around the diagram instead of cutting back across it.
<template>
<div style="height: 390px">
<FluxFlow :padding="21" interactive axis="vertical" background="dots">
<FluxFlowGraph>
<FluxFlowNode id="fetch">
<FluxFlowActionCard title="Fetch the invoice" icon="database"/>
</FluxFlowNode>
<FluxFlowNode id="validate">
<FluxFlowConditionCard title="Is the total settled?"/>
</FluxFlowNode>
<FluxFlowNode id="store">
<FluxFlowActionCard title="Store the result" icon="box" color="success"/>
</FluxFlowNode>
<FluxFlowConnection from="fetch" to="validate"/>
<FluxFlowConnection from="validate" to="store" label="Yes"/>
<!-- Runs back against the flow, so it takes the off axis and
loops around the diagram instead of cutting across it. -->
<FluxFlowConnection
from="validate"
to="fetch"
from-side="right"
to-side="right"
color="warning"
label="No"
dashed/>
</FluxFlowGraph>
</FluxFlow>
</div>
</template>
<script
setup
lang="ts">
import { FluxFlow, FluxFlowActionCard, FluxFlowConditionCard, FluxFlowConnection, FluxFlowGraph, FluxFlowNode } from '@flux-ui/flow';
</script>Keeping a node in place
A node that carries its own x and y is left where it is and takes no part in the layout, which is how an annotation stands beside a flow without pushing a layer aside.
<template>
<div style="height: 390px">
<FluxFlow :padding="21" interactive axis="vertical" background="dots">
<FluxFlowGraph>
<FluxFlowNode id="order">
<FluxFlowTriggerCard title="Order placed"/>
</FluxFlowNode>
<FluxFlowNode id="charge">
<FluxFlowActionCard title="Charge the card" icon="server"/>
</FluxFlowNode>
<FluxFlowNode id="receipt">
<FluxFlowActionCard title="Send the receipt" icon="paper-plane"/>
</FluxFlowNode>
<!-- Carries its own coordinates, so the layout leaves it where it
is and lays the three cards out around it. -->
<FluxFlowNode id="note" :x="360" :y="102">
<FluxFlowNote title="Retries">
A charge that is declined is retried twice before the order is released.
</FluxFlowNote>
</FluxFlowNode>
<FluxFlowConnection from="order" to="charge"/>
<FluxFlowConnection from="charge" to="receipt"/>
<FluxFlowConnection from="charge" to="note" from-side="right" to-side="left" marker-start="none" marker-end="none" dotted/>
</FluxFlowGraph>
</FluxFlow>
</div>
</template>
<script
setup
lang="ts">
import { FluxFlow, FluxFlowActionCard, FluxFlowConnection, FluxFlowGraph, FluxFlowNode, FluxFlowNote, FluxFlowTriggerCard } from '@flux-ui/flow';
</script>A running pipeline
The running pipeline from the examples, with every coordinate taken out. Each stage still swaps its icon and tint off one clock, spins while it runs and carries a FluxBadge naming its state, and the connectors still fill towards the stage they feed; the graph is what decides where the pill and the three cards land. The stages are written with a v-for, and the graph reads straight through it.
<template>
<div style="height: 690px">
<FluxFlow :padding="21" interactive axis="vertical" background="dots">
<FluxFlowGraph>
<FluxFlowNode id="trigger">
<FluxFlowPill
color="info"
icon="bolt"
label="Nightly sync"
:is-loading="isRunning(0)"/>
</FluxFlowNode>
<FluxFlowNode
v-for="(stage, index) of STAGES"
:id="stage.id"
:key="stage.id">
<FluxFlowActionCard
:title="stage.title"
:subtitle="stage.subtitle"
:icon="isDone(index + 1) ? 'circle-check' : stage.icon"
:color="colorOf(index + 1)"
:active="isRunning(index + 1)"
:is-loading="isRunning(index + 1)">
{{ stage.description }}
<template #footer>
<FluxBadge
:color="colorOf(index + 1)"
:label="labelOf(index + 1)"/>
</template>
</FluxFlowActionCard>
</FluxFlowNode>
<FluxFlowConnection
from="trigger"
to="fetch"
progress-color="primary"
:progress-value="progressOf(1)"/>
<FluxFlowConnection
from="fetch"
to="enrich"
progress-color="primary"
:progress-value="progressOf(2)"/>
<FluxFlowConnection
from="enrich"
to="load"
progress-color="primary"
:progress-value="progressOf(3)"/>
</FluxFlowGraph>
</FluxFlow>
</div>
</template>
<script
setup
lang="ts">
import type { FluxColor, FluxIconName } from '@flux-ui/types';
import { FluxBadge } from '@flux-ui/components';
import { FluxFlow, FluxFlowActionCard, FluxFlowConnection, FluxFlowGraph, FluxFlowNode, FluxFlowPill } from '@flux-ui/flow';
import { onBeforeUnmount, onMounted, ref } from 'vue';
const STAGES: { readonly id: string; readonly icon: FluxIconName; readonly title: string; readonly subtitle: string; readonly description: string }[] = [
{id: 'fetch', icon: 'database', title: 'Fetch orders', subtitle: 'Shopify API', description: 'Pulls every order created since the last successful run.'},
{id: 'enrich', icon: 'wand-magic-sparkles', title: 'Enrich records', subtitle: 'Customer service', description: 'Adds customer, currency and tax data to each order.'},
{id: 'load', icon: 'server', title: 'Load into warehouse', subtitle: 'BigQuery', description: 'Writes the enriched batch and marks the run as complete.'}
];
// The run walks the pill and the three cards in order: a step runs for one
// unit of elapsed time, during which the connector towards the next step
// fills, so a step that starts running is already fully connected.
const STEPS = STAGES.length + 1;
const STEP_DURATION = 1400;
// A finished run holds for a beat, then the canvas clears for a beat before
// the next one starts, so the reset reads as a reset.
const HOLD = 1;
const IDLE = 0.6;
const elapsed = ref(0);
let frame = 0;
onMounted(() => {
const start = performance.now();
const tick = (now: number) => {
elapsed.value = (((now - start) / STEP_DURATION) % (STEPS + HOLD + IDLE)) - IDLE;
frame = requestAnimationFrame(tick);
};
frame = requestAnimationFrame(tick);
});
onBeforeUnmount(() => cancelAnimationFrame(frame));
function isRunning(step: number): boolean {
return elapsed.value >= step && elapsed.value < step + 1;
}
function isDone(step: number): boolean {
return elapsed.value >= step + 1;
}
function colorOf(step: number): FluxColor {
if (isDone(step)) {
return 'success';
}
return isRunning(step) ? 'primary' : 'gray';
}
function labelOf(step: number): string {
if (isDone(step)) {
return 'Done';
}
return isRunning(step) ? 'Running' : 'Pending';
}
function progressOf(step: number): number {
return Math.min(Math.max(elapsed.value - step + 1, 0), 1);
}
</script>Routing rules
The routing rules from the examples, with every coordinate taken out. Naming the trigger and the two steps as the trunk runs them straight down a column and walks each rule out to the right, so a rule stays with its own fan-out. Every rule is as tall as what it does, and the next step clears the branch above it.
<template>
<div style="height: 540px">
<FluxFlow :padding="21" interactive background="dots">
<FluxFlowGraph :trunk="['trigger', 'step-1', 'step-2']">
<FluxFlowNode id="trigger">
<FluxFlowPill color="info" icon="bolt" label="Order Released"/>
</FluxFlowNode>
<FluxFlowNode id="step-1">
<FluxFlowStep :value="1"/>
</FluxFlowNode>
<FluxFlowNode id="condition-1">
<FluxFlowConditionCard>
US Shopify orders with matching state or SKU
</FluxFlowConditionCard>
</FluxFlowNode>
<FluxFlowNode id="fulfilment-1">
<FluxFlowActionCard>
Set the order's fulfilment profile to Prestons AU
</FluxFlowActionCard>
</FluxFlowNode>
<FluxFlowNode id="hold-1">
<FluxFlowActionCard>
Apply a manual hold
</FluxFlowActionCard>
</FluxFlowNode>
<FluxFlowNode id="step-2">
<FluxFlowStep :value="2"/>
</FluxFlowNode>
<FluxFlowNode id="condition-2">
<FluxFlowConditionCard>
EU orders shipping from the Rotterdam warehouse
</FluxFlowConditionCard>
</FluxFlowNode>
<FluxFlowNode id="fulfilment-2">
<FluxFlowActionCard>
Set delivery to one day delivery
</FluxFlowActionCard>
</FluxFlowNode>
<FluxFlowConnection from="trigger" to="step-1"/>
<FluxFlowConnection from="step-1" to="condition-1" to-align="start"/>
<FluxFlowConnection from="step-1" to="step-2"/>
<FluxFlowConnection from="step-2" to="condition-2" to-align="start"/>
<FluxFlowConnection from="condition-1" to="fulfilment-1" from-side="bottom" from-align="start" to-side="left" to-align="start" label="If true" label-placement="last-leg"/>
<FluxFlowConnection from="condition-1" to="hold-1" from-side="bottom" from-align="start" to-side="left" to-align="start" label="If false" label-placement="last-leg"/>
<FluxFlowConnection from="condition-2" to="fulfilment-2" from-side="bottom" from-align="start" to-side="left" to-align="start" label="If true" label-placement="last-leg"/>
</FluxFlowGraph>
<FluxFlowControls/>
</FluxFlow>
</div>
</template>
<script
setup
lang="ts">
import { FluxFlow, FluxFlowActionCard, FluxFlowConditionCard, FluxFlowConnection, FluxFlowControls, FluxFlowGraph, FluxFlowNode, FluxFlowPill, FluxFlowStep } from '@flux-ui/flow';
</script>