@naniteninja/profile-comparison-lib 1.0.23 → 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,7 +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));
2684
- const sharedBand = Math.max(12, Math.min(22, (zoneP2 - zoneP1) * 0.09));
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
+ }
2685
2708
  // --- Scan matrix for max similarity to normalize spring distances ---
2686
2709
  let matrixMax = 0;
2687
2710
  if (this.matrixData?.rows) {
@@ -2759,7 +2782,7 @@ class ProfileComparisonLibComponent {
2759
2782
  return nodes.filter(n => n.group === 'p1' || n.group === 'p2');
2760
2783
  };
2761
2784
  const ySimilarityFloor = Math.max(0.18, springThreshold - 0.06);
2762
- for (let pass = 0; pass < 3; pass++) {
2785
+ for (let pass = 0; pass < 6; pass++) {
2763
2786
  const snapshotY = new Map();
2764
2787
  nodes.forEach(node => {
2765
2788
  snapshotY.set(node.id, node.targetY ?? (minCenterY + verticalSpan * 0.5));
@@ -2783,60 +2806,27 @@ class ProfileComparisonLibComponent {
2783
2806
  return;
2784
2807
  const semanticY = weightedY / totalWeight;
2785
2808
  const currentY = snapshotY.get(node.id) ?? semanticY;
2786
- const blend = node.group === 'shared' ? 0.72 : 0.62;
2809
+ const blend = node.group === 'shared' ? 0.84 : 0.78;
2787
2810
  node.targetY = Math.max(minCenterY, Math.min(maxY, currentY + (semanticY - currentY) * blend));
2788
2811
  });
2789
2812
  }
2790
- const groupCounts = {
2791
- p1: p1Items.length,
2792
- shared: shared.length,
2793
- p2: p2Items.length,
2794
- };
2795
- const fullVerticalSpan = Math.max(rowGap, maxY - minCenterY);
2796
2813
  const buildNodeBounds = (node) => {
2797
- const halfW = getHalfWidth(node.label);
2798
- const globalMinX = getNodeMinX(node.label);
2799
- const globalMaxX = getNodeMaxX(node.label);
2800
- const leftLimit = zoneShared - halfW - centerGuard;
2801
- const rightLimit = zoneShared + halfW + centerGuard;
2802
2814
  let minX = globalMinX;
2803
2815
  let maxX = globalMaxX;
2804
2816
  if (node.group === 'p1') {
2805
- maxX = Math.min(globalMaxX, leftLimit);
2806
- if (maxX < minX)
2807
- maxX = minX;
2817
+ minX = p1BoundsMinX;
2818
+ maxX = p1BoundsMaxX;
2808
2819
  }
2809
2820
  else if (node.group === 'p2') {
2810
- minX = Math.max(globalMinX, rightLimit);
2811
- if (minX > maxX)
2812
- minX = maxX;
2813
- }
2814
- else {
2815
- minX = Math.max(globalMinX, zoneShared - sharedBand);
2816
- maxX = Math.min(globalMaxX, zoneShared + sharedBand);
2817
- if (minX > maxX) {
2818
- const centerX = Math.max(globalMinX, Math.min(globalMaxX, zoneShared));
2819
- minX = centerX;
2820
- maxX = centerX;
2821
- }
2822
- }
2823
- const baseY = node.targetY ?? (minCenterY + fullVerticalSpan * 0.5);
2824
- const groupCount = groupCounts[node.group];
2825
- let ySlack = rowGap * 2.2;
2826
- if (node.group === 'shared') {
2827
- ySlack = Math.max(rowGap * 4.2, fullVerticalSpan * 0.58);
2821
+ minX = p2BoundsMinX;
2822
+ maxX = p2BoundsMaxX;
2828
2823
  }
2829
2824
  else {
2830
- const laneSpan = groupCount > 1 ? fullVerticalSpan / (groupCount - 1) : fullVerticalSpan;
2831
- ySlack = Math.min(fullVerticalSpan, Math.max(rowGap * 2.4, laneSpan * 0.95));
2832
- }
2833
- let minY = Math.max(minCenterY, baseY - ySlack);
2834
- let maxYForNode = Math.min(maxY, baseY + ySlack);
2835
- if (minY > maxYForNode) {
2836
- minY = minCenterY;
2837
- maxYForNode = maxY;
2825
+ minX = sharedBoundsMinX;
2826
+ maxX = sharedBoundsMaxX;
2838
2827
  }
2839
- 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 };
2840
2830
  };
2841
2831
  const nodeBounds = new Map();
2842
2832
  nodes.forEach(node => nodeBounds.set(node.id, buildNodeBounds(node)));
@@ -2978,20 +2968,21 @@ class ProfileComparisonLibComponent {
2978
2968
  return;
2979
2969
  const targetY = weightedY / totalWeight;
2980
2970
  const dy = targetY - (node.y ?? targetY);
2971
+ const sparseNeighbors = neighbors.length <= 2;
2981
2972
  const alignStrength = node.group === 'shared'
2982
- ? (neighbors.length <= 2 ? 0.62 : 0.52)
2983
- : 0.36;
2973
+ ? (sparseNeighbors ? 0.95 : 0.8)
2974
+ : (sparseNeighbors ? 0.82 : 0.68);
2984
2975
  node.vy = (node.vy ?? 0) + dy * alignStrength * alpha;
2985
2976
  });
2986
2977
  };
2987
2978
  this.ngZone.runOutsideAngular(() => {
2988
2979
  const sim = forceSimulation(nodes)
2989
- .alphaDecay(0.02)
2980
+ .alphaDecay(0.014)
2990
2981
  .stop()
2991
2982
  .force('link', forceLink(links)
2992
2983
  .id(d => d.id)
2993
2984
  .distance(d => d.distance)
2994
- .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))
2995
2986
  .force('collide', forceCollide((d) => {
2996
2987
  const baseRadius = getHalfWidth(d.label) * 0.55 + ProfileComparisonLibComponent.CHIP_COLLISION_BORDER_PADDING;
2997
2988
  return Math.min(58, Math.max(ProfileComparisonLibComponent.CHIP_RADIUS + 8, baseRadius));
@@ -3000,7 +2991,7 @@ class ProfileComparisonLibComponent {
3000
2991
  .iterations(2))
3001
2992
  .force('xBound', forceX(d => d.targetX ?? zoneShared)
3002
2993
  .strength(d => d.group === 'shared' ? 1.15 : 0.85))
3003
- .force('yTargetSeed', forceY(d => d.targetY ?? minCenterY).strength(0.34))
2994
+ .force('yTargetSeed', forceY(d => d.targetY ?? minCenterY).strength(0.1))
3004
2995
  .force('crossAlign', crossAlignForce);
3005
2996
  for (let i = 0; i < 420 && sim.alpha() > 0.0035; i++) {
3006
2997
  sim.tick();
@@ -3008,21 +2999,6 @@ class ProfileComparisonLibComponent {
3008
2999
  clampNodeToBounds(n);
3009
3000
  });
3010
3001
  }
3011
- const enforceGroupStack = (group) => {
3012
- const groupNodes = nodes
3013
- .filter(n => n.group === group)
3014
- .sort((a, b) => (a.y ?? minCenterY) - (b.y ?? minCenterY));
3015
- for (let i = 1; i < groupNodes.length; i++) {
3016
- const prevY = groupNodes[i - 1].y ?? minCenterY;
3017
- const minAllowed = prevY + rowGap * 0.92;
3018
- if ((groupNodes[i].y ?? minCenterY) < minAllowed) {
3019
- groupNodes[i].y = minAllowed;
3020
- }
3021
- }
3022
- };
3023
- enforceGroupStack('p1');
3024
- enforceGroupStack('shared');
3025
- enforceGroupStack('p2');
3026
3002
  nodes.forEach(n => {
3027
3003
  clampNodeToBounds(n);
3028
3004
  n.originalFx = n.x;
@@ -3679,12 +3655,8 @@ class ProfileComparisonLibComponent {
3679
3655
  const maxY = bounds?.maxY ?? fallbackMaxY;
3680
3656
  const clampedX = Math.max(minX, Math.min(maxX, relX));
3681
3657
  const clampedY = Math.max(minY, Math.min(maxY, relY));
3682
- const anchorX = this.draggedNode.originalFx ?? clampedX;
3683
- const anchorY = this.draggedNode.originalFy ?? clampedY;
3684
- const xElasticity = this.draggedNode.group === 'shared' ? 0.62 : 0.72;
3685
- const yElasticity = this.draggedNode.group === 'shared' ? 0.58 : 0.68;
3686
- this.draggedNode.fx = anchorX + (clampedX - anchorX) * xElasticity;
3687
- this.draggedNode.fy = anchorY + (clampedY - anchorY) * yElasticity;
3658
+ this.draggedNode.fx = clampedX;
3659
+ this.draggedNode.fy = clampedY;
3688
3660
  // Keep active force simulation running
3689
3661
  this.simulation.alpha(0.3).restart();
3690
3662
  }