@@ -1802,70 +1802,61 @@ export function InteractiveGraphVisualization({ onResetLayout }: InteractiveGrap
18021802 simulation . alpha ( 0 ) . stop ( ) ;
18031803 }
18041804
1805+ const getNodeLayer = ( node : any ) => {
1806+ const typeHierarchy : Record < string , number > = {
1807+ 'OUTCOME' : 0 ,
1808+ 'EPIC' : 1 ,
1809+ 'MILESTONE' : 2 ,
1810+ 'FEATURE' : 3 ,
1811+ 'TASK' : 4 ,
1812+ 'BUG' : 5 ,
1813+ 'IDEA' : 6
1814+ } ;
1815+ return typeHierarchy [ node . type ] || 4 ;
1816+ } ;
1817+
18051818 simulation
18061819 . force ( 'link' , d3 . forceLink ( validatedEdges )
18071820 . id ( ( d : any ) => d . id )
18081821 . distance ( ( d : any ) => {
1809- const minDistance = Math . min ( width , height ) * 0.4 ; // 40% of screen size for minimum
1810- const maxDistance = Math . min ( width , height ) * 0.6 ; // 60% of screen size for maximum
1811-
1812- // Calculate current distance between nodes
1813- const sourceNode = d . source ;
1814- const targetNode = d . target ;
1815- const currentDistance = Math . sqrt (
1816- Math . pow ( targetNode . x - sourceNode . x , 2 ) +
1817- Math . pow ( targetNode . y - sourceNode . y , 2 )
1818- ) ;
1819-
1820- // If current distance exceeds maximum, return maximum to create pulling force
1821- if ( currentDistance > maxDistance ) {
1822- return maxDistance ;
1823- }
1824-
1825- // Otherwise use minimum as preferred distance
1826- return minDistance ;
1827- } )
1828- . strength ( ( d : any ) => {
1829- // Calculate current distance between nodes
1830- const sourceNode = d . source ;
1831- const targetNode = d . target ;
1832- const currentDistance = Math . sqrt (
1833- Math . pow ( targetNode . x - sourceNode . x , 2 ) +
1834- Math . pow ( targetNode . y - sourceNode . y , 2 )
1835- ) ;
1836-
1837- const maxDistance = Math . min ( width , height ) * 0.6 ;
1838-
1839- // Stronger force when edge is too long to create pulling effect
1840- if ( currentDistance > maxDistance ) {
1841- return 0.8 ; // Strong pulling force
1842- }
1843-
1844- // Normal strength otherwise
1845- return 0.3 ;
1822+ const sourceLayer = getNodeLayer ( d . source ) ;
1823+ const targetLayer = getNodeLayer ( d . target ) ;
1824+ const layerDiff = Math . abs ( sourceLayer - targetLayer ) ;
1825+ return layerDiff > 0 ? 250 : 350 ;
18461826 } )
1827+ . strength ( 0.3 )
18471828 )
18481829 . force ( 'charge' , d3 . forceManyBody ( )
1849- . strength ( - 100 ) // Simple, consistent repulsion for all nodes
1850- . distanceMax ( 200 ) // Reasonable influence range
1830+ . strength ( ( d : any ) => {
1831+ const layer = getNodeLayer ( d ) ;
1832+ return layer === 1 ? - 1200 : - 900 ;
1833+ } )
1834+ . distanceMax ( 700 )
18511835 )
1852- . force ( 'center' , d3 . forceCenter ( centerX , centerY ) . strength ( 0.01 ) ) // Minimal centering
1853- . force ( 'x' , d3 . forceX ( centerX ) . strength ( 0.002 ) ) // Extremely weak horizontal centering for maximum width
1854- . force ( 'y' , d3 . forceY ( centerY ) . strength ( 0.002 ) ) // Extremely weak vertical centering for maximum height
1855- . force ( 'collision' , d3 . forceCollide ( 90 ) // Sufficient collision radius to prevent overlap
1856- . strength ( 0.7 ) // Moderate collision prevention
1857- . iterations ( 2 ) // Fewer iterations for stability
1836+ . force ( 'center' , d3 . forceCenter ( centerX , centerY ) . strength ( 0.02 ) )
1837+ . force ( 'radial' , d3 . forceRadial ( ( d : any ) => {
1838+ const layer = getNodeLayer ( d ) ;
1839+ return layer * 180 + 100 ;
1840+ } , centerX , centerY ) . strength ( 0.15 ) )
1841+ . force ( 'x' , d3 . forceX ( centerX ) . strength ( 0.005 ) )
1842+ . force ( 'y' , d3 . forceY ( centerY ) . strength ( 0.005 ) )
1843+ . force ( 'collision' , d3 . forceCollide ( )
1844+ . radius ( ( d : any ) => {
1845+ const dims = getNodeDimensions ( d ) ;
1846+ return Math . max ( dims . width , dims . height ) / 2 + 40 ;
1847+ } )
1848+ . strength ( 1 )
1849+ . iterations ( 4 )
18581850 )
1859- // Add hierarchical attraction forces (Epic->Milestone, Feature->Task, etc.)
18601851 . force ( 'hierarchy' , d3 . forceLink ( )
18611852 . id ( ( d : any ) => d . id )
18621853 . links ( createHierarchicalLinks ( nodes ) )
1863- . distance ( ( d : any ) => d . distance || 250 ) // Much larger hierarchical distance
1864- . strength ( ( d : any ) => d . strength || 0.05 ) // Very weak hierarchical strength
1854+ . distance ( ( d : any ) => d . distance || 400 )
1855+ . strength ( ( d : any ) => d . strength || 0.02 )
18651856 )
1866- . alphaTarget ( 0.05 ) // Lower alpha target for calmer simulation
1867- . alphaDecay ( 0.015 ) // Slightly slower decay for better collision resolution
1868- . velocityDecay ( 0.4 ) ; // Add velocity decay for smoother movement
1857+ . alphaTarget ( 0.02 )
1858+ . alphaDecay ( 0.025 )
1859+ . velocityDecay ( 0.6 )
18691860
18701861 // Filter edges based on visible nodes for performance
18711862 // Temporarily show ALL edges for debugging
@@ -1888,7 +1879,22 @@ export function InteractiveGraphVisualization({ onResetLayout }: InteractiveGrap
18881879 }
18891880 } ;
18901881
1891- // Helper: Calculate smooth curved path directly between node edges
1882+ // Helper: Count edges between same nodes for bundle routing
1883+ const edgeBundleMap = new Map < string , number > ( ) ;
1884+ const edgeIndexMap = new Map < string , number > ( ) ;
1885+ visibleEdges . forEach ( ( edge : WorkItemEdge ) => {
1886+ const sourceId = typeof edge . source === 'string' ? edge . source : ( edge . source as any ) . id ;
1887+ const targetId = typeof edge . target === 'string' ? edge . target : ( edge . target as any ) . id ;
1888+ const key1 = `${ sourceId } -${ targetId } ` ;
1889+ const key2 = `${ targetId } -${ sourceId } ` ;
1890+ const key = key1 < key2 ? key1 : key2 ;
1891+
1892+ const currentCount = edgeBundleMap . get ( key ) || 0 ;
1893+ edgeIndexMap . set ( edge . id , currentCount ) ;
1894+ edgeBundleMap . set ( key , currentCount + 1 ) ;
1895+ } ) ;
1896+
1897+ // Helper: Calculate smooth curved path with anti-crossing routing
18921898 const linkArc = ( d : any ) => {
18931899 const dx = d . target . x - d . source . x ;
18941900 const dy = d . target . y - d . source . y ;
@@ -1916,49 +1922,39 @@ export function InteractiveGraphVisualization({ onResetLayout }: InteractiveGrap
19161922 const tx = targetHandle . x ;
19171923 const ty = targetHandle . y ;
19181924
1925+ const bundleKey1 = `${ d . source . id } -${ d . target . id } ` ;
1926+ const bundleKey2 = `${ d . target . id } -${ d . source . id } ` ;
1927+ const bundleKey = bundleKey1 < bundleKey2 ? bundleKey1 : bundleKey2 ;
1928+ const bundleCount = edgeBundleMap . get ( bundleKey ) || 1 ;
1929+ const edgeIndex = edgeIndexMap . get ( d . id ) || 0 ;
1930+
19191931 const edgeHash = ( d . source . id + d . target . id + ( d . type || '' ) ) . split ( '' ) . reduce ( ( a : number , b : string ) => {
19201932 a = ( ( a << 5 ) - a ) + b . charCodeAt ( 0 ) ;
19211933 return a & a ;
19221934 } , 0 ) ;
1923- const curveOffset = ( ( Math . abs ( edgeHash ) % 60 ) - 30 ) * 3.5 ;
19241935
1925- const distance = Math . sqrt ( dx * dx + dy * dy ) ;
1926- const controlPointDistance = Math . min ( distance * 0.6 , 150 ) ;
1927-
1928- let controlX1 = sx ;
1929- let controlY1 = sy ;
1930- let controlX2 = tx ;
1931- let controlY2 = ty ;
1932-
1933- if ( sourceHandleSide === 'right' ) {
1934- controlX1 = sx + controlPointDistance ;
1935- controlY1 = sy + curveOffset ;
1936- } else if ( sourceHandleSide === 'left' ) {
1937- controlX1 = sx - controlPointDistance ;
1938- controlY1 = sy + curveOffset ;
1939- } else if ( sourceHandleSide === 'bottom' ) {
1940- controlX1 = sx + curveOffset ;
1941- controlY1 = sy + controlPointDistance ;
1942- } else if ( sourceHandleSide === 'top' ) {
1943- controlX1 = sx + curveOffset ;
1944- controlY1 = sy - controlPointDistance ;
1936+ let curveOffset = 0 ;
1937+ if ( bundleCount > 1 ) {
1938+ const spreadRange = bundleCount * 50 ;
1939+ const step = spreadRange / ( bundleCount - 1 ) ;
1940+ curveOffset = ( edgeIndex * step ) - ( spreadRange / 2 ) ;
1941+ } else {
1942+ curveOffset = ( ( Math . abs ( edgeHash ) % 80 ) - 40 ) * 2.5 ;
19451943 }
19461944
1947- if ( targetHandleSide === 'right' ) {
1948- controlX2 = tx + controlPointDistance ;
1949- controlY2 = ty - curveOffset ;
1950- } else if ( targetHandleSide === 'left' ) {
1951- controlX2 = tx - controlPointDistance ;
1952- controlY2 = ty - curveOffset ;
1953- } else if ( targetHandleSide === 'bottom' ) {
1954- controlX2 = tx - curveOffset ;
1955- controlY2 = ty + controlPointDistance ;
1956- } else if ( targetHandleSide === 'top' ) {
1957- controlX2 = tx - curveOffset ;
1958- controlY2 = ty - controlPointDistance ;
1959- }
1945+ const distance = Math . sqrt ( dx * dx + dy * dy ) ;
1946+ const controlPointDistance = Math . min ( distance * 0.7 , 200 ) ;
1947+
1948+ const angle = Math . atan2 ( dy , dx ) ;
1949+ const perpAngle = angle + Math . PI / 2 ;
1950+
1951+ const midX = ( sx + tx ) / 2 ;
1952+ const midY = ( sy + ty ) / 2 ;
1953+
1954+ const controlX1 = midX + Math . cos ( perpAngle ) * curveOffset ;
1955+ const controlY1 = midY + Math . sin ( perpAngle ) * curveOffset ;
19601956
1961- return `M${ sx } ,${ sy } C ${ controlX1 } ,${ controlY1 } ${ controlX2 } , ${ controlY2 } ${ tx } ,${ ty } ` ;
1957+ return `M${ sx } ,${ sy } Q ${ controlX1 } ,${ controlY1 } ${ tx } ,${ ty } ` ;
19621958 } ;
19631959
19641960 // Create edges FIRST (so they render under nodes)
@@ -1990,19 +1986,19 @@ export function InteractiveGraphVisualization({ onResetLayout }: InteractiveGrap
19901986 return config . hexColor ;
19911987 } )
19921988 . attr ( 'stroke-width' , ( d : WorkItemEdge ) => {
1993- // Much thicker for active dialog
19941989 if ( editingEdge && editingEdge . edge && editingEdge . edge . id === d . id ) {
19951990 return 12 ;
19961991 }
1997- return 3 ;
1992+ return 2.5 ;
19981993 } )
19991994 . attr ( 'stroke-opacity' , ( d : WorkItemEdge ) => {
2000- // Full opacity for active dialog
20011995 if ( editingEdge && editingEdge . edge && editingEdge . edge . id === d . id ) {
20021996 return 1 ;
20031997 }
2004- return 0.9 ;
1998+ return 0.75 ;
20051999 } )
2000+ . attr ( 'stroke-linecap' , 'round' )
2001+ . attr ( 'stroke-linejoin' , 'round' )
20062002 . style ( 'filter' , ( d : WorkItemEdge ) => {
20072003 // Apply pulsing glow to edges with active dialogs
20082004 if ( editingEdge && editingEdge . edge && editingEdge . edge . id === d . id ) {
@@ -3550,64 +3546,42 @@ export function InteractiveGraphVisualization({ onResetLayout }: InteractiveGrap
35503546 const tx = targetHandle . x ;
35513547 const ty = targetHandle . y ;
35523548
3549+ const bundleKey1 = `${ d . source . id } -${ d . target . id } ` ;
3550+ const bundleKey2 = `${ d . target . id } -${ d . source . id } ` ;
3551+ const bundleKey = bundleKey1 < bundleKey2 ? bundleKey1 : bundleKey2 ;
3552+ const bundleCount = edgeBundleMap . get ( bundleKey ) || 1 ;
3553+ const edgeIndex = edgeIndexMap . get ( d . id ) || 0 ;
3554+
35533555 const edgeHash = ( d . source . id + d . target . id + ( d . type || '' ) ) . split ( '' ) . reduce ( ( a : number , b : string ) => {
35543556 a = ( ( a << 5 ) - a ) + b . charCodeAt ( 0 ) ;
35553557 return a & a ;
35563558 } , 0 ) ;
3557- const curveOffset = ( ( Math . abs ( edgeHash ) % 60 ) - 30 ) * 3.5 ;
35583559
3559- const distance = Math . sqrt ( dx * dx + dy * dy ) ;
3560- const controlPointDistance = Math . min ( distance * 0.6 , 150 ) ;
3561-
3562- let controlX1 = sx ;
3563- let controlY1 = sy ;
3564- let controlX2 = tx ;
3565- let controlY2 = ty ;
3566-
3567- if ( sourceHandleSide === 'right' ) {
3568- controlX1 = sx + controlPointDistance ;
3569- controlY1 = sy + curveOffset ;
3570- } else if ( sourceHandleSide === 'left' ) {
3571- controlX1 = sx - controlPointDistance ;
3572- controlY1 = sy + curveOffset ;
3573- } else if ( sourceHandleSide === 'bottom' ) {
3574- controlX1 = sx + curveOffset ;
3575- controlY1 = sy + controlPointDistance ;
3576- } else if ( sourceHandleSide === 'top' ) {
3577- controlX1 = sx + curveOffset ;
3578- controlY1 = sy - controlPointDistance ;
3560+ let curveOffset = 0 ;
3561+ if ( bundleCount > 1 ) {
3562+ const spreadRange = bundleCount * 50 ;
3563+ const step = spreadRange / ( bundleCount - 1 ) ;
3564+ curveOffset = ( edgeIndex * step ) - ( spreadRange / 2 ) ;
3565+ } else {
3566+ curveOffset = ( ( Math . abs ( edgeHash ) % 80 ) - 40 ) * 2.5 ;
35793567 }
35803568
3581- if ( targetHandleSide === 'right' ) {
3582- controlX2 = tx + controlPointDistance ;
3583- controlY2 = ty - curveOffset ;
3584- } else if ( targetHandleSide === 'left' ) {
3585- controlX2 = tx - controlPointDistance ;
3586- controlY2 = ty - curveOffset ;
3587- } else if ( targetHandleSide === 'bottom' ) {
3588- controlX2 = tx - curveOffset ;
3589- controlY2 = ty + controlPointDistance ;
3590- } else if ( targetHandleSide === 'top' ) {
3591- controlX2 = tx - curveOffset ;
3592- controlY2 = ty - controlPointDistance ;
3593- }
3569+ const distance = Math . sqrt ( dx * dx + dy * dy ) ;
3570+ const angle = Math . atan2 ( dy , dx ) ;
3571+ const perpAngle = angle + Math . PI / 2 ;
3572+
3573+ const midX = ( sx + tx ) / 2 ;
3574+ const midY = ( sy + ty ) / 2 ;
3575+
3576+ const controlX = midX + Math . cos ( perpAngle ) * curveOffset ;
3577+ const controlY = midY + Math . sin ( perpAngle ) * curveOffset ;
35943578
35953579 const t = 0.5 ;
3596- const curveX = Math . pow ( 1 - t , 3 ) * sx +
3597- 3 * Math . pow ( 1 - t , 2 ) * t * controlX1 +
3598- 3 * ( 1 - t ) * Math . pow ( t , 2 ) * controlX2 +
3599- Math . pow ( t , 3 ) * tx ;
3600- const curveY = Math . pow ( 1 - t , 3 ) * sy +
3601- 3 * Math . pow ( 1 - t , 2 ) * t * controlY1 +
3602- 3 * ( 1 - t ) * Math . pow ( t , 2 ) * controlY2 +
3603- Math . pow ( t , 3 ) * ty ;
3604-
3605- const tangentX = 3 * Math . pow ( 1 - t , 2 ) * ( controlX1 - sx ) +
3606- 6 * ( 1 - t ) * t * ( controlX2 - controlX1 ) +
3607- 3 * Math . pow ( t , 2 ) * ( tx - controlX2 ) ;
3608- const tangentY = 3 * Math . pow ( 1 - t , 2 ) * ( controlY1 - sy ) +
3609- 6 * ( 1 - t ) * t * ( controlY2 - controlY1 ) +
3610- 3 * Math . pow ( t , 2 ) * ( ty - controlY2 ) ;
3580+ const curveX = Math . pow ( 1 - t , 2 ) * sx + 2 * ( 1 - t ) * t * controlX + Math . pow ( t , 2 ) * tx ;
3581+ const curveY = Math . pow ( 1 - t , 2 ) * sy + 2 * ( 1 - t ) * t * controlY + Math . pow ( t , 2 ) * ty ;
3582+
3583+ const tangentX = 2 * ( 1 - t ) * ( controlX - sx ) + 2 * t * ( tx - controlX ) ;
3584+ const tangentY = 2 * ( 1 - t ) * ( controlY - sy ) + 2 * t * ( ty - controlY ) ;
36113585
36123586 const tangentLength = Math . sqrt ( tangentX * tangentX + tangentY * tangentY ) ;
36133587 const normalX = - tangentY / tangentLength ;
0 commit comments