All files / src/layout ordering.ts

95.74% Statements 90/94
66% Branches 33/50
100% Functions 14/14
96.34% Lines 79/82

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