@naniteninja/profile-comparison-lib 1.0.24 → 1.0.25

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.
@@ -2660,8 +2660,6 @@ class ProfileComparisonLibComponent {
2660
2660
  const horizontalPadding = this.chipPaddingXPx * 2;
2661
2661
  const getHalfWidth = (label) => (label.length * approxCharWidth + horizontalPadding) / 2;
2662
2662
  const EDGE_PADDING = 20;
2663
- const getNodeMinX = (label) => getHalfWidth(label) + EDGE_PADDING;
2664
- const getNodeMaxX = (label) => containerWidth - getHalfWidth(label) - EDGE_PADDING;
2665
2663
  const maxHalfP1 = p1Items.length ? Math.max(...p1Items.map(getHalfWidth)) : chipHalfH;
2666
2664
  const maxHalfShared = shared.length ? Math.max(...shared.map(getHalfWidth)) : chipHalfH;
2667
2665
  const maxHalfP2 = p2Items.length ? Math.max(...p2Items.map(getHalfWidth)) : chipHalfH;
@@ -2681,6 +2679,32 @@ class ProfileComparisonLibComponent {
2681
2679
  const minCenterY = startY + chipHalfH;
2682
2680
  const verticalSpan = Math.max(0, maxY - minCenterY);
2683
2681
  const centerGuard = Math.max(12, Math.min(26, (zoneP2 - zoneP1) * 0.12));
2682
+ const maxHalfAny = Math.max(maxHalfP1, maxHalfShared, maxHalfP2, chipHalfH);
2683
+ const globalMinX = maxHalfAny + EDGE_PADDING;
2684
+ const globalMaxX = containerWidth - maxHalfAny - EDGE_PADDING;
2685
+ const dividerLeft = (zoneP1 + zoneShared) / 2;
2686
+ const dividerRight = (zoneShared + zoneP2) / 2;
2687
+ const laneGap = Math.max(4, Math.min(16, centerGuard * 0.45));
2688
+ let p1BoundsMinX = globalMinX;
2689
+ let p1BoundsMaxX = Math.min(globalMaxX, dividerLeft - laneGap);
2690
+ let sharedBoundsMinX = Math.max(globalMinX, dividerLeft + laneGap);
2691
+ let sharedBoundsMaxX = Math.min(globalMaxX, dividerRight - laneGap);
2692
+ let p2BoundsMinX = Math.max(globalMinX, dividerRight + laneGap);
2693
+ let p2BoundsMaxX = globalMaxX;
2694
+ if (p1BoundsMaxX < p1BoundsMinX)
2695
+ p1BoundsMaxX = p1BoundsMinX;
2696
+ if (p2BoundsMinX > p2BoundsMaxX)
2697
+ p2BoundsMinX = p2BoundsMaxX;
2698
+ if (sharedBoundsMaxX < sharedBoundsMinX) {
2699
+ const centerX = Math.max(globalMinX, Math.min(globalMaxX, zoneShared));
2700
+ const fallbackHalfWidth = Math.max(6, Math.min(20, Math.max(0, p2BoundsMinX - p1BoundsMaxX) * 0.25));
2701
+ sharedBoundsMinX = Math.max(globalMinX, centerX - fallbackHalfWidth);
2702
+ sharedBoundsMaxX = Math.min(globalMaxX, centerX + fallbackHalfWidth);
2703
+ if (sharedBoundsMaxX < sharedBoundsMinX) {
2704
+ sharedBoundsMinX = centerX;
2705
+ sharedBoundsMaxX = centerX;
2706
+ }
2707
+ }
2684
2708
  // --- Scan matrix for max similarity to normalize spring distances ---
2685
2709
  let matrixMax = 0;
2686
2710
  if (this.matrixData?.rows) {
@@ -2758,7 +2782,7 @@ class ProfileComparisonLibComponent {
2758
2782
  return nodes.filter(n => n.group === 'p1' || n.group === 'p2');
2759
2783
  };
2760
2784
  const ySimilarityFloor = Math.max(0.18, springThreshold - 0.06);
2761
- for (let pass = 0; pass < 3; pass++) {
2785
+ for (let pass = 0; pass < 6; pass++) {
2762
2786
  const snapshotY = new Map();
2763
2787
  nodes.forEach(node => {
2764
2788
  snapshotY.set(node.id, node.targetY ?? (minCenterY + verticalSpan * 0.5));
@@ -2782,57 +2806,27 @@ class ProfileComparisonLibComponent {
2782
2806
  return;
2783
2807
  const semanticY = weightedY / totalWeight;
2784
2808
  const currentY = snapshotY.get(node.id) ?? semanticY;
2785
- const blend = node.group === 'shared' ? 0.72 : 0.62;
2809
+ const blend = node.group === 'shared' ? 0.84 : 0.78;
2786
2810
  node.targetY = Math.max(minCenterY, Math.min(maxY, currentY + (semanticY - currentY) * blend));
2787
2811
  });
2788
2812
  }
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
2813
  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
2814
  let minX = globalMinX;
2802
2815
  let maxX = globalMaxX;
2803
2816
  if (node.group === 'p1') {
2804
- maxX = Math.min(globalMaxX, leftLimit);
2805
- if (maxX < minX)
2806
- maxX = minX;
2817
+ minX = p1BoundsMinX;
2818
+ maxX = p1BoundsMaxX;
2807
2819
  }
2808
2820
  else if (node.group === 'p2') {
2809
- minX = Math.max(globalMinX, rightLimit);
2810
- if (minX > maxX)
2811
- minX = maxX;
2812
- }
2813
- 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);
2821
+ minX = p2BoundsMinX;
2822
+ maxX = p2BoundsMaxX;
2824
2823
  }
2825
2824
  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;
2825
+ minX = sharedBoundsMinX;
2826
+ maxX = sharedBoundsMaxX;
2834
2827
  }
2835
- return { minX, maxX, minY, maxY: maxYForNode };
2828
+ // Y should be column-wide so any chip can travel vertically within its lane.
2829
+ return { minX, maxX, minY: minCenterY, maxY };
2836
2830
  };
2837
2831
  const nodeBounds = new Map();
2838
2832
  nodes.forEach(node => nodeBounds.set(node.id, buildNodeBounds(node)));
@@ -2974,20 +2968,21 @@ class ProfileComparisonLibComponent {
2974
2968
  return;
2975
2969
  const targetY = weightedY / totalWeight;
2976
2970
  const dy = targetY - (node.y ?? targetY);
2971
+ const sparseNeighbors = neighbors.length <= 2;
2977
2972
  const alignStrength = node.group === 'shared'
2978
- ? (neighbors.length <= 2 ? 0.62 : 0.52)
2979
- : 0.36;
2973
+ ? (sparseNeighbors ? 0.95 : 0.8)
2974
+ : (sparseNeighbors ? 0.82 : 0.68);
2980
2975
  node.vy = (node.vy ?? 0) + dy * alignStrength * alpha;
2981
2976
  });
2982
2977
  };
2983
2978
  this.ngZone.runOutsideAngular(() => {
2984
2979
  const sim = forceSimulation(nodes)
2985
- .alphaDecay(0.02)
2980
+ .alphaDecay(0.014)
2986
2981
  .stop()
2987
2982
  .force('link', forceLink(links)
2988
2983
  .id(d => d.id)
2989
2984
  .distance(d => d.distance)
2990
- .strength(d => d.kind === 'cross' ? d.strength * 0.9 : d.strength * 0.65))
2985
+ .strength(d => d.kind === 'cross' ? d.strength * 1.45 : d.strength * 0.86))
2991
2986
  .force('collide', forceCollide((d) => {
2992
2987
  const baseRadius = getHalfWidth(d.label) * 0.55 + ProfileComparisonLibComponent.CHIP_COLLISION_BORDER_PADDING;
2993
2988
  return Math.min(58, Math.max(ProfileComparisonLibComponent.CHIP_RADIUS + 8, baseRadius));
@@ -2996,7 +2991,7 @@ class ProfileComparisonLibComponent {
2996
2991
  .iterations(2))
2997
2992
  .force('xBound', forceX(d => d.targetX ?? zoneShared)
2998
2993
  .strength(d => d.group === 'shared' ? 1.15 : 0.85))
2999
- .force('yTargetSeed', forceY(d => d.targetY ?? minCenterY).strength(0.34))
2994
+ .force('yTargetSeed', forceY(d => d.targetY ?? minCenterY).strength(0.1))
3000
2995
  .force('crossAlign', crossAlignForce);
3001
2996
  for (let i = 0; i < 420 && sim.alpha() > 0.0035; i++) {
3002
2997
  sim.tick();
@@ -3004,21 +2999,6 @@ class ProfileComparisonLibComponent {
3004
2999
  clampNodeToBounds(n);
3005
3000
  });
3006
3001
  }
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
3002
  nodes.forEach(n => {
3023
3003
  clampNodeToBounds(n);
3024
3004
  n.originalFx = n.x;
@@ -3675,12 +3655,8 @@ class ProfileComparisonLibComponent {
3675
3655
  const maxY = bounds?.maxY ?? fallbackMaxY;
3676
3656
  const clampedX = Math.max(minX, Math.min(maxX, relX));
3677
3657
  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;
3658
+ this.draggedNode.fx = clampedX;
3659
+ this.draggedNode.fy = clampedY;
3684
3660
  // Keep active force simulation running
3685
3661
  this.simulation.alpha(0.3).restart();
3686
3662
  }