Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | 24x 24x 22x 118x 118x 160x 22x 22x 22x 32x 32x 22x 118x 118x 160x 160x 22x 22x 96x 96x 96x 114x 114x 114x 114x 204x 204x 128x 128x 128x 114x 114x 114x 96x 26x 26x 26x 26x 96x 114x 96x 96x 114x 114x 114x 96x 96x 10x 10x 48x 48x 48x 84x 84x 84x 84x 126x 126x 80x 80x 80x 84x 66x 18x 84x 48x 36x 36x 36x 36x 48x 84x 48x 48x 84x 84x 84x 48x 48x | import type { LayoutState, Layer, LayoutNode } from "./types";
/**
* Minimize edge crossings using size-aware barycenter heuristic
* Uses cumulative height positions instead of simple indices
* Alternates forward and backward sweeps
*/
export function minimizeCrossings(state: LayoutState): void {
const { layers, nodes, config } = state;
if (layers.length < 2) return;
// Initial ordering: sort by node height (tall nodes first reduces gaps)
for (const layer of layers) {
layer.nodes.sort((a, b) => b.height - a.height);
layer.nodes.forEach((n, i) => {
n.orderInLayer = i;
});
}
// Pre-compute cumulative height positions for size-aware barycenter
const heightPositions = new Map<number, number>();
updateHeightPositions(layers, heightPositions, config.verticalGap);
// Iterative improvement with forward/backward sweeps
for (let iter = 0; iter < config.maxIterations; iter++) {
const improved =
iter % 2 === 0
? sweepForward(layers, nodes, heightPositions, config.verticalGap)
: sweepBackward(layers, nodes, heightPositions, config.verticalGap);
if (!improved) break;
}
}
/**
* Compute cumulative height positions for each node
* This gives a position-like weight for size-aware ordering
*/
function updateHeightPositions(
layers: Layer[],
positions: Map<number, number>,
verticalGap: number
): void {
for (const layer of layers) {
let y = 0;
for (const node of layer.nodes) {
// Position is center of node
positions.set(node.id, y + node.height / 2);
y += node.height + verticalGap;
}
}
}
/**
* Forward sweep: order each layer based on predecessors in previous layer
* Uses height-aware positions for better size consideration
*/
function sweepForward(
layers: Layer[],
nodes: Map<number, LayoutNode>,
heightPositions: Map<number, number>,
verticalGap: number
): boolean {
let improved = false;
for (let i = 1; i < layers.length; i++) {
const currLayer = layers[i];
// Compute size-aware barycenter for each node
const barycenters = new Map<number, number>();
for (const node of currLayer.nodes) {
if (node.predecessors.length > 0) {
let sum = 0;
let count = 0;
for (const predId of node.predecessors) {
const pred = nodes.get(predId);
if (pred && pred.layer === i - 1) {
// Use height position instead of simple index
const pos = heightPositions.get(predId) ?? pred.orderInLayer;
sum += pos;
count++;
}
}
if (count > 0) {
barycenters.set(node.id, sum / count);
} else E{
barycenters.set(node.id, heightPositions.get(node.id) ?? 0);
}
} else E{
barycenters.set(node.id, heightPositions.get(node.id) ?? 0);
}
}
// Sort by barycenter, tie-break by ID for determinism
const oldOrder = currLayer.nodes.map((n) => n.id).join(",");
currLayer.nodes.sort((a, b) => {
const aBar = barycenters.get(a.id) ?? 0;
const bBar = barycenters.get(b.id) ?? 0;
const diff = aBar - bBar;
return diff !== 0 ? diff : a.id - b.id;
});
// Reassign integer order
currLayer.nodes.forEach((n, idx) => {
n.orderInLayer = idx;
});
// Update height positions for next iteration
let y = 0;
for (const node of currLayer.nodes) {
heightPositions.set(node.id, y + node.height / 2);
y += node.height + verticalGap;
}
const newOrder = currLayer.nodes.map((n) => n.id).join(",");
if (oldOrder !== newOrder) improved = true;
}
return improved;
}
/**
* Backward sweep: order each layer based on successors in next layer
* Uses height-aware positions for better size consideration
*/
function sweepBackward(
layers: Layer[],
nodes: Map<number, LayoutNode>,
heightPositions: Map<number, number>,
verticalGap: number
): boolean {
let improved = false;
for (let i = layers.length - 2; i >= 0; i--) {
const currLayer = layers[i];
// Compute size-aware barycenter for each node based on successors
const barycenters = new Map<number, number>();
for (const node of currLayer.nodes) {
if (node.successors.length > 0) {
let sum = 0;
let count = 0;
for (const succId of node.successors) {
const succ = nodes.get(succId);
if (succ && succ.layer === i + 1) {
// Use height position instead of simple index
const pos = heightPositions.get(succId) ?? succ.orderInLayer;
sum += pos;
count++;
}
}
if (count > 0) {
barycenters.set(node.id, sum / count);
} else {
barycenters.set(node.id, heightPositions.get(node.id) ?? 0);
}
} else E{
barycenters.set(node.id, heightPositions.get(node.id) ?? 0);
}
}
// Sort by barycenter, tie-break by ID for determinism
const oldOrder = currLayer.nodes.map((n) => n.id).join(",");
currLayer.nodes.sort((a, b) => {
const aBar = barycenters.get(a.id) ?? 0;
const bBar = barycenters.get(b.id) ?? 0;
const diff = aBar - bBar;
return diff !== 0 ? diff : a.id - b.id;
});
// Reassign integer order
currLayer.nodes.forEach((n, idx) => {
n.orderInLayer = idx;
});
// Update height positions for next iteration
let y = 0;
for (const node of currLayer.nodes) {
heightPositions.set(node.id, y + node.height / 2);
y += node.height + verticalGap;
}
const newOrder = currLayer.nodes.map((n) => n.id).join(",");
Iif (oldOrder !== newOrder) improved = true;
}
return improved;
}
|