Metrics

Metrics :: # (opaque)

Public measurements for comparing layout quality. Every metric is algorithm-independent — it scores positions, routes, or bounds in the shared output vocabulary — so consumers and regression tests can compare any two layouts with the same numbers. All metrics are total and deterministic: any well-typed input has a defined, finite result.

area : { x : F64, y : F64, width : F64, height : F64 } -> F64

Area of the layout bounds — the amount of page the drawing claims. Smaller is denser. Constant time.

crossings : List(Route) -> U64

How many times edges visually cross each other — the strongest single predictor of how tangled a drawing reads. Counts transverse intersections between segments of different routes; segments within one route never count against each other, and two segments that only touch at a point (for example routes attaching to the same node boundary) or overlap collinearly are not crossings. Routes are straightened first: lines and polyline pieces score as themselves, and curve chains are scored by their chords (from to to of each cubic segment), an approximation that keeps the count exact for the polyline skeleton a curve smooths. O(s^2) over all segments; a sweep-based version for large drawings is future work.

bends : List(Route) -> U64

How many corners the reader's eye must follow along the edges. Lines have none; a polyline bends at each interior vertex where the incoming and outgoing directions differ; a curve chain bends at each joint where the incoming tangent (to - ctl_b) and outgoing tangent (ctl_a - from) differ — smooth chains with matching tangents add nothing. Directions differ when the cross-product magnitude exceeds 1e-9 relative to both segment lengths, or when they reverse outright. Linear in the total number of route pieces.

aspect_ratio : { x : F64, y : F64, width : F64, height : F64 } -> F64

The drawing's shape as width over height — near 1 is roughly square, large is a wide banner, small is a tall column. Degenerate rule, chosen to stay finite: a zero-height bounds with zero width (a point or empty layout) reports 1, and a zero-height bounds with positive width (a fully flat layout) reports its width, which still grows the flatter and wider the drawing gets. Constant time.

separation_violations : List({ x : F64, y : F64 }), List({ width : F64, height : F64 }), F64 -> U64

How many node pairs sit closer than the required clearance — pairs a reader sees as colliding or crowded. Each node's box is centered on its position and expanded by gap / 2 on every side; a pair counts when the expanded boxes strictly overlap on both axes, so boxes that exactly touch are allowed. Positions and sizes are index-aligned; indices present in only one list are ignored. O(n^2) over node pairs.

displacement : List({ x : F64, y : F64 }), List({ x : F64, y : F64 }) -> { mean : F64, max : F64 }

How far the drawing moved between two layouts of the same nodes — the stability measure (F7): small numbers mean a reader's mental map survives a re-layout. Distances are between index-aligned position pairs; indices present in only one list are ignored, and no pairs at all reports { mean: 0, max: 0 }. Linear in the number of pairs.

stress : { nodes : List({ width : F64, height : F64 }), edges : List({ from : U64, to : U64 }) }, List({ x : F64, y : F64 }), F64 -> F64

How faithfully drawn distances reflect graph distances — 0 means every reachable pair sits exactly where the graph structure says it should; larger values mean the drawing compresses or stretches relationships. For every unordered pair of mutually reachable nodes, the ideal distance is the unweighted hop count times unit, and the pair contributes the squared relative error ((drawn - ideal) / ideal)^2 (the classic 1/ideal^2 stress weighting). Self pairs, unreachable pairs, pairs without a position, and pairs whose ideal distance is not positive and finite contribute nothing, so the result is defined and finite for any input — this is a metric, not a validator, and self-loops or out-of-range edge endpoints are simply skipped. Fewer than two nodes scores 0. O(n * (n + m)) for the per-node breadth-first searches plus O(n^2) pair summation.