@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/README.md +948 -132
- package/dist/algorithms/crossing-minimization.d.ts +5 -1
- package/dist/algorithms/crossing-minimization.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +257 -87
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +257 -87
- package/dist/index.mjs.map +1 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/proposals.d.ts +14 -1
- package/dist/proposals.d.ts.map +1 -1
- package/dist/types.d.ts +60 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
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 ??
|
|
8
|
-
coordinateOptimizationIterations: options?.coordinateOptimizationIterations ??
|
|
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
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
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
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
462
|
-
for (let i = 0; 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
|
|
501
|
-
|
|
502
|
-
|
|
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
|
|
508
|
-
|
|
509
|
-
|
|
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
|
|
518
|
-
|
|
519
|
-
|
|
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 (
|
|
524
|
-
|
|
525
|
-
|
|
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
|
|
531
|
-
|
|
532
|
-
|
|
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
|
|
538
|
-
|
|
539
|
-
|
|
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
|
|
548
|
-
|
|
549
|
-
|
|
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 (
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
let
|
|
596
|
-
let
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
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 =
|
|
706
|
-
desired
|
|
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 <
|
|
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
|
|
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
|
|
792
|
+
if (desiredPos < prevEnd + options.nodeSpacing) desiredPos = prevEnd + options.nodeSpacing;
|
|
717
793
|
}
|
|
718
794
|
setOrderPos(node, isHorizontal, desiredPos);
|
|
719
795
|
}
|
|
720
|
-
for (let 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
|
-
|
|
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(
|
|
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);
|