Enhance graph layout control with improved animations and stability
- Added custom animation timer for smoother transitions - Improved Force Directed layout parameters - Added auto-stop feature for animations
This commit is contained in:
@@ -7,7 +7,7 @@ import { useLayoutForce, useWorkerLayoutForce } from '@react-sigma/layout-force'
|
|||||||
import { useLayoutForceAtlas2, useWorkerLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
|
import { useLayoutForceAtlas2, useWorkerLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
|
||||||
import { useLayoutNoverlap, useWorkerLayoutNoverlap } from '@react-sigma/layout-noverlap'
|
import { useLayoutNoverlap, useWorkerLayoutNoverlap } from '@react-sigma/layout-noverlap'
|
||||||
import { useLayoutRandom } from '@react-sigma/layout-random'
|
import { useLayoutRandom } from '@react-sigma/layout-random'
|
||||||
import { useCallback, useMemo, useState, useEffect } from 'react'
|
import { useCallback, useMemo, useState, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
||||||
@@ -26,43 +26,161 @@ type LayoutName =
|
|||||||
| 'Force Directed'
|
| 'Force Directed'
|
||||||
| 'Force Atlas'
|
| 'Force Atlas'
|
||||||
|
|
||||||
const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) => {
|
// Extend WorkerLayoutControlProps to include mainLayout
|
||||||
|
interface ExtendedWorkerLayoutControlProps extends WorkerLayoutControlProps {
|
||||||
|
mainLayout: LayoutHook;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WorkerLayoutControl = ({ layout, autoRunFor, mainLayout }: ExtendedWorkerLayoutControlProps) => {
|
||||||
const sigma = useSigma()
|
const sigma = useSigma()
|
||||||
const { stop, start, isRunning } = layout
|
// Use local state to track animation running status
|
||||||
|
const [isRunning, setIsRunning] = useState(false)
|
||||||
|
// Timer reference for animation
|
||||||
|
const animationTimerRef = useRef<number | null>(null)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
// Function to update node positions using the layout algorithm
|
||||||
|
const updatePositions = useCallback(() => {
|
||||||
|
if (!sigma) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const graph = sigma.getGraph()
|
||||||
|
if (!graph || graph.order === 0) return
|
||||||
|
|
||||||
|
// Use mainLayout to get positions, similar to refreshLayout function
|
||||||
|
// console.log('Getting positions from mainLayout')
|
||||||
|
const positions = mainLayout.positions()
|
||||||
|
|
||||||
|
// Animate nodes to new positions
|
||||||
|
// console.log('Updating node positions with layout algorithm')
|
||||||
|
animateNodes(graph, positions, { duration: 400 }) // Increase duration for smoother transitions
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating positions:', error)
|
||||||
|
// Stop animation if there's an error
|
||||||
|
if (animationTimerRef.current) {
|
||||||
|
window.clearInterval(animationTimerRef.current)
|
||||||
|
animationTimerRef.current = null
|
||||||
|
setIsRunning(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [sigma, mainLayout])
|
||||||
|
|
||||||
|
// Improved click handler that uses our own animation timer
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
if (isRunning) {
|
||||||
|
// Stop the animation
|
||||||
|
console.log('Stopping layout animation')
|
||||||
|
if (animationTimerRef.current) {
|
||||||
|
window.clearInterval(animationTimerRef.current)
|
||||||
|
animationTimerRef.current = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to kill the layout algorithm if it's running
|
||||||
|
try {
|
||||||
|
if (typeof layout.kill === 'function') {
|
||||||
|
layout.kill()
|
||||||
|
console.log('Layout algorithm killed')
|
||||||
|
} else if (typeof layout.stop === 'function') {
|
||||||
|
layout.stop()
|
||||||
|
console.log('Layout algorithm stopped')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error stopping layout algorithm:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRunning(false)
|
||||||
|
} else {
|
||||||
|
// Start the animation
|
||||||
|
console.log('Starting layout animation')
|
||||||
|
|
||||||
|
// Initial position update
|
||||||
|
updatePositions()
|
||||||
|
|
||||||
|
// Set up interval for continuous updates
|
||||||
|
animationTimerRef.current = window.setInterval(() => {
|
||||||
|
updatePositions()
|
||||||
|
}, 400) // Match interval with animation duration for smoother transitions
|
||||||
|
|
||||||
|
setIsRunning(true)
|
||||||
|
|
||||||
|
// Set a timeout to automatically stop the animation after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (animationTimerRef.current) {
|
||||||
|
console.log('Auto-stopping layout animation after 2 seconds')
|
||||||
|
window.clearInterval(animationTimerRef.current)
|
||||||
|
animationTimerRef.current = null
|
||||||
|
setIsRunning(false)
|
||||||
|
|
||||||
|
// Try to stop the layout algorithm
|
||||||
|
try {
|
||||||
|
if (typeof layout.kill === 'function') {
|
||||||
|
layout.kill()
|
||||||
|
} else if (typeof layout.stop === 'function') {
|
||||||
|
layout.stop()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error stopping layout algorithm:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
}, [isRunning, layout, updatePositions])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init component when Sigma or component settings change.
|
* Init component when Sigma or component settings change.
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sigma) {
|
if (!sigma) {
|
||||||
|
console.log('No sigma instance available')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// we run the algo
|
// Auto-run if specified
|
||||||
let timeout: number | null = null
|
let timeout: number | null = null
|
||||||
if (autoRunFor !== undefined && autoRunFor > -1 && sigma.getGraph().order > 0) {
|
if (autoRunFor !== undefined && autoRunFor > -1 && sigma.getGraph().order > 0) {
|
||||||
start()
|
console.log('Auto-starting layout animation')
|
||||||
// set a timeout to stop it
|
|
||||||
timeout =
|
|
||||||
autoRunFor > 0
|
|
||||||
? window.setTimeout(() => { stop() }, autoRunFor) // prettier-ignore
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
|
|
||||||
//cleaning
|
// Initial position update
|
||||||
return () => {
|
updatePositions()
|
||||||
stop()
|
|
||||||
if (timeout) {
|
// Set up interval for continuous updates
|
||||||
clearTimeout(timeout)
|
animationTimerRef.current = window.setInterval(() => {
|
||||||
|
updatePositions()
|
||||||
|
}, 400) // Match interval with animation duration for smoother transitions
|
||||||
|
|
||||||
|
setIsRunning(true)
|
||||||
|
|
||||||
|
// Set a timeout to stop it if autoRunFor > 0
|
||||||
|
if (autoRunFor > 0) {
|
||||||
|
timeout = window.setTimeout(() => {
|
||||||
|
console.log('Auto-stopping layout animation after timeout')
|
||||||
|
if (animationTimerRef.current) {
|
||||||
|
window.clearInterval(animationTimerRef.current)
|
||||||
|
animationTimerRef.current = null
|
||||||
|
}
|
||||||
|
setIsRunning(false)
|
||||||
|
}, autoRunFor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [autoRunFor, start, stop, sigma])
|
|
||||||
|
// Cleanup function
|
||||||
|
return () => {
|
||||||
|
// console.log('Cleaning up WorkerLayoutControl')
|
||||||
|
if (animationTimerRef.current) {
|
||||||
|
window.clearInterval(animationTimerRef.current)
|
||||||
|
animationTimerRef.current = null
|
||||||
|
}
|
||||||
|
if (timeout) {
|
||||||
|
window.clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
setIsRunning(false)
|
||||||
|
}
|
||||||
|
}, [autoRunFor, sigma, updatePositions])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => (isRunning ? stop() : start())}
|
onClick={handleClick}
|
||||||
tooltip={isRunning ? t('graphPanel.sideBar.layoutsControl.stopAnimation') : t('graphPanel.sideBar.layoutsControl.startAnimation')}
|
tooltip={isRunning ? t('graphPanel.sideBar.layoutsControl.stopAnimation') : t('graphPanel.sideBar.layoutsControl.startAnimation')}
|
||||||
variant={controlButtonVariant}
|
variant={controlButtonVariant}
|
||||||
>
|
>
|
||||||
@@ -86,7 +204,17 @@ const LayoutsControl = () => {
|
|||||||
const layoutCirclepack = useLayoutCirclepack()
|
const layoutCirclepack = useLayoutCirclepack()
|
||||||
const layoutRandom = useLayoutRandom()
|
const layoutRandom = useLayoutRandom()
|
||||||
const layoutNoverlap = useLayoutNoverlap({ settings: { margin: 1 } })
|
const layoutNoverlap = useLayoutNoverlap({ settings: { margin: 1 } })
|
||||||
const layoutForce = useLayoutForce({ maxIterations: maxIterations })
|
// Add parameters for Force Directed layout to improve convergence
|
||||||
|
const layoutForce = useLayoutForce({
|
||||||
|
maxIterations: maxIterations * 3, // Triple the iterations for better convergence
|
||||||
|
settings: {
|
||||||
|
attraction: 0.0003, // Lower attraction force to reduce oscillation
|
||||||
|
repulsion: 0.05, // Lower repulsion force to reduce oscillation
|
||||||
|
gravity: 0.01, // Increase gravity to make nodes converge to center faster
|
||||||
|
inertia: 0.4, // Lower inertia to add damping effect
|
||||||
|
maxMove: 100 // Limit maximum movement per step to prevent large jumps
|
||||||
|
}
|
||||||
|
})
|
||||||
const layoutForceAtlas2 = useLayoutForceAtlas2({ iterations: maxIterations })
|
const layoutForceAtlas2 = useLayoutForceAtlas2({ iterations: maxIterations })
|
||||||
const workerNoverlap = useWorkerLayoutNoverlap()
|
const workerNoverlap = useWorkerLayoutNoverlap()
|
||||||
const workerForce = useWorkerLayoutForce()
|
const workerForce = useWorkerLayoutForce()
|
||||||
@@ -130,10 +258,23 @@ const LayoutsControl = () => {
|
|||||||
|
|
||||||
const runLayout = useCallback(
|
const runLayout = useCallback(
|
||||||
(newLayout: LayoutName) => {
|
(newLayout: LayoutName) => {
|
||||||
console.debug(newLayout)
|
console.debug('Running layout:', newLayout)
|
||||||
const { positions } = layouts[newLayout].layout
|
const { positions } = layouts[newLayout].layout
|
||||||
animateNodes(sigma.getGraph(), positions(), { duration: 500 })
|
|
||||||
setLayout(newLayout)
|
try {
|
||||||
|
const graph = sigma.getGraph()
|
||||||
|
if (!graph) {
|
||||||
|
console.error('No graph available')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = positions()
|
||||||
|
console.log('Positions calculated, animating nodes')
|
||||||
|
animateNodes(graph, pos, { duration: 500 })
|
||||||
|
setLayout(newLayout)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error running layout:', error)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[layouts, sigma]
|
[layouts, sigma]
|
||||||
)
|
)
|
||||||
@@ -157,7 +298,10 @@ const LayoutsControl = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div>
|
||||||
{layouts[layout] && 'worker' in layouts[layout] && (
|
{layouts[layout] && 'worker' in layouts[layout] && (
|
||||||
<WorkerLayoutControl layout={layouts[layout].worker!} />
|
<WorkerLayoutControl
|
||||||
|
layout={layouts[layout].worker!}
|
||||||
|
mainLayout={layouts[layout].layout}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
Reference in New Issue
Block a user