A phone's wrapped flow stacks into one column: when a graph is still wider than three nodes after wrapping, a last pass centres every rank, so wrapped rows sit under the row above (Demo house 3721 to 947px across at 390, crossings 295 to 263)
Docs / docs (push) Successful in 25s
Playwright Tests / test-playwright (1, 2) (push) Failing after 5m34s
Playwright Tests / test-playwright (2, 2) (push) Canceled after 1m27s
Playwright Tests / merge-reports (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Test Backend / test-backend (3.12) (push) Canceled after 0s
Test Backend / test-backend (3.14) (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
Docs / docs (push) Successful in 25s
Playwright Tests / test-playwright (1, 2) (push) Failing after 5m34s
Playwright Tests / test-playwright (2, 2) (push) Canceled after 1m27s
Playwright Tests / merge-reports (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Test Backend / test-backend (3.12) (push) Canceled after 0s
Test Backend / test-backend (3.14) (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WgBfkm4rCmtd1T4vHWjEPx
This commit is contained in:
@@ -5,7 +5,9 @@ the connections and arranges the canvas for you.
|
||||
|
||||
## The canvas
|
||||
|
||||
The graph runs left to right on desktop and top to bottom on phones.
|
||||
The graph runs left to right on desktop and top to bottom on phones. On a
|
||||
phone, at most two nodes sit side by side and the rest wrap onto the rows
|
||||
below, so a large flow grows downwards in one column.
|
||||
|
||||
- **Nodes** show their execution status.
|
||||
- **Edges** are routed around the nodes, a loop back to an earlier node
|
||||
|
||||
@@ -270,6 +270,38 @@ for (const [y, count] of rows) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A hub read by eight readers, each feeding a sink of its own: the shape of
|
||||
* the Demo's house and its dashboards. Wrapped two abreast with straight
|
||||
* edges, each row landed where its edges pulled it and the graph staircased
|
||||
* to 1400px across. Centred, the rows stack into one column.
|
||||
*/
|
||||
const readers = Array.from({ length: 8 }, (_, i) => `r${i}`)
|
||||
const hub = await layoutGraph(
|
||||
["hub", ...readers, ...readers.map((r) => `${r}.sink`)],
|
||||
readers.flatMap((r) => [
|
||||
{ id: r, source: "hub", target: r, sourceHandle: "v", targetHandle: "v" },
|
||||
{
|
||||
id: `${r}.sink`,
|
||||
source: r,
|
||||
target: `${r}.sink`,
|
||||
sourceHandle: "v",
|
||||
targetHandle: "v",
|
||||
},
|
||||
]),
|
||||
"TB",
|
||||
new Map(
|
||||
["hub", ...readers, ...readers.map((r) => `${r}.sink`)].map((id) => [
|
||||
id,
|
||||
one("v"),
|
||||
]),
|
||||
),
|
||||
)
|
||||
const hubX = [...hub.positions.values()].map((at) => at.x)
|
||||
const across = Math.max(...hubX) + 220 - Math.min(...hubX)
|
||||
// Three nodes and their gaps: one more than the two a row holds.
|
||||
assert.ok(across <= 3 * 260, `a wrapped graph runs ${across}px across`)
|
||||
|
||||
/**
|
||||
* Every edge comes back routed, port to port, square, and around what is in
|
||||
* the way. `back` closes a cycle over `mid`: drawn straight between its two
|
||||
|
||||
@@ -114,6 +114,27 @@ const ABREAST = 2
|
||||
*/
|
||||
const WRAP_PASSES = 6
|
||||
|
||||
/**
|
||||
* How wide a wrapped graph may still be before its ranks are centred.
|
||||
*
|
||||
* Wrapping keeps a rank to `ABREAST` nodes, but the layout straightens edges,
|
||||
* so each wrapped row lands where its edges pull it: the rows staircase
|
||||
* sideways, and a big flow ended up thousands of pixels across on a phone.
|
||||
* Past one node more than the rows allow, every rank is centred instead, which
|
||||
* stacks the rows into one column. Below it the straight edges are worth more,
|
||||
* which is the case for a flow that wraps a rank or two.
|
||||
*/
|
||||
const TOO_WIDE = (ABREAST + 1) * (NODE_W + NODE_GAP)
|
||||
|
||||
/**
|
||||
* Room for a value chip when the ranks are centred, running downwards.
|
||||
*
|
||||
* Centring packs a rank's chips together where the edges meet in the middle,
|
||||
* so with no width at all they piled onto each other. This is a number's
|
||||
* worth: a longer value overlaps its neighbour rather than widening the graph.
|
||||
*/
|
||||
const CENTRED_LABEL_W = 60
|
||||
|
||||
/**
|
||||
* What a node is, as far as the layout is concerned.
|
||||
*
|
||||
@@ -200,6 +221,7 @@ function graphOf(
|
||||
direction: Direction,
|
||||
shapes: Map<string, NodeShape>,
|
||||
wrap: Wrap[],
|
||||
centred: boolean,
|
||||
): ElkNode {
|
||||
const across = direction === "LR"
|
||||
// A port is identified by the side it is on as well as by its name: a node
|
||||
@@ -279,6 +301,16 @@ function graphOf(
|
||||
// The default, set out loud: the same flow has to lay out the same way
|
||||
// every time it is opened.
|
||||
"org.eclipse.elk.randomSeed": "1",
|
||||
// See `TOO_WIDE`. Every rank is centred on one axis, so a wrapped row
|
||||
// sits under the row above rather than where its edges pull it. What
|
||||
// bends the edges then takes room down the channels between the ranks,
|
||||
// so the runs there sit closer together.
|
||||
...(centred
|
||||
? {
|
||||
"org.eclipse.elk.layered.nodePlacement.strategy": "SIMPLE",
|
||||
"org.eclipse.elk.layered.spacing.edgeEdgeBetweenLayers": "6",
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
children,
|
||||
edges: [
|
||||
@@ -300,7 +332,7 @@ function graphOf(
|
||||
// Only where it lands is read — `LiveEdge` draws the chip itself —
|
||||
// but a label with no text at all is one ELK reserves no room for.
|
||||
text: " ",
|
||||
width: across ? LABEL_W : 0,
|
||||
width: across ? LABEL_W : centred ? CENTRED_LABEL_W : 0,
|
||||
height: LABEL_H,
|
||||
layoutOptions: { "org.eclipse.elk.edgeLabels.inline": "true" },
|
||||
},
|
||||
@@ -378,9 +410,9 @@ export async function layoutGraph(
|
||||
shapes: Map<string, NodeShape> = new Map(),
|
||||
): Promise<Layout> {
|
||||
const engine = await elk()
|
||||
const place = async (wrap: Wrap[]) => {
|
||||
const place = async (wrap: Wrap[], centred = false) => {
|
||||
const laid = await engine.layout(
|
||||
graphOf(ids, edges, direction, shapes, wrap),
|
||||
graphOf(ids, edges, direction, shapes, wrap, centred),
|
||||
)
|
||||
const placed = new Map<string, Point>()
|
||||
for (const child of laid.children ?? []) {
|
||||
@@ -404,6 +436,10 @@ export async function layoutGraph(
|
||||
wrap.push(...more)
|
||||
;({ placed, laid } = await place(wrap))
|
||||
}
|
||||
const [left, right] = extentOf(placed, shapes, direction)
|
||||
if (wrap.length && right - left > TOO_WIDE) {
|
||||
;({ placed, laid } = await place(wrap, true))
|
||||
}
|
||||
}
|
||||
|
||||
// ELK places top-left corners already, which is what React Flow wants, and
|
||||
@@ -413,14 +449,7 @@ export async function layoutGraph(
|
||||
)
|
||||
const wired = wiredOf(ids, edges)
|
||||
// How far across the nodes reach, for `longestRun`.
|
||||
let [left, right] = [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]
|
||||
for (const [id, at] of positions) {
|
||||
left = Math.min(left, at.x)
|
||||
right = Math.max(
|
||||
right,
|
||||
at.x + sizeOf(shapes.get(id) ?? EMPTY, direction).width,
|
||||
)
|
||||
}
|
||||
const [left, right] = extentOf(positions, shapes, direction)
|
||||
const routes = new Map<string, Route>()
|
||||
for (const edge of laid.edges ?? []) {
|
||||
const index = Number(edge.id.slice(1))
|
||||
@@ -453,6 +482,23 @@ export async function layoutGraph(
|
||||
return { positions, routes }
|
||||
}
|
||||
|
||||
/** The leftmost and rightmost x any node reaches. */
|
||||
function extentOf(
|
||||
placed: Map<string, Point>,
|
||||
shapes: Map<string, NodeShape>,
|
||||
direction: Direction,
|
||||
): [number, number] {
|
||||
let [left, right] = [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]
|
||||
for (const [id, at] of placed) {
|
||||
left = Math.min(left, at.x)
|
||||
right = Math.max(
|
||||
right,
|
||||
at.x + sizeOf(shapes.get(id) ?? EMPTY, direction).width,
|
||||
)
|
||||
}
|
||||
return [left, right]
|
||||
}
|
||||
|
||||
/**
|
||||
* The middle of a route's longest run across the flow, running downwards.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user