useFlowLayout
useFlowLayout turns a graph into a drawing. Hand it your nodes and edges and it returns a position per node id plus the connections between them, ready to bind to FluxFlowNode and FluxFlowConnection. Where a Chain places a run of steps in a line, this places a graph that branches and comes back together.
It is a plain function: no component, no DOM and no reactivity. Nodes are laid out in layers, every node lands one layer past its furthest source, and every layer is centred against the widest one, so a straight stretch of the graph stays straight.
Usage
import { useFlowLayout } from '@flux-ui/flow';
const {positions, connections} = useFlowLayout(
[{id: 'intake'}, {id: 'verify'}, {id: 'score'}],
[{from: 'intake', to: 'verify'}, {from: 'verify', to: 'score'}]
);
// positions: { intake: {x: 0, y: 0}, verify: {x: 0, y: 150}, … }
// connections: [{from: 'intake', to: 'verify', fromSide: 'bottom', toSide: 'top'}, …]Both are meant to be spread straight onto the components:
<FluxFlowNode
v-for="node of nodes"
:key="node.id"
:id="node.id"
v-bind="positions[node.id]">
<FluxFlowActionCard :title="node.title"/>
</FluxFlowNode>
<FluxFlowConnection
v-for="wire of connections"
:key="`${wire.from} ${wire.to}`"
v-bind="wire"/>TIP
The function runs without a DOM, so it cannot measure a card. Nodes without a width and height of their own fall back to nodeWidth and nodeHeight; pass the size your cards actually have to keep the gaps even.
TIP
The sides come with the connections, so you never name them yourself. Binding a bare from and to instead leaves each connector picking the shortest axis between the two cards, and in a layer wider than it is deep that is the wrong one. Setting axis on the surrounding FluxFlow fixes that for hand placed nodes too.
Example
Left to right
A release pipeline laid out with direction: 'horizontal'. The retry edge closes a cycle, which the layout cuts open instead of following, so the run still lands in four layers.
<template>
<!-- Room under the row for the retry loop and its badge. -->
<FluxFlow :padding="45">
<FluxFlowNode
v-for="node of nodes"
:key="node.id"
:id="node.id"
v-bind="positions[node.id]">
<FluxFlowPill :icon="node.icon" :label="node.label" :color="node.color"/>
</FluxFlowNode>
<FluxFlowConnection
v-for="wire of wires"
:key="`${wire.from} ${wire.to}`"
v-bind="wire"/>
</FluxFlow>
</template>
<script
setup
lang="ts">
import type { FluxColor, FluxIconName } from '@flux-ui/types';
import { FluxFlow, FluxFlowConnection, FluxFlowNode, FluxFlowPill, useFlowLayout } from '@flux-ui/flow';
const nodes: { id: string; label: string; icon: FluxIconName; color: FluxColor }[] = [
{id: 'queued', label: 'Queued', icon: 'stopwatch', color: 'gray'},
{id: 'building', label: 'Building', icon: 'box', color: 'info'},
{id: 'testing', label: 'Testing', icon: 'robot', color: 'info'},
{id: 'released', label: 'Released', icon: 'rocket', color: 'success'}
];
const edges = [
{from: 'queued', to: 'building'},
{from: 'building', to: 'testing'},
{from: 'testing', to: 'released'},
// A cycle. The layout cuts it open instead of following it, and hands it
// back with sides that loop under the row rather than back through it.
{from: 'released', to: 'building'}
];
// How a connector looks is yours; where it attaches comes from the layout.
const style: Record<string, { dashed?: boolean; label?: string }> = {
'released->building': {dashed: true, label: 'Retry'}
};
const {positions, connections} = useFlowLayout(nodes, edges, {
direction: 'horizontal',
nodeWidth: 162,
nodeHeight: 44,
layerGap: 45
});
const wires = connections.map(wire => ({...wire, ...style[`${wire.from}->${wire.to}`]}));
</script>Options
direction ('vertical')
The axis the layers stack on. vertical runs top to bottom, horizontal runs left to right.
layerGap (60)
The space, in pixels, between two layers.
nodeGap (45)
The space, in pixels, between two nodes inside one layer.
nodeWidth (300) and nodeHeight (90)
The size assumed for a node that does not carry one, since the function runs without a DOM.
x (0) and y (0)
The top-left corner the layout starts from.
Cycles
Only directed acyclic graphs lay out in layers, but a graph that loops back on itself is not an error here. An edge that would close a cycle is cut rather than followed, so a retry or a rollback still produces a usable layout instead of an empty one.
A cut edge runs against the flow, and its connection says so: it leaves and enters on the off axis, looping around the diagram instead of cutting back across it. You do not have to work out which edge that was.
Edges to a node that was never given, and edges from a node to itself, are dropped. They are missing from connections too, so iterating over it never leaves a connector pointing at nothing.
Type declarations
type FluxFlowLayoutNode = {
readonly id: string;
readonly width?: number;
readonly height?: number;
};
type FluxFlowLayoutEdge = {
readonly from: string;
readonly to: string;
};
type FluxFlowLayoutOptions = {
readonly x?: number;
readonly y?: number;
readonly direction?: 'horizontal' | 'vertical';
readonly layerGap?: number;
readonly nodeGap?: number;
readonly nodeWidth?: number;
readonly nodeHeight?: number;
};
type FluxFlowLayoutConnection = {
readonly from: string;
readonly to: string;
readonly fromSide: 'top' | 'right' | 'bottom' | 'left';
readonly toSide: 'top' | 'right' | 'bottom' | 'left';
};
type FluxFlowLayoutResult = {
readonly positions: Record<string, { readonly x: number; readonly y: number }>;
readonly connections: readonly FluxFlowLayoutConnection[];
};
declare function useFlowLayout(
nodes: readonly FluxFlowLayoutNode[],
edges: readonly FluxFlowLayoutEdge[],
options?: FluxFlowLayoutOptions
): FluxFlowLayoutResult;