Skip to content

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.

Payment received
Amount over €1,000?
Manual review
Auto-approve
Book the payment

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.

Push to main
Run tests
Build image
Ship

<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.

Push to mainEvery commit
Lint
Unit tests
Audit
Build image
Shipped

<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.

Fetch the invoice
Is the total settled?
Store the result

<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.

Order placed
Charge the card
Send the receipt
Retries A charge that is declined is retried twice before the order is released.

<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.

Nightly sync
Fetch ordersShopify API
Pulls every order created since the last successful run.
Pending
Enrich recordsCustomer service
Adds customer, currency and tax data to each order.
Pending
Load into warehouseBigQuery
Writes the enriched batch and marks the run as complete.
Pending

<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.

Order Released
1
Condition
US Shopify orders with matching state or SKU
Action
Set the order's fulfilment profile to Prestons AU
Action
Apply a manual hold
2
Condition
EU orders shipping from the Rotterdam warehouse
Action
Set delivery to one day delivery

<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>

Used components