@naniteninja/profile-comparison-lib 1.0.25 → 1.0.26
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.
|
@@ -2428,6 +2428,11 @@ class ProfileComparisonLibComponent {
|
|
|
2428
2428
|
const idB = this.getLinkNodeId(b);
|
|
2429
2429
|
return idA < idB ? `${idA}|${idB}` : `${idB}|${idA}`;
|
|
2430
2430
|
}
|
|
2431
|
+
getRuntimeLinkStrength(link) {
|
|
2432
|
+
return link.kind === 'cross'
|
|
2433
|
+
? link.strength * 1.7
|
|
2434
|
+
: link.strength * 1.12;
|
|
2435
|
+
}
|
|
2431
2436
|
selectLinksWithCaps(candidates, maxPerNode, minPerNode = 0) {
|
|
2432
2437
|
if (!candidates.length || maxPerNode <= 0)
|
|
2433
2438
|
return [];
|
|
@@ -2685,19 +2690,24 @@ class ProfileComparisonLibComponent {
|
|
|
2685
2690
|
const dividerLeft = (zoneP1 + zoneShared) / 2;
|
|
2686
2691
|
const dividerRight = (zoneShared + zoneP2) / 2;
|
|
2687
2692
|
const laneGap = Math.max(4, Math.min(16, centerGuard * 0.45));
|
|
2693
|
+
const sharedRawMin = Math.max(globalMinX, dividerLeft + laneGap);
|
|
2694
|
+
const sharedRawMax = Math.min(globalMaxX, dividerRight - laneGap);
|
|
2695
|
+
const sharedRawWidth = Math.max(0, sharedRawMax - sharedRawMin);
|
|
2696
|
+
const sharedTighten = Math.max(12, Math.min(34, sharedRawWidth * 0.22));
|
|
2697
|
+
const sharedMinWidth = Math.max(44, Math.min(84, maxHalfShared * 1.05));
|
|
2688
2698
|
let p1BoundsMinX = globalMinX;
|
|
2689
2699
|
let p1BoundsMaxX = Math.min(globalMaxX, dividerLeft - laneGap);
|
|
2690
|
-
let sharedBoundsMinX = Math.max(globalMinX,
|
|
2691
|
-
let sharedBoundsMaxX = Math.min(globalMaxX,
|
|
2700
|
+
let sharedBoundsMinX = Math.max(globalMinX, sharedRawMin + sharedTighten);
|
|
2701
|
+
let sharedBoundsMaxX = Math.min(globalMaxX, sharedRawMax - sharedTighten);
|
|
2692
2702
|
let p2BoundsMinX = Math.max(globalMinX, dividerRight + laneGap);
|
|
2693
2703
|
let p2BoundsMaxX = globalMaxX;
|
|
2694
2704
|
if (p1BoundsMaxX < p1BoundsMinX)
|
|
2695
2705
|
p1BoundsMaxX = p1BoundsMinX;
|
|
2696
2706
|
if (p2BoundsMinX > p2BoundsMaxX)
|
|
2697
2707
|
p2BoundsMinX = p2BoundsMaxX;
|
|
2698
|
-
if (sharedBoundsMaxX <
|
|
2708
|
+
if (sharedBoundsMaxX - sharedBoundsMinX < sharedMinWidth) {
|
|
2699
2709
|
const centerX = Math.max(globalMinX, Math.min(globalMaxX, zoneShared));
|
|
2700
|
-
const fallbackHalfWidth =
|
|
2710
|
+
const fallbackHalfWidth = sharedMinWidth / 2;
|
|
2701
2711
|
sharedBoundsMinX = Math.max(globalMinX, centerX - fallbackHalfWidth);
|
|
2702
2712
|
sharedBoundsMaxX = Math.min(globalMaxX, centerX + fallbackHalfWidth);
|
|
2703
2713
|
if (sharedBoundsMaxX < sharedBoundsMinX) {
|
|
@@ -2838,8 +2848,8 @@ class ProfileComparisonLibComponent {
|
|
|
2838
2848
|
};
|
|
2839
2849
|
const crossCandidates = [];
|
|
2840
2850
|
const intraCandidates = [];
|
|
2841
|
-
const MIN_DIST =
|
|
2842
|
-
const SPAN =
|
|
2851
|
+
const MIN_DIST = 34;
|
|
2852
|
+
const SPAN = 72;
|
|
2843
2853
|
for (let i = 0; i < nodes.length; i++) {
|
|
2844
2854
|
for (let j = i + 1; j < nodes.length; j++) {
|
|
2845
2855
|
if (!this.canLinkNodes(nodes[i], nodes[j]))
|
|
@@ -2873,9 +2883,9 @@ class ProfileComparisonLibComponent {
|
|
|
2873
2883
|
kind: 'intra',
|
|
2874
2884
|
source: groupNodes[i],
|
|
2875
2885
|
target: groupNodes[j],
|
|
2876
|
-
strength: 0.
|
|
2886
|
+
strength: 0.12 + normSim * 0.24,
|
|
2877
2887
|
similarity,
|
|
2878
|
-
distance:
|
|
2888
|
+
distance: 34 + (1 - normSim) * 46,
|
|
2879
2889
|
});
|
|
2880
2890
|
}
|
|
2881
2891
|
}
|
|
@@ -2975,6 +2985,41 @@ class ProfileComparisonLibComponent {
|
|
|
2975
2985
|
node.vy = (node.vy ?? 0) + dy * alignStrength * alpha;
|
|
2976
2986
|
});
|
|
2977
2987
|
};
|
|
2988
|
+
const linkRecords = links
|
|
2989
|
+
.map(link => {
|
|
2990
|
+
const source = typeof link.source === 'object' ? link.source : nodeById.get(String(link.source));
|
|
2991
|
+
const target = typeof link.target === 'object' ? link.target : nodeById.get(String(link.target));
|
|
2992
|
+
if (!source || !target)
|
|
2993
|
+
return null;
|
|
2994
|
+
return {
|
|
2995
|
+
source,
|
|
2996
|
+
target,
|
|
2997
|
+
similarity: Number(link.similarity ?? link.strength ?? 0.5),
|
|
2998
|
+
kind: link.kind,
|
|
2999
|
+
};
|
|
3000
|
+
})
|
|
3001
|
+
.filter((record) => !!record);
|
|
3002
|
+
const maxLinkSimilarity = Math.max(0.001, ...linkRecords.map(record => Math.max(0, record.similarity)));
|
|
3003
|
+
const MIN_PAIR_DISTANCE = 34;
|
|
3004
|
+
const MAX_PAIR_DISTANCE = 88;
|
|
3005
|
+
const similarityAttractionForce = (alpha) => {
|
|
3006
|
+
linkRecords.forEach(record => {
|
|
3007
|
+
const dx = (record.target.x ?? 0) - (record.source.x ?? 0);
|
|
3008
|
+
const dy = (record.target.y ?? 0) - (record.source.y ?? 0);
|
|
3009
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
3010
|
+
const normalizedSimilarity = Math.max(0, Math.min(1, record.similarity / maxLinkSimilarity));
|
|
3011
|
+
const targetDistance = MIN_PAIR_DISTANCE + (MAX_PAIR_DISTANCE - MIN_PAIR_DISTANCE) * (1 - normalizedSimilarity);
|
|
3012
|
+
if (dist <= targetDistance)
|
|
3013
|
+
return;
|
|
3014
|
+
const pullGap = dist - targetDistance;
|
|
3015
|
+
const pullBase = record.kind === 'cross' ? 0.9 : 0.82;
|
|
3016
|
+
const pullFactor = pullBase * alpha * (pullGap / dist);
|
|
3017
|
+
record.source.vx = (record.source.vx ?? 0) + dx * pullFactor;
|
|
3018
|
+
record.source.vy = (record.source.vy ?? 0) + dy * pullFactor;
|
|
3019
|
+
record.target.vx = (record.target.vx ?? 0) - dx * pullFactor;
|
|
3020
|
+
record.target.vy = (record.target.vy ?? 0) - dy * pullFactor;
|
|
3021
|
+
});
|
|
3022
|
+
};
|
|
2978
3023
|
this.ngZone.runOutsideAngular(() => {
|
|
2979
3024
|
const sim = forceSimulation(nodes)
|
|
2980
3025
|
.alphaDecay(0.014)
|
|
@@ -2982,7 +3027,7 @@ class ProfileComparisonLibComponent {
|
|
|
2982
3027
|
.force('link', forceLink(links)
|
|
2983
3028
|
.id(d => d.id)
|
|
2984
3029
|
.distance(d => d.distance)
|
|
2985
|
-
.strength(d =>
|
|
3030
|
+
.strength(d => this.getRuntimeLinkStrength(d)))
|
|
2986
3031
|
.force('collide', forceCollide((d) => {
|
|
2987
3032
|
const baseRadius = getHalfWidth(d.label) * 0.55 + ProfileComparisonLibComponent.CHIP_COLLISION_BORDER_PADDING;
|
|
2988
3033
|
return Math.min(58, Math.max(ProfileComparisonLibComponent.CHIP_RADIUS + 8, baseRadius));
|
|
@@ -2992,7 +3037,8 @@ class ProfileComparisonLibComponent {
|
|
|
2992
3037
|
.force('xBound', forceX(d => d.targetX ?? zoneShared)
|
|
2993
3038
|
.strength(d => d.group === 'shared' ? 1.15 : 0.85))
|
|
2994
3039
|
.force('yTargetSeed', forceY(d => d.targetY ?? minCenterY).strength(0.1))
|
|
2995
|
-
.force('crossAlign', crossAlignForce)
|
|
3040
|
+
.force('crossAlign', crossAlignForce)
|
|
3041
|
+
.force('similarityAttraction', similarityAttractionForce);
|
|
2996
3042
|
for (let i = 0; i < 420 && sim.alpha() > 0.0035; i++) {
|
|
2997
3043
|
sim.tick();
|
|
2998
3044
|
nodes.forEach(n => {
|
|
@@ -3008,8 +3054,8 @@ class ProfileComparisonLibComponent {
|
|
|
3008
3054
|
sim.force('yTargetSeed', null);
|
|
3009
3055
|
sim.force('crossAlign', null);
|
|
3010
3056
|
sim.force('xTarget', forceX(d => d.originalFx ?? d.x ?? 0)
|
|
3011
|
-
.strength(d => d.group === 'shared' ? 0.
|
|
3012
|
-
sim.force('yTarget', forceY(d => d.originalFy ?? d.y ?? 0).strength(0.
|
|
3057
|
+
.strength(d => d.group === 'shared' ? 0.12 : 0.045));
|
|
3058
|
+
sim.force('yTarget', forceY(d => d.originalFy ?? d.y ?? 0).strength(0.045));
|
|
3013
3059
|
this.simulation = sim;
|
|
3014
3060
|
this.ngZone.run(() => {
|
|
3015
3061
|
this.forceNodes = [...nodes];
|
|
@@ -3439,85 +3485,19 @@ class ProfileComparisonLibComponent {
|
|
|
3439
3485
|
}
|
|
3440
3486
|
}
|
|
3441
3487
|
/**
|
|
3442
|
-
*
|
|
3443
|
-
*
|
|
3444
|
-
* highest-sim → MIN_ORBIT (touches anchor), lowest-sim → MAX_ORBIT.
|
|
3445
|
-
*
|
|
3446
|
-
* While active, the xTarget/yTarget restoring forces are muted so they don't
|
|
3447
|
-
* fight the attraction. They are restored the moment attraction is cleared.
|
|
3488
|
+
* Anchor interaction should only affect highlighting/energy.
|
|
3489
|
+
* Similarity attraction is always-on via the base simulation forces.
|
|
3448
3490
|
*/
|
|
3449
3491
|
updateAttractionForce(anchorNode) {
|
|
3450
3492
|
if (!this.simulation)
|
|
3451
3493
|
return;
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
xT.strength(0.1);
|
|
3458
|
-
const yT = this.simulation.force('yTarget');
|
|
3459
|
-
if (yT)
|
|
3460
|
-
yT.strength(0.1);
|
|
3461
|
-
const col = this.simulation.force('collide');
|
|
3462
|
-
if (col)
|
|
3463
|
-
col.strength(1.0);
|
|
3494
|
+
this.simulation.force('attraction', null);
|
|
3495
|
+
if (anchorNode) {
|
|
3496
|
+
this.simulation.alpha(0.24).restart();
|
|
3497
|
+
}
|
|
3498
|
+
else if (!this.isDragging) {
|
|
3464
3499
|
this.simulation.alphaTarget(0);
|
|
3465
|
-
this.simulation.alpha(0.6).restart();
|
|
3466
|
-
return;
|
|
3467
3500
|
}
|
|
3468
|
-
// Collect connected node IDs and their similarity values
|
|
3469
|
-
const linkSim = new Map();
|
|
3470
|
-
this.forceLinks.forEach(link => {
|
|
3471
|
-
const srcId = typeof link.source === 'object' ? link.source.id : link.source;
|
|
3472
|
-
const tgtId = typeof link.target === 'object' ? link.target.id : link.target;
|
|
3473
|
-
const sim = Number(link.similarity ?? link.strength ?? 0.5);
|
|
3474
|
-
if (srcId === anchorNode.id)
|
|
3475
|
-
linkSim.set(String(tgtId), sim);
|
|
3476
|
-
if (tgtId === anchorNode.id)
|
|
3477
|
-
linkSim.set(String(srcId), sim);
|
|
3478
|
-
});
|
|
3479
|
-
const simValues = Array.from(linkSim.values());
|
|
3480
|
-
const maxSim = simValues.length ? Math.max(...simValues) : 1;
|
|
3481
|
-
// Closest chip (highest sim) touches the anchor (36px ≈ one chip height).
|
|
3482
|
-
// Others are spread tightly between 36–80px.
|
|
3483
|
-
const MIN_ORBIT = 36; // px — highest-sim chip: touching
|
|
3484
|
-
const MAX_ORBIT = 80; // px — lowest-sim chip: still very close
|
|
3485
|
-
const PULL = 0.5; // strong pull — overcomes any residual forces
|
|
3486
|
-
// Mute the forces that would fight the pull while attraction is active
|
|
3487
|
-
const xT = this.simulation.force('xTarget');
|
|
3488
|
-
if (xT)
|
|
3489
|
-
xT.strength(0);
|
|
3490
|
-
const yT = this.simulation.force('yTarget');
|
|
3491
|
-
if (yT)
|
|
3492
|
-
yT.strength(0);
|
|
3493
|
-
const col = this.simulation.force('collide');
|
|
3494
|
-
if (col)
|
|
3495
|
-
col.strength(0.35); // keep slight chip separation while still allowing orbiting
|
|
3496
|
-
const attractionForce = (alpha) => {
|
|
3497
|
-
const ax = anchorNode.x ?? 0;
|
|
3498
|
-
const ay = anchorNode.y ?? 0;
|
|
3499
|
-
this.forceNodes.forEach(n => {
|
|
3500
|
-
if (n.id === anchorNode.id)
|
|
3501
|
-
return;
|
|
3502
|
-
const sim = linkSim.get(n.id);
|
|
3503
|
-
if (sim === undefined)
|
|
3504
|
-
return; // not connected — leave untouched
|
|
3505
|
-
const normalizedSim = maxSim > 0 ? sim / maxSim : 0;
|
|
3506
|
-
const orbitDist = MIN_ORBIT + (MAX_ORBIT - MIN_ORBIT) * (1 - normalizedSim);
|
|
3507
|
-
const dx = ax - (n.x ?? 0);
|
|
3508
|
-
const dy = ay - (n.y ?? 0);
|
|
3509
|
-
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
3510
|
-
if (dist > orbitDist) {
|
|
3511
|
-
// Pull toward orbit boundary — proportional to gap remaining
|
|
3512
|
-
const factor = PULL * alpha * (dist - orbitDist) / dist;
|
|
3513
|
-
n.vx = (n.vx ?? 0) + dx * factor;
|
|
3514
|
-
n.vy = (n.vy ?? 0) + dy * factor;
|
|
3515
|
-
}
|
|
3516
|
-
});
|
|
3517
|
-
};
|
|
3518
|
-
this.simulation.force('attraction', attractionForce);
|
|
3519
|
-
// alphaTarget 0.3 keeps alpha high so attraction stays strong indefinitely
|
|
3520
|
-
this.simulation.alphaTarget(0.3).restart();
|
|
3521
3501
|
}
|
|
3522
3502
|
isNodeConnected(node) {
|
|
3523
3503
|
if (!this.activeNode)
|
|
@@ -3609,21 +3589,21 @@ class ProfileComparisonLibComponent {
|
|
|
3609
3589
|
if (this.simulation) {
|
|
3610
3590
|
const linkForce = this.simulation.force('link');
|
|
3611
3591
|
if (linkForce)
|
|
3612
|
-
linkForce.strength((d) => d
|
|
3592
|
+
linkForce.strength((d) => this.getRuntimeLinkStrength(d));
|
|
3613
3593
|
const collideForce = this.simulation.force('collide');
|
|
3614
3594
|
if (collideForce)
|
|
3615
3595
|
collideForce.strength(1.0);
|
|
3616
3596
|
const xTarget = this.simulation.force('xTarget');
|
|
3617
3597
|
if (xTarget)
|
|
3618
|
-
xTarget.strength(0.
|
|
3598
|
+
xTarget.strength((d) => d.group === 'shared' ? 0.12 : 0.05);
|
|
3619
3599
|
const yTarget = this.simulation.force('yTarget');
|
|
3620
3600
|
if (yTarget)
|
|
3621
|
-
yTarget.strength(0.
|
|
3601
|
+
yTarget.strength(0.05);
|
|
3622
3602
|
}
|
|
3623
3603
|
// Pull connected chips toward the dragged node
|
|
3624
3604
|
this.updateAttractionForce(node);
|
|
3625
3605
|
// Wake up the simulation target to keep it highly active during drag
|
|
3626
|
-
this.simulation.alphaTarget(0.
|
|
3606
|
+
this.simulation.alphaTarget(0.28).restart();
|
|
3627
3607
|
}
|
|
3628
3608
|
onDocumentDragMove(event) {
|
|
3629
3609
|
if (!this.isDragging || !this.draggedNode)
|
|
@@ -3666,27 +3646,25 @@ class ProfileComparisonLibComponent {
|
|
|
3666
3646
|
// Release the node. Clear fx and fy so it is no longer pinned to the mouse
|
|
3667
3647
|
this.draggedNode.fx = undefined;
|
|
3668
3648
|
this.draggedNode.fy = undefined;
|
|
3669
|
-
//
|
|
3649
|
+
// Keep baseline similarity attraction and restoring forces active after drag release.
|
|
3670
3650
|
this.simulation.force('attraction', null);
|
|
3671
|
-
// Set other forces to 0 so all chips glide directly to their starting coordinates
|
|
3672
3651
|
if (this.simulation) {
|
|
3673
3652
|
const linkForce = this.simulation.force('link');
|
|
3674
3653
|
if (linkForce)
|
|
3675
|
-
linkForce.strength(
|
|
3654
|
+
linkForce.strength((d) => this.getRuntimeLinkStrength(d));
|
|
3676
3655
|
const collideForce = this.simulation.force('collide');
|
|
3677
3656
|
if (collideForce)
|
|
3678
|
-
collideForce.strength(0
|
|
3679
|
-
// Pull nodes smoothly and slowly back to their exact starting points
|
|
3657
|
+
collideForce.strength(1.0);
|
|
3680
3658
|
const xTarget = this.simulation.force('xTarget');
|
|
3681
3659
|
if (xTarget)
|
|
3682
|
-
xTarget.strength(0.
|
|
3660
|
+
xTarget.strength((d) => d.group === 'shared' ? 0.12 : 0.045);
|
|
3683
3661
|
const yTarget = this.simulation.force('yTarget');
|
|
3684
3662
|
if (yTarget)
|
|
3685
|
-
yTarget.strength(0.
|
|
3663
|
+
yTarget.strength(0.045);
|
|
3686
3664
|
}
|
|
3687
3665
|
// Cool down simulation targets
|
|
3688
3666
|
this.simulation.alphaTarget(0);
|
|
3689
|
-
this.simulation.alpha(0.
|
|
3667
|
+
this.simulation.alpha(0.4).restart();
|
|
3690
3668
|
this.draggedNode = null;
|
|
3691
3669
|
this.isDragging = false;
|
|
3692
3670
|
}
|