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:
yangdx
2025-03-17 15:18:33 +08:00
parent 9e9c0bdd96
commit f231f2df7a

View File

@@ -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>