@naniteninja/profile-comparison-lib 1.0.24 → 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 [];
@@ -2660,8 +2665,6 @@ class ProfileComparisonLibComponent {
2660
2665
  const horizontalPadding = this.chipPaddingXPx * 2;
2661
2666
  const getHalfWidth = (label) => (label.length * approxCharWidth + horizontalPadding) / 2;
2662
2667
  const EDGE_PADDING = 20;
2663
- const getNodeMinX = (label) => getHalfWidth(label) + EDGE_PADDING;
2664
- const getNodeMaxX = (label) => containerWidth - getHalfWidth(label) - EDGE_PADDING;
2665
2668
  const maxHalfP1 = p1Items.length ? Math.max(...p1Items.map(getHalfWidth)) : chipHalfH;
2666
2669
  const maxHalfShared = shared.length ? Math.max(...shared.map(getHalfWidth)) : chipHalfH;
2667
2670
  const maxHalfP2 = p2Items.length ? Math.max(...p2Items.map(getHalfWidth)) : chipHalfH;
@@ -2681,6 +2684,37 @@ class ProfileComparisonLibComponent {
2681
2684
  const minCenterY = startY + chipHalfH;
2682
2685
  const verticalSpan = Math.max(0, maxY - minCenterY);
2683
2686
  const centerGuard = Math.max(12, Math.min(26, (zoneP2 - zoneP1) * 0.12));
2687
+ const maxHalfAny = Math.max(maxHalfP1, maxHalfShared, maxHalfP2, chipHalfH);
2688
+ const globalMinX = maxHalfAny + EDGE_PADDING;
2689
+ const globalMaxX = containerWidth - maxHalfAny - EDGE_PADDING;
2690
+ const dividerLeft = (zoneP1 + zoneShared) / 2;
2691
+ const dividerRight = (zoneShared + zoneP2) / 2;
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));
2698
+ let p1BoundsMinX = globalMinX;
2699
+ let p1BoundsMaxX = Math.min(globalMaxX, dividerLeft - laneGap);
2700
+ let sharedBoundsMinX = Math.max(globalMinX, sharedRawMin + sharedTighten);
2701
+ let sharedBoundsMaxX = Math.min(globalMaxX, sharedRawMax - sharedTighten);
2702
+ let p2BoundsMinX = Math.max(globalMinX, dividerRight + laneGap);
2703
+ let p2BoundsMaxX = globalMaxX;
2704
+ if (p1BoundsMaxX < p1BoundsMinX)
2705
+ p1BoundsMaxX = p1BoundsMinX;
2706
+ if (p2BoundsMinX > p2BoundsMaxX)
2707
+ p2BoundsMinX = p2BoundsMaxX;
2708
+ if (sharedBoundsMaxX - sharedBoundsMinX < sharedMinWidth) {
2709
+ const centerX = Math.max(globalMinX, Math.min(globalMaxX, zoneShared));
2710
+ const fallbackHalfWidth = sharedMinWidth / 2;
2711
+ sharedBoundsMinX = Math.max(globalMinX, centerX - fallbackHalfWidth);
2712
+ sharedBoundsMaxX = Math.min(globalMaxX, centerX + fallbackHalfWidth);
2713
+ if (sharedBoundsMaxX < sharedBoundsMinX) {
2714
+ sharedBoundsMinX = centerX;
2715
+ sharedBoundsMaxX = centerX;
2716
+ }
2717
+ }
2684
2718
  // --- Scan matrix for max similarity to normalize spring distances ---
2685
2719
  let matrixMax = 0;
2686
2720
  if (this.matrixData?.rows) {
@@ -2758,7 +2792,7 @@ class ProfileComparisonLibComponent {
2758
2792
  return nodes.filter(n => n.group === 'p1' || n.group === 'p2');
2759
2793
  };
2760
2794
  const ySimilarityFloor = Math.max(0.18, springThreshold - 0.06);
2761
- for (let pass = 0; pass < 3; pass++) {
2795
+ for (let pass = 0; pass < 6; pass++) {
2762
2796
  const snapshotY = new Map();
2763
2797
  nodes.forEach(node => {
2764
2798
  snapshotY.set(node.id, node.targetY ?? (minCenterY + verticalSpan * 0.5));
@@ -2782,57 +2816,27 @@ class ProfileComparisonLibComponent {
2782
2816
  return;
2783
2817
  const semanticY = weightedY / totalWeight;
2784
2818
  const currentY = snapshotY.get(node.id) ?? semanticY;
2785
- const blend = node.group === 'shared' ? 0.72 : 0.62;
2819
+ const blend = node.group === 'shared' ? 0.84 : 0.78;
2786
2820
  node.targetY = Math.max(minCenterY, Math.min(maxY, currentY + (semanticY - currentY) * blend));
2787
2821
  });
2788
2822
  }
2789
- const groupCounts = {
2790
- p1: p1Items.length,
2791
- shared: shared.length,
2792
- p2: p2Items.length,
2793
- };
2794
- const fullVerticalSpan = Math.max(rowGap, maxY - minCenterY);
2795
2823
  const buildNodeBounds = (node) => {
2796
- const halfW = getHalfWidth(node.label);
2797
- const globalMinX = getNodeMinX(node.label);
2798
- const globalMaxX = getNodeMaxX(node.label);
2799
- const leftLimit = zoneShared - halfW - centerGuard;
2800
- const rightLimit = zoneShared + halfW + centerGuard;
2801
2824
  let minX = globalMinX;
2802
2825
  let maxX = globalMaxX;
2803
2826
  if (node.group === 'p1') {
2804
- maxX = Math.min(globalMaxX, leftLimit);
2805
- if (maxX < minX)
2806
- maxX = minX;
2827
+ minX = p1BoundsMinX;
2828
+ maxX = p1BoundsMaxX;
2807
2829
  }
2808
2830
  else if (node.group === 'p2') {
2809
- minX = Math.max(globalMinX, rightLimit);
2810
- if (minX > maxX)
2811
- minX = maxX;
2831
+ minX = p2BoundsMinX;
2832
+ maxX = p2BoundsMaxX;
2812
2833
  }
2813
2834
  else {
2814
- // Shared chips should align directly under the centered Shared title.
2815
- const centerX = Math.max(globalMinX, Math.min(globalMaxX, zoneShared));
2816
- minX = centerX;
2817
- maxX = centerX;
2818
- }
2819
- const baseY = node.targetY ?? (minCenterY + fullVerticalSpan * 0.5);
2820
- const groupCount = groupCounts[node.group];
2821
- let ySlack = rowGap * 2.2;
2822
- if (node.group === 'shared') {
2823
- ySlack = Math.max(rowGap * 4.2, fullVerticalSpan * 0.58);
2824
- }
2825
- else {
2826
- const laneSpan = groupCount > 1 ? fullVerticalSpan / (groupCount - 1) : fullVerticalSpan;
2827
- ySlack = Math.min(fullVerticalSpan, Math.max(rowGap * 2.4, laneSpan * 0.95));
2828
- }
2829
- let minY = Math.max(minCenterY, baseY - ySlack);
2830
- let maxYForNode = Math.min(maxY, baseY + ySlack);
2831
- if (minY > maxYForNode) {
2832
- minY = minCenterY;
2833
- maxYForNode = maxY;
2835
+ minX = sharedBoundsMinX;
2836
+ maxX = sharedBoundsMaxX;
2834
2837
  }
2835
- return { minX, maxX, minY, maxY: maxYForNode };
2838
+ // Y should be column-wide so any chip can travel vertically within its lane.
2839
+ return { minX, maxX, minY: minCenterY, maxY };
2836
2840
  };
2837
2841
  const nodeBounds = new Map();
2838
2842
  nodes.forEach(node => nodeBounds.set(node.id, buildNodeBounds(node)));
@@ -2844,8 +2848,8 @@ class ProfileComparisonLibComponent {
2844
2848
  };
2845
2849
  const crossCandidates = [];
2846
2850
  const intraCandidates = [];
2847
- const MIN_DIST = 62;
2848
- const SPAN = 92;
2851
+ const MIN_DIST = 34;
2852
+ const SPAN = 72;
2849
2853
  for (let i = 0; i < nodes.length; i++) {
2850
2854
  for (let j = i + 1; j < nodes.length; j++) {
2851
2855
  if (!this.canLinkNodes(nodes[i], nodes[j]))
@@ -2879,9 +2883,9 @@ class ProfileComparisonLibComponent {
2879
2883
  kind: 'intra',
2880
2884
  source: groupNodes[i],
2881
2885
  target: groupNodes[j],
2882
- strength: 0.08 + normSim * 0.2,
2886
+ strength: 0.12 + normSim * 0.24,
2883
2887
  similarity,
2884
- distance: 50 + (1 - normSim) * 42,
2888
+ distance: 34 + (1 - normSim) * 46,
2885
2889
  });
2886
2890
  }
2887
2891
  }
@@ -2974,20 +2978,56 @@ class ProfileComparisonLibComponent {
2974
2978
  return;
2975
2979
  const targetY = weightedY / totalWeight;
2976
2980
  const dy = targetY - (node.y ?? targetY);
2981
+ const sparseNeighbors = neighbors.length <= 2;
2977
2982
  const alignStrength = node.group === 'shared'
2978
- ? (neighbors.length <= 2 ? 0.62 : 0.52)
2979
- : 0.36;
2983
+ ? (sparseNeighbors ? 0.95 : 0.8)
2984
+ : (sparseNeighbors ? 0.82 : 0.68);
2980
2985
  node.vy = (node.vy ?? 0) + dy * alignStrength * alpha;
2981
2986
  });
2982
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
+ };
2983
3023
  this.ngZone.runOutsideAngular(() => {
2984
3024
  const sim = forceSimulation(nodes)
2985
- .alphaDecay(0.02)
3025
+ .alphaDecay(0.014)
2986
3026
  .stop()
2987
3027
  .force('link', forceLink(links)
2988
3028
  .id(d => d.id)
2989
3029
  .distance(d => d.distance)
2990
- .strength(d => d.kind === 'cross' ? d.strength * 0.9 : d.strength * 0.65))
3030
+ .strength(d => this.getRuntimeLinkStrength(d)))
2991
3031
  .force('collide', forceCollide((d) => {
2992
3032
  const baseRadius = getHalfWidth(d.label) * 0.55 + ProfileComparisonLibComponent.CHIP_COLLISION_BORDER_PADDING;
2993
3033
  return Math.min(58, Math.max(ProfileComparisonLibComponent.CHIP_RADIUS + 8, baseRadius));
@@ -2996,29 +3036,15 @@ class ProfileComparisonLibComponent {
2996
3036
  .iterations(2))
2997
3037
  .force('xBound', forceX(d => d.targetX ?? zoneShared)
2998
3038
  .strength(d => d.group === 'shared' ? 1.15 : 0.85))
2999
- .force('yTargetSeed', forceY(d => d.targetY ?? minCenterY).strength(0.34))
3000
- .force('crossAlign', crossAlignForce);
3039
+ .force('yTargetSeed', forceY(d => d.targetY ?? minCenterY).strength(0.1))
3040
+ .force('crossAlign', crossAlignForce)
3041
+ .force('similarityAttraction', similarityAttractionForce);
3001
3042
  for (let i = 0; i < 420 && sim.alpha() > 0.0035; i++) {
3002
3043
  sim.tick();
3003
3044
  nodes.forEach(n => {
3004
3045
  clampNodeToBounds(n);
3005
3046
  });
3006
3047
  }
3007
- const enforceGroupStack = (group) => {
3008
- const groupNodes = nodes
3009
- .filter(n => n.group === group)
3010
- .sort((a, b) => (a.y ?? minCenterY) - (b.y ?? minCenterY));
3011
- for (let i = 1; i < groupNodes.length; i++) {
3012
- const prevY = groupNodes[i - 1].y ?? minCenterY;
3013
- const minAllowed = prevY + rowGap * 0.92;
3014
- if ((groupNodes[i].y ?? minCenterY) < minAllowed) {
3015
- groupNodes[i].y = minAllowed;
3016
- }
3017
- }
3018
- };
3019
- enforceGroupStack('p1');
3020
- enforceGroupStack('shared');
3021
- enforceGroupStack('p2');
3022
3048
  nodes.forEach(n => {
3023
3049
  clampNodeToBounds(n);
3024
3050
  n.originalFx = n.x;
@@ -3028,8 +3054,8 @@ class ProfileComparisonLibComponent {
3028
3054
  sim.force('yTargetSeed', null);
3029
3055
  sim.force('crossAlign', null);
3030
3056
  sim.force('xTarget', forceX(d => d.originalFx ?? d.x ?? 0)
3031
- .strength(d => d.group === 'shared' ? 0.16 : 0.075));
3032
- sim.force('yTarget', forceY(d => d.originalFy ?? d.y ?? 0).strength(0.085));
3057
+ .strength(d => d.group === 'shared' ? 0.12 : 0.045));
3058
+ sim.force('yTarget', forceY(d => d.originalFy ?? d.y ?? 0).strength(0.045));
3033
3059
  this.simulation = sim;
3034
3060
  this.ngZone.run(() => {
3035
3061
  this.forceNodes = [...nodes];
@@ -3459,85 +3485,19 @@ class ProfileComparisonLibComponent {
3459
3485
  }
3460
3486
  }
3461
3487
  /**
3462
- * Installs (or removes) a custom d3 force that pulls every linked node toward
3463
- * the anchor, stopping at an orbit radius scaled by similarity:
3464
- * highest-sim → MIN_ORBIT (touches anchor), lowest-sim → MAX_ORBIT.
3465
- *
3466
- * While active, the xTarget/yTarget restoring forces are muted so they don't
3467
- * 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.
3468
3490
  */
3469
3491
  updateAttractionForce(anchorNode) {
3470
3492
  if (!this.simulation)
3471
3493
  return;
3472
- if (!anchorNode) {
3473
- this.simulation.force('attraction', null);
3474
- // Restore restoring forces so nodes spring back home
3475
- const xT = this.simulation.force('xTarget');
3476
- if (xT)
3477
- xT.strength(0.1);
3478
- const yT = this.simulation.force('yTarget');
3479
- if (yT)
3480
- yT.strength(0.1);
3481
- const col = this.simulation.force('collide');
3482
- if (col)
3483
- 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) {
3484
3499
  this.simulation.alphaTarget(0);
3485
- this.simulation.alpha(0.6).restart();
3486
- return;
3487
3500
  }
3488
- // Collect connected node IDs and their similarity values
3489
- const linkSim = new Map();
3490
- this.forceLinks.forEach(link => {
3491
- const srcId = typeof link.source === 'object' ? link.source.id : link.source;
3492
- const tgtId = typeof link.target === 'object' ? link.target.id : link.target;
3493
- const sim = Number(link.similarity ?? link.strength ?? 0.5);
3494
- if (srcId === anchorNode.id)
3495
- linkSim.set(String(tgtId), sim);
3496
- if (tgtId === anchorNode.id)
3497
- linkSim.set(String(srcId), sim);
3498
- });
3499
- const simValues = Array.from(linkSim.values());
3500
- const maxSim = simValues.length ? Math.max(...simValues) : 1;
3501
- // Closest chip (highest sim) touches the anchor (36px ≈ one chip height).
3502
- // Others are spread tightly between 36–80px.
3503
- const MIN_ORBIT = 36; // px — highest-sim chip: touching
3504
- const MAX_ORBIT = 80; // px — lowest-sim chip: still very close
3505
- const PULL = 0.5; // strong pull — overcomes any residual forces
3506
- // Mute the forces that would fight the pull while attraction is active
3507
- const xT = this.simulation.force('xTarget');
3508
- if (xT)
3509
- xT.strength(0);
3510
- const yT = this.simulation.force('yTarget');
3511
- if (yT)
3512
- yT.strength(0);
3513
- const col = this.simulation.force('collide');
3514
- if (col)
3515
- col.strength(0.35); // keep slight chip separation while still allowing orbiting
3516
- const attractionForce = (alpha) => {
3517
- const ax = anchorNode.x ?? 0;
3518
- const ay = anchorNode.y ?? 0;
3519
- this.forceNodes.forEach(n => {
3520
- if (n.id === anchorNode.id)
3521
- return;
3522
- const sim = linkSim.get(n.id);
3523
- if (sim === undefined)
3524
- return; // not connected — leave untouched
3525
- const normalizedSim = maxSim > 0 ? sim / maxSim : 0;
3526
- const orbitDist = MIN_ORBIT + (MAX_ORBIT - MIN_ORBIT) * (1 - normalizedSim);
3527
- const dx = ax - (n.x ?? 0);
3528
- const dy = ay - (n.y ?? 0);
3529
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
3530
- if (dist > orbitDist) {
3531
- // Pull toward orbit boundary — proportional to gap remaining
3532
- const factor = PULL * alpha * (dist - orbitDist) / dist;
3533
- n.vx = (n.vx ?? 0) + dx * factor;
3534
- n.vy = (n.vy ?? 0) + dy * factor;
3535
- }
3536
- });
3537
- };
3538
- this.simulation.force('attraction', attractionForce);
3539
- // alphaTarget 0.3 keeps alpha high so attraction stays strong indefinitely
3540
- this.simulation.alphaTarget(0.3).restart();
3541
3501
  }
3542
3502
  isNodeConnected(node) {
3543
3503
  if (!this.activeNode)
@@ -3629,21 +3589,21 @@ class ProfileComparisonLibComponent {
3629
3589
  if (this.simulation) {
3630
3590
  const linkForce = this.simulation.force('link');
3631
3591
  if (linkForce)
3632
- linkForce.strength((d) => d.strength * 0.6);
3592
+ linkForce.strength((d) => this.getRuntimeLinkStrength(d));
3633
3593
  const collideForce = this.simulation.force('collide');
3634
3594
  if (collideForce)
3635
3595
  collideForce.strength(1.0);
3636
3596
  const xTarget = this.simulation.force('xTarget');
3637
3597
  if (xTarget)
3638
- xTarget.strength(0.08);
3598
+ xTarget.strength((d) => d.group === 'shared' ? 0.12 : 0.05);
3639
3599
  const yTarget = this.simulation.force('yTarget');
3640
3600
  if (yTarget)
3641
- yTarget.strength(0.08);
3601
+ yTarget.strength(0.05);
3642
3602
  }
3643
3603
  // Pull connected chips toward the dragged node
3644
3604
  this.updateAttractionForce(node);
3645
3605
  // Wake up the simulation target to keep it highly active during drag
3646
- this.simulation.alphaTarget(0.3).restart();
3606
+ this.simulation.alphaTarget(0.28).restart();
3647
3607
  }
3648
3608
  onDocumentDragMove(event) {
3649
3609
  if (!this.isDragging || !this.draggedNode)
@@ -3675,12 +3635,8 @@ class ProfileComparisonLibComponent {
3675
3635
  const maxY = bounds?.maxY ?? fallbackMaxY;
3676
3636
  const clampedX = Math.max(minX, Math.min(maxX, relX));
3677
3637
  const clampedY = Math.max(minY, Math.min(maxY, relY));
3678
- const anchorX = this.draggedNode.originalFx ?? clampedX;
3679
- const anchorY = this.draggedNode.originalFy ?? clampedY;
3680
- const xElasticity = this.draggedNode.group === 'shared' ? 0.62 : 0.72;
3681
- const yElasticity = this.draggedNode.group === 'shared' ? 0.58 : 0.68;
3682
- this.draggedNode.fx = anchorX + (clampedX - anchorX) * xElasticity;
3683
- this.draggedNode.fy = anchorY + (clampedY - anchorY) * yElasticity;
3638
+ this.draggedNode.fx = clampedX;
3639
+ this.draggedNode.fy = clampedY;
3684
3640
  // Keep active force simulation running
3685
3641
  this.simulation.alpha(0.3).restart();
3686
3642
  }
@@ -3690,27 +3646,25 @@ class ProfileComparisonLibComponent {
3690
3646
  // Release the node. Clear fx and fy so it is no longer pinned to the mouse
3691
3647
  this.draggedNode.fx = undefined;
3692
3648
  this.draggedNode.fy = undefined;
3693
- // Remove attraction so connected chips return home
3649
+ // Keep baseline similarity attraction and restoring forces active after drag release.
3694
3650
  this.simulation.force('attraction', null);
3695
- // Set other forces to 0 so all chips glide directly to their starting coordinates
3696
3651
  if (this.simulation) {
3697
3652
  const linkForce = this.simulation.force('link');
3698
3653
  if (linkForce)
3699
- linkForce.strength(0);
3654
+ linkForce.strength((d) => this.getRuntimeLinkStrength(d));
3700
3655
  const collideForce = this.simulation.force('collide');
3701
3656
  if (collideForce)
3702
- collideForce.strength(0.92);
3703
- // Pull nodes smoothly and slowly back to their exact starting points
3657
+ collideForce.strength(1.0);
3704
3658
  const xTarget = this.simulation.force('xTarget');
3705
3659
  if (xTarget)
3706
- xTarget.strength(0.13);
3660
+ xTarget.strength((d) => d.group === 'shared' ? 0.12 : 0.045);
3707
3661
  const yTarget = this.simulation.force('yTarget');
3708
3662
  if (yTarget)
3709
- yTarget.strength(0.13);
3663
+ yTarget.strength(0.045);
3710
3664
  }
3711
3665
  // Cool down simulation targets
3712
3666
  this.simulation.alphaTarget(0);
3713
- this.simulation.alpha(0.6).restart();
3667
+ this.simulation.alpha(0.4).restart();
3714
3668
  this.draggedNode = null;
3715
3669
  this.isDragging = false;
3716
3670
  }