@nodius/layouting 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,11 +1,32 @@
1
1
  // src/types.ts
2
+ var QUALITY_PROFILES = {
3
+ draft: {
4
+ crossingMinimizationIterations: 6,
5
+ coordinateOptimizationIterations: 2,
6
+ skipTranspose: true
7
+ },
8
+ balanced: {
9
+ crossingMinimizationIterations: 24,
10
+ coordinateOptimizationIterations: 8,
11
+ skipTranspose: false
12
+ },
13
+ high: {
14
+ crossingMinimizationIterations: 48,
15
+ coordinateOptimizationIterations: 16,
16
+ skipTranspose: false
17
+ }
18
+ };
2
19
  function resolveOptions(options) {
20
+ const quality = options?.quality ?? "balanced";
21
+ const profile = QUALITY_PROFILES[quality];
3
22
  return {
4
23
  direction: options?.direction ?? "TB",
24
+ quality,
5
25
  nodeSpacing: options?.nodeSpacing ?? 40,
6
26
  layerSpacing: options?.layerSpacing ?? 60,
7
- crossingMinimizationIterations: options?.crossingMinimizationIterations ?? 24,
8
- coordinateOptimizationIterations: options?.coordinateOptimizationIterations ?? 8,
27
+ crossingMinimizationIterations: options?.crossingMinimizationIterations ?? profile.crossingMinimizationIterations,
28
+ coordinateOptimizationIterations: options?.coordinateOptimizationIterations ?? profile.coordinateOptimizationIterations,
29
+ skipTranspose: options?.skipTranspose ?? profile.skipTranspose,
9
30
  edgeMargin: options?.edgeMargin ?? 20,
10
31
  controlWeight: options?.edgeWeights?.control ?? 1,
11
32
  dataWeight: options?.edgeWeights?.data ?? 0.25,
@@ -370,11 +391,11 @@ function insertDummyNodes(graph, layers) {
370
391
  }
371
392
 
372
393
  // src/algorithms/crossing-minimization.ts
373
- function minimizeCrossings(graph, layers, iterations) {
394
+ function minimizeCrossings(graph, layers, iterations, forceSkipTranspose = false) {
374
395
  if (layers.length <= 1) return layers;
375
396
  const totalNodes = layers.reduce((s, l) => s + l.length, 0);
376
397
  const effectiveIter = totalNodes > 500 ? Math.min(iterations, 6) : totalNodes > 200 ? Math.min(iterations, 12) : iterations;
377
- const skipTranspose = totalNodes > 800;
398
+ const skipTranspose = forceSkipTranspose || totalNodes > 800;
378
399
  for (let l = 0; l < layers.length; l++) {
379
400
  for (let i = 0; i < layers[l].length; i++) {
380
401
  const node = graph.nodes.get(layers[l][i]);
@@ -421,45 +442,54 @@ function orderByBarycenter(graph, layers, layerIndex, direction) {
421
442
  const layer = layers[layerIndex];
422
443
  const adjLayerIndex = direction === "up" ? layerIndex - 1 : layerIndex + 1;
423
444
  if (adjLayerIndex < 0 || adjLayerIndex >= layers.length) return;
424
- const adjLayerSet = new Set(layers[adjLayerIndex]);
425
- const barycenters = /* @__PURE__ */ new Map();
426
- for (let i = 0; i < layer.length; i++) {
427
- const nodeId = layer[i];
428
- let sum = 0;
429
- let count = 0;
430
- if (direction === "up") {
445
+ const n = layer.length;
446
+ const bary = new Float64Array(n);
447
+ if (direction === "up") {
448
+ for (let i = 0; i < n; i++) {
449
+ const nodeId = layer[i];
450
+ let sum = 0;
451
+ let count = 0;
431
452
  const inEdgeIds = graph.inEdges.get(nodeId);
432
453
  if (inEdgeIds) {
433
454
  for (const eid of inEdgeIds) {
434
455
  const edge = graph.edges.get(eid);
435
- if (edge && adjLayerSet.has(edge.from)) {
436
- const neighbor = graph.nodes.get(edge.from);
437
- if (neighbor) {
438
- sum += neighbor.order;
439
- count++;
440
- }
456
+ if (!edge) continue;
457
+ const neighbor = graph.nodes.get(edge.from);
458
+ if (neighbor && neighbor.layer === adjLayerIndex) {
459
+ sum += neighbor.order;
460
+ count++;
441
461
  }
442
462
  }
443
463
  }
444
- } else {
464
+ bary[i] = count > 0 ? sum / count : i;
465
+ }
466
+ } else {
467
+ for (let i = 0; i < n; i++) {
468
+ const nodeId = layer[i];
469
+ let sum = 0;
470
+ let count = 0;
445
471
  const outEdgeIds = graph.outEdges.get(nodeId);
446
472
  if (outEdgeIds) {
447
473
  for (const eid of outEdgeIds) {
448
474
  const edge = graph.edges.get(eid);
449
- if (edge && adjLayerSet.has(edge.to)) {
450
- const neighbor = graph.nodes.get(edge.to);
451
- if (neighbor) {
452
- sum += neighbor.order;
453
- count++;
454
- }
475
+ if (!edge) continue;
476
+ const neighbor = graph.nodes.get(edge.to);
477
+ if (neighbor && neighbor.layer === adjLayerIndex) {
478
+ sum += neighbor.order;
479
+ count++;
455
480
  }
456
481
  }
457
482
  }
483
+ bary[i] = count > 0 ? sum / count : i;
458
484
  }
459
- barycenters.set(nodeId, count > 0 ? sum / count : i);
460
485
  }
461
- layer.sort((a, b) => barycenters.get(a) - barycenters.get(b));
462
- for (let i = 0; i < layer.length; i++) {
486
+ const indices = new Array(n);
487
+ for (let i = 0; i < n; i++) indices[i] = i;
488
+ indices.sort((a, b) => bary[a] - bary[b]);
489
+ const reordered = new Array(n);
490
+ for (let i = 0; i < n; i++) reordered[i] = layer[indices[i]];
491
+ for (let i = 0; i < n; i++) {
492
+ layer[i] = reordered[i];
463
493
  const node = graph.nodes.get(layer[i]);
464
494
  if (node) node.order = i;
465
495
  }
@@ -494,65 +524,65 @@ function transposeImprove(graph, layers, layerIndex) {
494
524
  }
495
525
  }
496
526
  }
527
+ var _scratchA = [];
528
+ var _scratchB = [];
497
529
  function countPairCrossings(graph, layers, layerIndex, nodeA, nodeB) {
498
530
  let crossings = 0;
499
531
  if (layerIndex > 0) {
500
- const upperLayer = new Set(layers[layerIndex - 1]);
501
- const predsA = [];
502
- const predsB = [];
532
+ const upperIdx = layerIndex - 1;
533
+ _scratchA.length = 0;
534
+ _scratchB.length = 0;
503
535
  const inA = graph.inEdges.get(nodeA);
504
536
  if (inA) {
505
537
  for (const eid of inA) {
506
538
  const edge = graph.edges.get(eid);
507
- if (edge && upperLayer.has(edge.from)) {
508
- const n = graph.nodes.get(edge.from);
509
- if (n) predsA.push(n.order);
510
- }
539
+ if (!edge) continue;
540
+ const n = graph.nodes.get(edge.from);
541
+ if (n && n.layer === upperIdx) _scratchA.push(n.order);
511
542
  }
512
543
  }
513
544
  const inB = graph.inEdges.get(nodeB);
514
545
  if (inB) {
515
546
  for (const eid of inB) {
516
547
  const edge = graph.edges.get(eid);
517
- if (edge && upperLayer.has(edge.from)) {
518
- const n = graph.nodes.get(edge.from);
519
- if (n) predsB.push(n.order);
520
- }
548
+ if (!edge) continue;
549
+ const n = graph.nodes.get(edge.from);
550
+ if (n && n.layer === upperIdx) _scratchB.push(n.order);
521
551
  }
522
552
  }
523
- for (const pA of predsA) {
524
- for (const pB of predsB) {
525
- if (pA > pB) crossings++;
553
+ for (let i = 0; i < _scratchA.length; i++) {
554
+ const pA = _scratchA[i];
555
+ for (let j = 0; j < _scratchB.length; j++) {
556
+ if (pA > _scratchB[j]) crossings++;
526
557
  }
527
558
  }
528
559
  }
529
560
  if (layerIndex < layers.length - 1) {
530
- const lowerLayer = new Set(layers[layerIndex + 1]);
531
- const succsA = [];
532
- const succsB = [];
561
+ const lowerIdx = layerIndex + 1;
562
+ _scratchA.length = 0;
563
+ _scratchB.length = 0;
533
564
  const outA = graph.outEdges.get(nodeA);
534
565
  if (outA) {
535
566
  for (const eid of outA) {
536
567
  const edge = graph.edges.get(eid);
537
- if (edge && lowerLayer.has(edge.to)) {
538
- const n = graph.nodes.get(edge.to);
539
- if (n) succsA.push(n.order);
540
- }
568
+ if (!edge) continue;
569
+ const n = graph.nodes.get(edge.to);
570
+ if (n && n.layer === lowerIdx) _scratchA.push(n.order);
541
571
  }
542
572
  }
543
573
  const outB = graph.outEdges.get(nodeB);
544
574
  if (outB) {
545
575
  for (const eid of outB) {
546
576
  const edge = graph.edges.get(eid);
547
- if (edge && lowerLayer.has(edge.to)) {
548
- const n = graph.nodes.get(edge.to);
549
- if (n) succsB.push(n.order);
550
- }
577
+ if (!edge) continue;
578
+ const n = graph.nodes.get(edge.to);
579
+ if (n && n.layer === lowerIdx) _scratchB.push(n.order);
551
580
  }
552
581
  }
553
- for (const sA of succsA) {
554
- for (const sB of succsB) {
555
- if (sA > sB) crossings++;
582
+ for (let i = 0; i < _scratchA.length; i++) {
583
+ const sA = _scratchA[i];
584
+ for (let j = 0; j < _scratchB.length; j++) {
585
+ if (sA > _scratchB[j]) crossings++;
556
586
  }
557
587
  }
558
588
  }
@@ -588,22 +618,36 @@ function countLayerCrossings(graph, upperLayer, lowerLayer) {
588
618
  return mergeSortCount(lowerPositions);
589
619
  }
590
620
  function mergeSortCount(arr) {
591
- if (arr.length <= 1) return 0;
592
- const mid = arr.length >> 1;
593
- const left = arr.slice(0, mid);
594
- const right = arr.slice(mid);
595
- let count = mergeSortCount(left) + mergeSortCount(right);
596
- let i = 0, j = 0, k = 0;
597
- while (i < left.length && j < right.length) {
598
- if (left[i] <= right[j]) {
599
- arr[k++] = left[i++];
600
- } else {
601
- count += left.length - i;
602
- arr[k++] = right[j++];
621
+ const n = arr.length;
622
+ if (n <= 1) return 0;
623
+ let buf = new Array(n);
624
+ let src = arr;
625
+ let dst = buf;
626
+ let count = 0;
627
+ for (let width = 1; width < n; width <<= 1) {
628
+ for (let i = 0; i < n; i += width << 1) {
629
+ const left = i;
630
+ const mid = Math.min(i + width, n);
631
+ const right = Math.min(i + (width << 1), n);
632
+ let a = left, b = mid, k = left;
633
+ while (a < mid && b < right) {
634
+ if (src[a] <= src[b]) {
635
+ dst[k++] = src[a++];
636
+ } else {
637
+ count += mid - a;
638
+ dst[k++] = src[b++];
639
+ }
640
+ }
641
+ while (a < mid) dst[k++] = src[a++];
642
+ while (b < right) dst[k++] = src[b++];
603
643
  }
644
+ const tmp = src;
645
+ src = dst;
646
+ dst = tmp;
647
+ }
648
+ if (src !== arr) {
649
+ for (let i = 0; i < n; i++) arr[i] = src[i];
604
650
  }
605
- while (i < left.length) arr[k++] = left[i++];
606
- while (j < right.length) arr[k++] = right[j++];
607
651
  return count;
608
652
  }
609
653
 
@@ -687,37 +731,69 @@ function optimizePositions(graph, layers, options, isHorizontal) {
687
731
  }
688
732
  centerAllLayers(graph, layers, isHorizontal);
689
733
  }
734
+ var _neighborBuf = new Float64Array(64);
735
+ var _desiredBuf = new Float64Array(64);
736
+ function ensureBufSize(size) {
737
+ if (_neighborBuf.length < size) _neighborBuf = new Float64Array(size * 2);
738
+ if (_desiredBuf.length < size) _desiredBuf = new Float64Array(size * 2);
739
+ }
690
740
  function optimizeLayer(graph, layer, options, isHorizontal) {
691
- if (layer.length === 0) return;
692
- const desired = /* @__PURE__ */ new Map();
693
- for (const nodeId of layer) {
741
+ const n = layer.length;
742
+ if (n === 0) return;
743
+ let maxDegree = 0;
744
+ for (let i = 0; i < n; i++) {
745
+ const id = layer[i];
746
+ const d = (graph.inEdges.get(id)?.size ?? 0) + (graph.outEdges.get(id)?.size ?? 0);
747
+ if (d > maxDegree) maxDegree = d;
748
+ }
749
+ ensureBufSize(Math.max(n, maxDegree));
750
+ const desired = _desiredBuf;
751
+ for (let i = 0; i < n; i++) {
752
+ const nodeId = layer[i];
694
753
  const node = graph.nodes.get(nodeId);
695
- const connected = [...graph.predecessors(nodeId), ...graph.successors(nodeId)];
696
- if (connected.length === 0) {
697
- desired.set(nodeId, getOrderPos(node, isHorizontal));
754
+ let nbCount = 0;
755
+ const inEdgeIds = graph.inEdges.get(nodeId);
756
+ if (inEdgeIds) {
757
+ for (const eid of inEdgeIds) {
758
+ const edge = graph.edges.get(eid);
759
+ if (!edge) continue;
760
+ const c = graph.nodes.get(edge.from);
761
+ if (!c) continue;
762
+ _neighborBuf[nbCount++] = getOrderPos(c, isHorizontal) + getOrderSize(c, isHorizontal) / 2;
763
+ }
764
+ }
765
+ const outEdgeIds = graph.outEdges.get(nodeId);
766
+ if (outEdgeIds) {
767
+ for (const eid of outEdgeIds) {
768
+ const edge = graph.edges.get(eid);
769
+ if (!edge) continue;
770
+ const c = graph.nodes.get(edge.to);
771
+ if (!c) continue;
772
+ _neighborBuf[nbCount++] = getOrderPos(c, isHorizontal) + getOrderSize(c, isHorizontal) / 2;
773
+ }
774
+ }
775
+ if (nbCount === 0) {
776
+ desired[i] = getOrderPos(node, isHorizontal);
698
777
  continue;
699
778
  }
700
- const positions = connected.map((cId) => {
701
- const c = graph.nodes.get(cId);
702
- return getOrderPos(c, isHorizontal) + getOrderSize(c, isHorizontal) / 2;
703
- }).sort((a, b) => a - b);
779
+ sortPrefix(_neighborBuf, nbCount);
704
780
  const nodeSize = getOrderSize(node, isHorizontal);
705
- const median = positions.length % 2 === 0 ? (positions[positions.length / 2 - 1] + positions[positions.length / 2]) / 2 : positions[Math.floor(positions.length / 2)];
706
- desired.set(nodeId, median - nodeSize / 2);
781
+ const median = (nbCount & 1) === 0 ? (_neighborBuf[(nbCount >> 1) - 1] + _neighborBuf[nbCount >> 1]) / 2 : _neighborBuf[nbCount >> 1];
782
+ desired[i] = median - nodeSize / 2;
707
783
  }
708
- for (let i = 0; i < layer.length; i++) {
784
+ for (let i = 0; i < n; i++) {
709
785
  const nodeId = layer[i];
710
786
  const node = graph.nodes.get(nodeId);
711
- let desiredPos = desired.get(nodeId);
787
+ let desiredPos = desired[i];
712
788
  if (i > 0) {
713
789
  const prevId = layer[i - 1];
714
790
  const prev = graph.nodes.get(prevId);
715
791
  const prevEnd = getOrderPos(prev, isHorizontal) + getOrderSize(prev, isHorizontal);
716
- desiredPos = Math.max(desiredPos, prevEnd + options.nodeSpacing);
792
+ if (desiredPos < prevEnd + options.nodeSpacing) desiredPos = prevEnd + options.nodeSpacing;
717
793
  }
718
794
  setOrderPos(node, isHorizontal, desiredPos);
719
795
  }
720
- for (let i = layer.length - 2; i >= 0; i--) {
796
+ for (let i = n - 2; i >= 0; i--) {
721
797
  const nodeId = layer[i];
722
798
  const node = graph.nodes.get(nodeId);
723
799
  const nextId = layer[i + 1];
@@ -729,6 +805,17 @@ function optimizeLayer(graph, layer, options, isHorizontal) {
729
805
  }
730
806
  }
731
807
  }
808
+ function sortPrefix(arr, count) {
809
+ for (let i = 1; i < count; i++) {
810
+ const x = arr[i];
811
+ let j = i - 1;
812
+ while (j >= 0 && arr[j] > x) {
813
+ arr[j + 1] = arr[j];
814
+ j--;
815
+ }
816
+ arr[j + 1] = x;
817
+ }
818
+ }
732
819
  function centerAllLayers(graph, layers, isHorizontal) {
733
820
  if (layers.length === 0) return;
734
821
  let globalMin = Infinity;
@@ -1381,6 +1468,79 @@ function score(handles, expected) {
1381
1468
  }
1382
1469
  return { matched, total, matchRatio: total === 0 ? 1 : matched / total };
1383
1470
  }
1471
+ function applyRelocateProposals(input, preview, options) {
1472
+ if (!options.onProposal) return input;
1473
+ const previewIndex = new Map(preview.nodes.map((n) => [n.id, n]));
1474
+ const inputIndex = new Map(input.nodes.map((n) => [n.id, n]));
1475
+ const newNodes = input.nodes.map((node) => {
1476
+ const proposal = computeRelocateProposal(node, input.edges, inputIndex, previewIndex);
1477
+ if (!proposal) return node;
1478
+ const accepted = options.onProposal(proposal);
1479
+ return accepted ?? node;
1480
+ });
1481
+ return { nodes: newNodes, edges: input.edges };
1482
+ }
1483
+ function computeRelocateProposal(node, edges, inputIndex, previewIndex) {
1484
+ const selfPreview = previewIndex.get(node.id);
1485
+ if (!selfPreview) return null;
1486
+ if (node.handles.length === 0) return null;
1487
+ const usedHandleIds = /* @__PURE__ */ new Set();
1488
+ for (const e of edges) {
1489
+ if (e.from === node.id) usedHandleIds.add(e.fromHandle);
1490
+ if (e.to === node.id) usedHandleIds.add(e.toHandle);
1491
+ }
1492
+ if (usedHandleIds.size === 0) return null;
1493
+ const votesByHandle = /* @__PURE__ */ new Map();
1494
+ const selfCenter = {
1495
+ x: selfPreview.x + selfPreview.width / 2,
1496
+ y: selfPreview.y + selfPreview.height / 2
1497
+ };
1498
+ for (const e of edges) {
1499
+ const isFrom = e.from === node.id;
1500
+ const isTo = e.to === node.id;
1501
+ if (!isFrom && !isTo) continue;
1502
+ const handleId = isFrom ? e.fromHandle : e.toHandle;
1503
+ const neighborId = isFrom ? e.to : e.from;
1504
+ const neighborPreview = previewIndex.get(neighborId);
1505
+ if (!neighborPreview) continue;
1506
+ const neighborCenter = {
1507
+ x: neighborPreview.x + neighborPreview.width / 2,
1508
+ y: neighborPreview.y + neighborPreview.height / 2
1509
+ };
1510
+ const dx = neighborCenter.x - selfCenter.x;
1511
+ const dy = neighborCenter.y - selfCenter.y;
1512
+ const side = Math.abs(dx) >= Math.abs(dy) ? dx >= 0 ? "right" : "left" : dy >= 0 ? "bottom" : "top";
1513
+ const dist = Math.hypot(dx, dy);
1514
+ const weight = dist > 0 ? 1 / dist : 1;
1515
+ const arr = votesByHandle.get(handleId) ?? [];
1516
+ arr.push({ side, weight });
1517
+ votesByHandle.set(handleId, arr);
1518
+ }
1519
+ const changes = {};
1520
+ const newHandles = node.handles.map((h) => {
1521
+ const votes = votesByHandle.get(h.id);
1522
+ if (!votes || votes.length === 0) return h;
1523
+ const tallies = { top: 0, right: 0, bottom: 0, left: 0 };
1524
+ for (const v of votes) tallies[v.side] += v.weight;
1525
+ const best = Object.entries(tallies).reduce((acc, cur) => cur[1] > acc[1] ? cur : acc, ["top", -Infinity])[0];
1526
+ if (best !== h.position) {
1527
+ changes[h.id] = { from: h.position, to: best };
1528
+ return { ...h, position: best };
1529
+ }
1530
+ return h;
1531
+ });
1532
+ if (Object.keys(changes).length === 0) return null;
1533
+ const proposed = { ...node, handles: newHandles };
1534
+ const summary = Object.entries(changes).map(([id, c]) => `${id}: ${c.from}\u2192${c.to}`).join(", ");
1535
+ return {
1536
+ type: "relocate-handles",
1537
+ nodeId: node.id,
1538
+ current: node,
1539
+ proposed,
1540
+ changes,
1541
+ reason: `${Object.keys(changes).length} handle(s) point away from their neighbor (${summary})`
1542
+ };
1543
+ }
1384
1544
  function rotateHandles(handles, rot) {
1385
1545
  const rotateSide = (s) => {
1386
1546
  if (rot === 180) {
@@ -1399,7 +1559,12 @@ var COMPOUND_HEADER = 28;
1399
1559
  function layout(input, options) {
1400
1560
  const resolved = resolveOptions(options);
1401
1561
  if (input.nodes.length === 0) return { nodes: [], edges: [] };
1402
- const adjusted = applyRotationProposals(input, resolved);
1562
+ let adjusted = applyRotationProposals(input, resolved);
1563
+ if (resolved.onProposal) {
1564
+ const previewOptions = { ...resolved, onProposal: void 0 };
1565
+ const preview = layoutCompound(adjusted, previewOptions);
1566
+ adjusted = applyRelocateProposals(adjusted, preview, resolved);
1567
+ }
1403
1568
  return layoutCompound(adjusted, resolved);
1404
1569
  }
1405
1570
  function computeLayout(graph, options) {
@@ -1408,7 +1573,12 @@ function computeLayout(graph, options) {
1408
1573
  let layers = assignLayers(graph);
1409
1574
  layers = insertDummyNodes(graph, layers);
1410
1575
  let rail = railLayers(layers, graph);
1411
- rail = minimizeCrossings(graph, rail, options.crossingMinimizationIterations);
1576
+ rail = minimizeCrossings(
1577
+ graph,
1578
+ rail,
1579
+ options.crossingMinimizationIterations,
1580
+ options.skipTranspose
1581
+ );
1412
1582
  assignCoordinates(graph, rail, options);
1413
1583
  placeValueSidecars(graph, layers, options);
1414
1584
  const routedEdges = routeEdges(graph, options.direction, options.edgeMargin);