@nodius/layouting 0.1.1 → 0.1.4
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 +1136 -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 +334 -87
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +334 -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
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { Graph } from '../graph';
|
|
2
2
|
/**
|
|
3
3
|
* Minimize edge crossings using barycenter heuristic with transpose improvement.
|
|
4
|
+
*
|
|
5
|
+
* @param iterations Maximum number of barycenter sweep iterations.
|
|
6
|
+
* @param forceSkipTranspose When true, never run the transpose pass — used by
|
|
7
|
+
* the 'draft' quality preset for ~3× speedup at the cost of some crossings.
|
|
4
8
|
*/
|
|
5
|
-
export declare function minimizeCrossings(graph: Graph, layers: string[][], iterations: number): string[][];
|
|
9
|
+
export declare function minimizeCrossings(graph: Graph, layers: string[][], iterations: number, forceSkipTranspose?: boolean): string[][];
|
|
6
10
|
export declare function countAllCrossings(graph: Graph, layers: string[][]): number;
|
|
7
11
|
//# sourceMappingURL=crossing-minimization.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crossing-minimization.d.ts","sourceRoot":"","sources":["../../src/algorithms/crossing-minimization.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEjC
|
|
1
|
+
{"version":3,"file":"crossing-minimization.d.ts","sourceRoot":"","sources":["../../src/algorithms/crossing-minimization.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEjC;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EAAE,EAAE,EAClB,UAAU,EAAE,MAAM,EAClB,kBAAkB,GAAE,OAAe,GAClC,MAAM,EAAE,EAAE,CAqEZ;AA8MD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,GAAG,MAAM,CAM1E"}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ export { layout } from './layout';
|
|
|
2
2
|
export { IncrementalLayout } from './incremental';
|
|
3
3
|
export { countAllCrossings } from './algorithms/crossing-minimization';
|
|
4
4
|
export { printLayout } from './debug';
|
|
5
|
-
export type { HandleSide, HandleType, LayoutDirection, EdgeKind, EdgeWeights, Point, HandleInput, NodeInput, EdgeInput, LayoutInput, LayoutOptions, HandleOutput, NodeOutput, EdgeOutput, LayoutResult, LayoutProposal, RotateProposal, ProposalCallback, } from './types';
|
|
5
|
+
export type { HandleSide, HandleType, LayoutDirection, LayoutQuality, EdgeKind, EdgeWeights, Point, HandleInput, NodeInput, EdgeInput, LayoutInput, LayoutOptions, HandleOutput, NodeOutput, EdgeOutput, LayoutResult, LayoutProposal, RotateProposal, RelocateHandlesProposal, ProposalCallback, } from './types';
|
|
6
6
|
export { rotateHandles } from './proposals';
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtC,YAAY,EACV,UAAU,EACV,UAAU,EACV,eAAe,EACf,QAAQ,EACR,WAAW,EACX,KAAK,EACL,WAAW,EACX,SAAS,EACT,SAAS,EACT,WAAW,EACX,aAAa,EACb,YAAY,EACZ,UAAU,EACV,UAAU,EACV,YAAY,EACZ,cAAc,EACd,cAAc,EACd,gBAAgB,GACjB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtC,YAAY,EACV,UAAU,EACV,UAAU,EACV,eAAe,EACf,aAAa,EACb,QAAQ,EACR,WAAW,EACX,KAAK,EACL,WAAW,EACX,SAAS,EACT,SAAS,EACT,WAAW,EACX,aAAa,EACb,YAAY,EACZ,UAAU,EACV,UAAU,EACV,YAAY,EACZ,cAAc,EACd,cAAc,EACd,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -29,13 +29,34 @@ __export(index_exports, {
|
|
|
29
29
|
module.exports = __toCommonJS(index_exports);
|
|
30
30
|
|
|
31
31
|
// src/types.ts
|
|
32
|
+
var QUALITY_PROFILES = {
|
|
33
|
+
draft: {
|
|
34
|
+
crossingMinimizationIterations: 6,
|
|
35
|
+
coordinateOptimizationIterations: 2,
|
|
36
|
+
skipTranspose: true
|
|
37
|
+
},
|
|
38
|
+
balanced: {
|
|
39
|
+
crossingMinimizationIterations: 24,
|
|
40
|
+
coordinateOptimizationIterations: 8,
|
|
41
|
+
skipTranspose: false
|
|
42
|
+
},
|
|
43
|
+
high: {
|
|
44
|
+
crossingMinimizationIterations: 48,
|
|
45
|
+
coordinateOptimizationIterations: 16,
|
|
46
|
+
skipTranspose: false
|
|
47
|
+
}
|
|
48
|
+
};
|
|
32
49
|
function resolveOptions(options) {
|
|
50
|
+
const quality = options?.quality ?? "balanced";
|
|
51
|
+
const profile = QUALITY_PROFILES[quality];
|
|
33
52
|
return {
|
|
34
53
|
direction: options?.direction ?? "TB",
|
|
54
|
+
quality,
|
|
35
55
|
nodeSpacing: options?.nodeSpacing ?? 40,
|
|
36
56
|
layerSpacing: options?.layerSpacing ?? 60,
|
|
37
|
-
crossingMinimizationIterations: options?.crossingMinimizationIterations ??
|
|
38
|
-
coordinateOptimizationIterations: options?.coordinateOptimizationIterations ??
|
|
57
|
+
crossingMinimizationIterations: options?.crossingMinimizationIterations ?? profile.crossingMinimizationIterations,
|
|
58
|
+
coordinateOptimizationIterations: options?.coordinateOptimizationIterations ?? profile.coordinateOptimizationIterations,
|
|
59
|
+
skipTranspose: options?.skipTranspose ?? profile.skipTranspose,
|
|
39
60
|
edgeMargin: options?.edgeMargin ?? 20,
|
|
40
61
|
controlWeight: options?.edgeWeights?.control ?? 1,
|
|
41
62
|
dataWeight: options?.edgeWeights?.data ?? 0.25,
|
|
@@ -400,11 +421,11 @@ function insertDummyNodes(graph, layers) {
|
|
|
400
421
|
}
|
|
401
422
|
|
|
402
423
|
// src/algorithms/crossing-minimization.ts
|
|
403
|
-
function minimizeCrossings(graph, layers, iterations) {
|
|
424
|
+
function minimizeCrossings(graph, layers, iterations, forceSkipTranspose = false) {
|
|
404
425
|
if (layers.length <= 1) return layers;
|
|
405
426
|
const totalNodes = layers.reduce((s, l) => s + l.length, 0);
|
|
406
427
|
const effectiveIter = totalNodes > 500 ? Math.min(iterations, 6) : totalNodes > 200 ? Math.min(iterations, 12) : iterations;
|
|
407
|
-
const skipTranspose = totalNodes > 800;
|
|
428
|
+
const skipTranspose = forceSkipTranspose || totalNodes > 800;
|
|
408
429
|
for (let l = 0; l < layers.length; l++) {
|
|
409
430
|
for (let i = 0; i < layers[l].length; i++) {
|
|
410
431
|
const node = graph.nodes.get(layers[l][i]);
|
|
@@ -451,45 +472,54 @@ function orderByBarycenter(graph, layers, layerIndex, direction) {
|
|
|
451
472
|
const layer = layers[layerIndex];
|
|
452
473
|
const adjLayerIndex = direction === "up" ? layerIndex - 1 : layerIndex + 1;
|
|
453
474
|
if (adjLayerIndex < 0 || adjLayerIndex >= layers.length) return;
|
|
454
|
-
const
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
475
|
+
const n = layer.length;
|
|
476
|
+
const bary = new Float64Array(n);
|
|
477
|
+
if (direction === "up") {
|
|
478
|
+
for (let i = 0; i < n; i++) {
|
|
479
|
+
const nodeId = layer[i];
|
|
480
|
+
let sum = 0;
|
|
481
|
+
let count = 0;
|
|
461
482
|
const inEdgeIds = graph.inEdges.get(nodeId);
|
|
462
483
|
if (inEdgeIds) {
|
|
463
484
|
for (const eid of inEdgeIds) {
|
|
464
485
|
const edge = graph.edges.get(eid);
|
|
465
|
-
if (edge
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
486
|
+
if (!edge) continue;
|
|
487
|
+
const neighbor = graph.nodes.get(edge.from);
|
|
488
|
+
if (neighbor && neighbor.layer === adjLayerIndex) {
|
|
489
|
+
sum += neighbor.order;
|
|
490
|
+
count++;
|
|
471
491
|
}
|
|
472
492
|
}
|
|
473
493
|
}
|
|
474
|
-
|
|
494
|
+
bary[i] = count > 0 ? sum / count : i;
|
|
495
|
+
}
|
|
496
|
+
} else {
|
|
497
|
+
for (let i = 0; i < n; i++) {
|
|
498
|
+
const nodeId = layer[i];
|
|
499
|
+
let sum = 0;
|
|
500
|
+
let count = 0;
|
|
475
501
|
const outEdgeIds = graph.outEdges.get(nodeId);
|
|
476
502
|
if (outEdgeIds) {
|
|
477
503
|
for (const eid of outEdgeIds) {
|
|
478
504
|
const edge = graph.edges.get(eid);
|
|
479
|
-
if (edge
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
505
|
+
if (!edge) continue;
|
|
506
|
+
const neighbor = graph.nodes.get(edge.to);
|
|
507
|
+
if (neighbor && neighbor.layer === adjLayerIndex) {
|
|
508
|
+
sum += neighbor.order;
|
|
509
|
+
count++;
|
|
485
510
|
}
|
|
486
511
|
}
|
|
487
512
|
}
|
|
513
|
+
bary[i] = count > 0 ? sum / count : i;
|
|
488
514
|
}
|
|
489
|
-
barycenters.set(nodeId, count > 0 ? sum / count : i);
|
|
490
515
|
}
|
|
491
|
-
|
|
492
|
-
for (let i = 0; i <
|
|
516
|
+
const indices = new Array(n);
|
|
517
|
+
for (let i = 0; i < n; i++) indices[i] = i;
|
|
518
|
+
indices.sort((a, b) => bary[a] - bary[b]);
|
|
519
|
+
const reordered = new Array(n);
|
|
520
|
+
for (let i = 0; i < n; i++) reordered[i] = layer[indices[i]];
|
|
521
|
+
for (let i = 0; i < n; i++) {
|
|
522
|
+
layer[i] = reordered[i];
|
|
493
523
|
const node = graph.nodes.get(layer[i]);
|
|
494
524
|
if (node) node.order = i;
|
|
495
525
|
}
|
|
@@ -524,65 +554,65 @@ function transposeImprove(graph, layers, layerIndex) {
|
|
|
524
554
|
}
|
|
525
555
|
}
|
|
526
556
|
}
|
|
557
|
+
var _scratchA = [];
|
|
558
|
+
var _scratchB = [];
|
|
527
559
|
function countPairCrossings(graph, layers, layerIndex, nodeA, nodeB) {
|
|
528
560
|
let crossings = 0;
|
|
529
561
|
if (layerIndex > 0) {
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
562
|
+
const upperIdx = layerIndex - 1;
|
|
563
|
+
_scratchA.length = 0;
|
|
564
|
+
_scratchB.length = 0;
|
|
533
565
|
const inA = graph.inEdges.get(nodeA);
|
|
534
566
|
if (inA) {
|
|
535
567
|
for (const eid of inA) {
|
|
536
568
|
const edge = graph.edges.get(eid);
|
|
537
|
-
if (edge
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
569
|
+
if (!edge) continue;
|
|
570
|
+
const n = graph.nodes.get(edge.from);
|
|
571
|
+
if (n && n.layer === upperIdx) _scratchA.push(n.order);
|
|
541
572
|
}
|
|
542
573
|
}
|
|
543
574
|
const inB = graph.inEdges.get(nodeB);
|
|
544
575
|
if (inB) {
|
|
545
576
|
for (const eid of inB) {
|
|
546
577
|
const edge = graph.edges.get(eid);
|
|
547
|
-
if (edge
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}
|
|
578
|
+
if (!edge) continue;
|
|
579
|
+
const n = graph.nodes.get(edge.from);
|
|
580
|
+
if (n && n.layer === upperIdx) _scratchB.push(n.order);
|
|
551
581
|
}
|
|
552
582
|
}
|
|
553
|
-
for (
|
|
554
|
-
|
|
555
|
-
|
|
583
|
+
for (let i = 0; i < _scratchA.length; i++) {
|
|
584
|
+
const pA = _scratchA[i];
|
|
585
|
+
for (let j = 0; j < _scratchB.length; j++) {
|
|
586
|
+
if (pA > _scratchB[j]) crossings++;
|
|
556
587
|
}
|
|
557
588
|
}
|
|
558
589
|
}
|
|
559
590
|
if (layerIndex < layers.length - 1) {
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
591
|
+
const lowerIdx = layerIndex + 1;
|
|
592
|
+
_scratchA.length = 0;
|
|
593
|
+
_scratchB.length = 0;
|
|
563
594
|
const outA = graph.outEdges.get(nodeA);
|
|
564
595
|
if (outA) {
|
|
565
596
|
for (const eid of outA) {
|
|
566
597
|
const edge = graph.edges.get(eid);
|
|
567
|
-
if (edge
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
598
|
+
if (!edge) continue;
|
|
599
|
+
const n = graph.nodes.get(edge.to);
|
|
600
|
+
if (n && n.layer === lowerIdx) _scratchA.push(n.order);
|
|
571
601
|
}
|
|
572
602
|
}
|
|
573
603
|
const outB = graph.outEdges.get(nodeB);
|
|
574
604
|
if (outB) {
|
|
575
605
|
for (const eid of outB) {
|
|
576
606
|
const edge = graph.edges.get(eid);
|
|
577
|
-
if (edge
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
}
|
|
607
|
+
if (!edge) continue;
|
|
608
|
+
const n = graph.nodes.get(edge.to);
|
|
609
|
+
if (n && n.layer === lowerIdx) _scratchB.push(n.order);
|
|
581
610
|
}
|
|
582
611
|
}
|
|
583
|
-
for (
|
|
584
|
-
|
|
585
|
-
|
|
612
|
+
for (let i = 0; i < _scratchA.length; i++) {
|
|
613
|
+
const sA = _scratchA[i];
|
|
614
|
+
for (let j = 0; j < _scratchB.length; j++) {
|
|
615
|
+
if (sA > _scratchB[j]) crossings++;
|
|
586
616
|
}
|
|
587
617
|
}
|
|
588
618
|
}
|
|
@@ -618,22 +648,36 @@ function countLayerCrossings(graph, upperLayer, lowerLayer) {
|
|
|
618
648
|
return mergeSortCount(lowerPositions);
|
|
619
649
|
}
|
|
620
650
|
function mergeSortCount(arr) {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
let
|
|
626
|
-
let
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
651
|
+
const n = arr.length;
|
|
652
|
+
if (n <= 1) return 0;
|
|
653
|
+
let buf = new Array(n);
|
|
654
|
+
let src = arr;
|
|
655
|
+
let dst = buf;
|
|
656
|
+
let count = 0;
|
|
657
|
+
for (let width = 1; width < n; width <<= 1) {
|
|
658
|
+
for (let i = 0; i < n; i += width << 1) {
|
|
659
|
+
const left = i;
|
|
660
|
+
const mid = Math.min(i + width, n);
|
|
661
|
+
const right = Math.min(i + (width << 1), n);
|
|
662
|
+
let a = left, b = mid, k = left;
|
|
663
|
+
while (a < mid && b < right) {
|
|
664
|
+
if (src[a] <= src[b]) {
|
|
665
|
+
dst[k++] = src[a++];
|
|
666
|
+
} else {
|
|
667
|
+
count += mid - a;
|
|
668
|
+
dst[k++] = src[b++];
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
while (a < mid) dst[k++] = src[a++];
|
|
672
|
+
while (b < right) dst[k++] = src[b++];
|
|
633
673
|
}
|
|
674
|
+
const tmp = src;
|
|
675
|
+
src = dst;
|
|
676
|
+
dst = tmp;
|
|
677
|
+
}
|
|
678
|
+
if (src !== arr) {
|
|
679
|
+
for (let i = 0; i < n; i++) arr[i] = src[i];
|
|
634
680
|
}
|
|
635
|
-
while (i < left.length) arr[k++] = left[i++];
|
|
636
|
-
while (j < right.length) arr[k++] = right[j++];
|
|
637
681
|
return count;
|
|
638
682
|
}
|
|
639
683
|
|
|
@@ -717,37 +761,69 @@ function optimizePositions(graph, layers, options, isHorizontal) {
|
|
|
717
761
|
}
|
|
718
762
|
centerAllLayers(graph, layers, isHorizontal);
|
|
719
763
|
}
|
|
764
|
+
var _neighborBuf = new Float64Array(64);
|
|
765
|
+
var _desiredBuf = new Float64Array(64);
|
|
766
|
+
function ensureBufSize(size) {
|
|
767
|
+
if (_neighborBuf.length < size) _neighborBuf = new Float64Array(size * 2);
|
|
768
|
+
if (_desiredBuf.length < size) _desiredBuf = new Float64Array(size * 2);
|
|
769
|
+
}
|
|
720
770
|
function optimizeLayer(graph, layer, options, isHorizontal) {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
771
|
+
const n = layer.length;
|
|
772
|
+
if (n === 0) return;
|
|
773
|
+
let maxDegree = 0;
|
|
774
|
+
for (let i = 0; i < n; i++) {
|
|
775
|
+
const id = layer[i];
|
|
776
|
+
const d = (graph.inEdges.get(id)?.size ?? 0) + (graph.outEdges.get(id)?.size ?? 0);
|
|
777
|
+
if (d > maxDegree) maxDegree = d;
|
|
778
|
+
}
|
|
779
|
+
ensureBufSize(Math.max(n, maxDegree));
|
|
780
|
+
const desired = _desiredBuf;
|
|
781
|
+
for (let i = 0; i < n; i++) {
|
|
782
|
+
const nodeId = layer[i];
|
|
724
783
|
const node = graph.nodes.get(nodeId);
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
784
|
+
let nbCount = 0;
|
|
785
|
+
const inEdgeIds = graph.inEdges.get(nodeId);
|
|
786
|
+
if (inEdgeIds) {
|
|
787
|
+
for (const eid of inEdgeIds) {
|
|
788
|
+
const edge = graph.edges.get(eid);
|
|
789
|
+
if (!edge) continue;
|
|
790
|
+
const c = graph.nodes.get(edge.from);
|
|
791
|
+
if (!c) continue;
|
|
792
|
+
_neighborBuf[nbCount++] = getOrderPos(c, isHorizontal) + getOrderSize(c, isHorizontal) / 2;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
const outEdgeIds = graph.outEdges.get(nodeId);
|
|
796
|
+
if (outEdgeIds) {
|
|
797
|
+
for (const eid of outEdgeIds) {
|
|
798
|
+
const edge = graph.edges.get(eid);
|
|
799
|
+
if (!edge) continue;
|
|
800
|
+
const c = graph.nodes.get(edge.to);
|
|
801
|
+
if (!c) continue;
|
|
802
|
+
_neighborBuf[nbCount++] = getOrderPos(c, isHorizontal) + getOrderSize(c, isHorizontal) / 2;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (nbCount === 0) {
|
|
806
|
+
desired[i] = getOrderPos(node, isHorizontal);
|
|
728
807
|
continue;
|
|
729
808
|
}
|
|
730
|
-
|
|
731
|
-
const c = graph.nodes.get(cId);
|
|
732
|
-
return getOrderPos(c, isHorizontal) + getOrderSize(c, isHorizontal) / 2;
|
|
733
|
-
}).sort((a, b) => a - b);
|
|
809
|
+
sortPrefix(_neighborBuf, nbCount);
|
|
734
810
|
const nodeSize = getOrderSize(node, isHorizontal);
|
|
735
|
-
const median =
|
|
736
|
-
desired
|
|
811
|
+
const median = (nbCount & 1) === 0 ? (_neighborBuf[(nbCount >> 1) - 1] + _neighborBuf[nbCount >> 1]) / 2 : _neighborBuf[nbCount >> 1];
|
|
812
|
+
desired[i] = median - nodeSize / 2;
|
|
737
813
|
}
|
|
738
|
-
for (let i = 0; i <
|
|
814
|
+
for (let i = 0; i < n; i++) {
|
|
739
815
|
const nodeId = layer[i];
|
|
740
816
|
const node = graph.nodes.get(nodeId);
|
|
741
|
-
let desiredPos = desired
|
|
817
|
+
let desiredPos = desired[i];
|
|
742
818
|
if (i > 0) {
|
|
743
819
|
const prevId = layer[i - 1];
|
|
744
820
|
const prev = graph.nodes.get(prevId);
|
|
745
821
|
const prevEnd = getOrderPos(prev, isHorizontal) + getOrderSize(prev, isHorizontal);
|
|
746
|
-
desiredPos
|
|
822
|
+
if (desiredPos < prevEnd + options.nodeSpacing) desiredPos = prevEnd + options.nodeSpacing;
|
|
747
823
|
}
|
|
748
824
|
setOrderPos(node, isHorizontal, desiredPos);
|
|
749
825
|
}
|
|
750
|
-
for (let i =
|
|
826
|
+
for (let i = n - 2; i >= 0; i--) {
|
|
751
827
|
const nodeId = layer[i];
|
|
752
828
|
const node = graph.nodes.get(nodeId);
|
|
753
829
|
const nextId = layer[i + 1];
|
|
@@ -759,6 +835,17 @@ function optimizeLayer(graph, layer, options, isHorizontal) {
|
|
|
759
835
|
}
|
|
760
836
|
}
|
|
761
837
|
}
|
|
838
|
+
function sortPrefix(arr, count) {
|
|
839
|
+
for (let i = 1; i < count; i++) {
|
|
840
|
+
const x = arr[i];
|
|
841
|
+
let j = i - 1;
|
|
842
|
+
while (j >= 0 && arr[j] > x) {
|
|
843
|
+
arr[j + 1] = arr[j];
|
|
844
|
+
j--;
|
|
845
|
+
}
|
|
846
|
+
arr[j + 1] = x;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
762
849
|
function centerAllLayers(graph, layers, isHorizontal) {
|
|
763
850
|
if (layers.length === 0) return;
|
|
764
851
|
let globalMin = Infinity;
|
|
@@ -1411,6 +1498,156 @@ function score(handles, expected) {
|
|
|
1411
1498
|
}
|
|
1412
1499
|
return { matched, total, matchRatio: total === 0 ? 1 : matched / total };
|
|
1413
1500
|
}
|
|
1501
|
+
function applyRelocateProposals(input, preview, options) {
|
|
1502
|
+
if (!options.onProposal) return input;
|
|
1503
|
+
const previewIndex = new Map(preview.nodes.map((n) => [n.id, n]));
|
|
1504
|
+
const inputIndex = new Map(input.nodes.map((n) => [n.id, n]));
|
|
1505
|
+
const newNodes = input.nodes.map((node) => {
|
|
1506
|
+
const proposal = computeRelocateProposal(node, input.edges, inputIndex, previewIndex, options.direction);
|
|
1507
|
+
if (!proposal) return node;
|
|
1508
|
+
const accepted = options.onProposal(proposal);
|
|
1509
|
+
return accepted ?? node;
|
|
1510
|
+
});
|
|
1511
|
+
return { nodes: newNodes, edges: input.edges };
|
|
1512
|
+
}
|
|
1513
|
+
function computeRelocateProposal(node, edges, inputIndex, previewIndex, direction) {
|
|
1514
|
+
const selfPreview = previewIndex.get(node.id);
|
|
1515
|
+
if (!selfPreview) return null;
|
|
1516
|
+
if (node.handles.length === 0) return null;
|
|
1517
|
+
const isHorizontal = direction === "LR" || direction === "RL";
|
|
1518
|
+
const rankMin = isHorizontal ? selfPreview.x : selfPreview.y;
|
|
1519
|
+
const rankMax = isHorizontal ? selfPreview.x + selfPreview.width : selfPreview.y + selfPreview.height;
|
|
1520
|
+
const orderMin = isHorizontal ? selfPreview.y : selfPreview.x;
|
|
1521
|
+
const orderMax = isHorizontal ? selfPreview.y + selfPreview.height : selfPreview.x + selfPreview.width;
|
|
1522
|
+
const orderCenter = (orderMin + orderMax) / 2;
|
|
1523
|
+
const usedHandleIds = /* @__PURE__ */ new Set();
|
|
1524
|
+
for (const e of edges) {
|
|
1525
|
+
if (e.from === node.id) usedHandleIds.add(e.fromHandle);
|
|
1526
|
+
if (e.to === node.id) usedHandleIds.add(e.toHandle);
|
|
1527
|
+
}
|
|
1528
|
+
if (usedHandleIds.size === 0) return null;
|
|
1529
|
+
const votesByHandle = /* @__PURE__ */ new Map();
|
|
1530
|
+
for (const e of edges) {
|
|
1531
|
+
const isFrom = e.from === node.id;
|
|
1532
|
+
const isTo = e.to === node.id;
|
|
1533
|
+
if (!isFrom && !isTo) continue;
|
|
1534
|
+
const handleId = isFrom ? e.fromHandle : e.toHandle;
|
|
1535
|
+
const neighborId = isFrom ? e.to : e.from;
|
|
1536
|
+
const np = previewIndex.get(neighborId);
|
|
1537
|
+
if (!np) continue;
|
|
1538
|
+
const nbrRankMin = isHorizontal ? np.x : np.y;
|
|
1539
|
+
const nbrRankMax = isHorizontal ? np.x + np.width : np.y + np.height;
|
|
1540
|
+
const overlap = Math.min(rankMax, nbrRankMax) - Math.max(rankMin, nbrRankMin);
|
|
1541
|
+
const isFlow = overlap <= 0;
|
|
1542
|
+
const nbrOrderC = isHorizontal ? np.y + np.height / 2 : np.x + np.width / 2;
|
|
1543
|
+
const nbrRankC = isHorizontal ? np.x + np.width / 2 : np.y + np.height / 2;
|
|
1544
|
+
const selfRankC = isHorizontal ? selfPreview.x + selfPreview.width / 2 : selfPreview.y + selfPreview.height / 2;
|
|
1545
|
+
const nbrIsBefore = nbrRankMax <= rankMin || overlap <= 0 && nbrRankC < selfRankC;
|
|
1546
|
+
const flowSide = isHorizontal ? nbrIsBefore ? "left" : "right" : nbrIsBefore ? "top" : "bottom";
|
|
1547
|
+
const d = nbrOrderC - orderCenter;
|
|
1548
|
+
const perpSide = isHorizontal ? d >= 0 ? "bottom" : "top" : d >= 0 ? "right" : "left";
|
|
1549
|
+
const beyond = nbrOrderC > orderMax ? 1 : nbrOrderC < orderMin ? -1 : 0;
|
|
1550
|
+
const dist = Math.hypot(nbrRankC - selfRankC, nbrOrderC - orderCenter);
|
|
1551
|
+
const weight = dist > 0 ? 1 / dist : 1;
|
|
1552
|
+
const arr = votesByHandle.get(handleId) ?? [];
|
|
1553
|
+
arr.push({ weight, isFlow, flowSide, perpSide, orderCoord: nbrOrderC, rankCoord: nbrRankC, beyond });
|
|
1554
|
+
votesByHandle.set(handleId, arr);
|
|
1555
|
+
}
|
|
1556
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1557
|
+
for (const [handleId, votes] of votesByHandle) {
|
|
1558
|
+
if (votes.length === 0) continue;
|
|
1559
|
+
const tallies = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
1560
|
+
let dom = votes[0];
|
|
1561
|
+
for (const v of votes) {
|
|
1562
|
+
tallies[v.isFlow ? v.flowSide : v.perpSide] += v.weight;
|
|
1563
|
+
if (v.weight > dom.weight) dom = v;
|
|
1564
|
+
}
|
|
1565
|
+
const side = Object.entries(tallies).reduce((acc, cur) => cur[1] > acc[1] ? cur : acc, ["top", -Infinity])[0];
|
|
1566
|
+
resolved.set(handleId, {
|
|
1567
|
+
side,
|
|
1568
|
+
isFlow: dom.isFlow,
|
|
1569
|
+
flowSide: dom.flowSide,
|
|
1570
|
+
perpSide: dom.perpSide,
|
|
1571
|
+
orderCoord: dom.orderCoord,
|
|
1572
|
+
rankCoord: dom.rankCoord,
|
|
1573
|
+
beyond: dom.beyond
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
for (const flowSide of ["top", "bottom", "left", "right"]) {
|
|
1577
|
+
const group = [...resolved.entries()].filter(([, r]) => r.isFlow && r.side === flowSide);
|
|
1578
|
+
if (group.length < 2) continue;
|
|
1579
|
+
let hasLeft = false, hasRight = false;
|
|
1580
|
+
for (const [, r] of group) {
|
|
1581
|
+
if (r.orderCoord < orderCenter) hasLeft = true;
|
|
1582
|
+
else if (r.orderCoord > orderCenter) hasRight = true;
|
|
1583
|
+
}
|
|
1584
|
+
if (hasLeft && hasRight) continue;
|
|
1585
|
+
const distOf = (oc) => oc > orderMax ? oc - orderMax : orderMin - oc;
|
|
1586
|
+
const distinctCoords = [...new Set(group.filter(([, r]) => r.beyond !== 0).map(([, r]) => r.orderCoord))].sort((a, b) => distOf(a) - distOf(b));
|
|
1587
|
+
if (distinctCoords.length >= 2) {
|
|
1588
|
+
const keep = distinctCoords[0];
|
|
1589
|
+
for (const [, r] of group) {
|
|
1590
|
+
if (r.beyond !== 0 && r.orderCoord !== keep) r.side = r.perpSide;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
const sideChanges = {};
|
|
1595
|
+
const newSideById = /* @__PURE__ */ new Map();
|
|
1596
|
+
for (const h of node.handles) {
|
|
1597
|
+
const r = resolved.get(h.id);
|
|
1598
|
+
if (!r) continue;
|
|
1599
|
+
newSideById.set(h.id, r.side);
|
|
1600
|
+
if (r.side !== h.position) sideChanges[h.id] = { from: h.position, to: r.side };
|
|
1601
|
+
}
|
|
1602
|
+
const newOffsetById = /* @__PURE__ */ new Map();
|
|
1603
|
+
const bySide = /* @__PURE__ */ new Map();
|
|
1604
|
+
for (const [hid, r] of resolved) {
|
|
1605
|
+
const arr = bySide.get(r.side) ?? [];
|
|
1606
|
+
arr.push(hid);
|
|
1607
|
+
bySide.set(r.side, arr);
|
|
1608
|
+
}
|
|
1609
|
+
for (const [side, handleIds] of bySide) {
|
|
1610
|
+
if (handleIds.length < 2) continue;
|
|
1611
|
+
const offsets = handleIds.map((hid) => node.handles.find((h) => h.id === hid).offset ?? 0.5).sort((a, b) => a - b);
|
|
1612
|
+
const horizontalSide = side === "top" || side === "bottom";
|
|
1613
|
+
const sortKey = (hid) => {
|
|
1614
|
+
const r = resolved.get(hid);
|
|
1615
|
+
if (isHorizontal) {
|
|
1616
|
+
return horizontalSide ? r.rankCoord : r.orderCoord;
|
|
1617
|
+
}
|
|
1618
|
+
return horizontalSide ? r.orderCoord : r.rankCoord;
|
|
1619
|
+
};
|
|
1620
|
+
const sortedHandles = [...handleIds].sort((a, b) => {
|
|
1621
|
+
const ra = sortKey(a), rb = sortKey(b);
|
|
1622
|
+
return ra - rb || (a < b ? -1 : a > b ? 1 : 0);
|
|
1623
|
+
});
|
|
1624
|
+
for (let i = 0; i < sortedHandles.length; i++) {
|
|
1625
|
+
newOffsetById.set(sortedHandles[i], offsets[i]);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
let changed = false;
|
|
1629
|
+
const newHandles = node.handles.map((h) => {
|
|
1630
|
+
const side = newSideById.get(h.id) ?? h.position;
|
|
1631
|
+
const offset = newOffsetById.has(h.id) ? newOffsetById.get(h.id) : h.offset ?? 0.5;
|
|
1632
|
+
if (side !== h.position || offset !== (h.offset ?? 0.5)) {
|
|
1633
|
+
changed = true;
|
|
1634
|
+
return { ...h, position: side, offset };
|
|
1635
|
+
}
|
|
1636
|
+
return h;
|
|
1637
|
+
});
|
|
1638
|
+
if (!changed) return null;
|
|
1639
|
+
const proposed = { ...node, handles: newHandles };
|
|
1640
|
+
const sideSummary = Object.entries(sideChanges).map(([id, c]) => `${id}: ${c.from}\u2192${c.to}`).join(", ");
|
|
1641
|
+
const reason = sideSummary ? `${Object.keys(sideChanges).length} handle(s) repositioned toward their neighbor (${sideSummary})` : `handles on a shared side reordered to match neighbor order`;
|
|
1642
|
+
return {
|
|
1643
|
+
type: "relocate-handles",
|
|
1644
|
+
nodeId: node.id,
|
|
1645
|
+
current: node,
|
|
1646
|
+
proposed,
|
|
1647
|
+
changes: sideChanges,
|
|
1648
|
+
reason
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1414
1651
|
function rotateHandles(handles, rot) {
|
|
1415
1652
|
const rotateSide = (s) => {
|
|
1416
1653
|
if (rot === 180) {
|
|
@@ -1429,7 +1666,12 @@ var COMPOUND_HEADER = 28;
|
|
|
1429
1666
|
function layout(input, options) {
|
|
1430
1667
|
const resolved = resolveOptions(options);
|
|
1431
1668
|
if (input.nodes.length === 0) return { nodes: [], edges: [] };
|
|
1432
|
-
|
|
1669
|
+
let adjusted = applyRotationProposals(input, resolved);
|
|
1670
|
+
if (resolved.onProposal) {
|
|
1671
|
+
const previewOptions = { ...resolved, onProposal: void 0 };
|
|
1672
|
+
const preview = layoutCompound(adjusted, previewOptions);
|
|
1673
|
+
adjusted = applyRelocateProposals(adjusted, preview, resolved);
|
|
1674
|
+
}
|
|
1433
1675
|
return layoutCompound(adjusted, resolved);
|
|
1434
1676
|
}
|
|
1435
1677
|
function computeLayout(graph, options) {
|
|
@@ -1438,7 +1680,12 @@ function computeLayout(graph, options) {
|
|
|
1438
1680
|
let layers = assignLayers(graph);
|
|
1439
1681
|
layers = insertDummyNodes(graph, layers);
|
|
1440
1682
|
let rail = railLayers(layers, graph);
|
|
1441
|
-
rail = minimizeCrossings(
|
|
1683
|
+
rail = minimizeCrossings(
|
|
1684
|
+
graph,
|
|
1685
|
+
rail,
|
|
1686
|
+
options.crossingMinimizationIterations,
|
|
1687
|
+
options.skipTranspose
|
|
1688
|
+
);
|
|
1442
1689
|
assignCoordinates(graph, rail, options);
|
|
1443
1690
|
placeValueSidecars(graph, layers, options);
|
|
1444
1691
|
const routedEdges = routeEdges(graph, options.direction, options.edgeMargin);
|