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 { useLayoutNoverlap, useWorkerLayoutNoverlap } from '@react-sigma/layout-noverlap'
|
||||
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 { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
||||
@@ -26,43 +26,161 @@ type LayoutName =
|
||||
| 'Force Directed'
|
||||
| '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 { 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()
|
||||
|
||||
// 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.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!sigma) {
|
||||
console.log('No sigma instance available')
|
||||
return
|
||||
}
|
||||
|
||||
// we run the algo
|
||||
// Auto-run if specified
|
||||
let timeout: number | null = null
|
||||
if (autoRunFor !== undefined && autoRunFor > -1 && sigma.getGraph().order > 0) {
|
||||
start()
|
||||
// set a timeout to stop it
|
||||
timeout =
|
||||
autoRunFor > 0
|
||||
? window.setTimeout(() => { stop() }, autoRunFor) // prettier-ignore
|
||||
: null
|
||||
}
|
||||
console.log('Auto-starting layout animation')
|
||||
|
||||
//cleaning
|
||||
return () => {
|
||||
stop()
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
// 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 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 (
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={() => (isRunning ? stop() : start())}
|
||||
onClick={handleClick}
|
||||
tooltip={isRunning ? t('graphPanel.sideBar.layoutsControl.stopAnimation') : t('graphPanel.sideBar.layoutsControl.startAnimation')}
|
||||
variant={controlButtonVariant}
|
||||
>
|
||||
@@ -86,7 +204,17 @@ const LayoutsControl = () => {
|
||||
const layoutCirclepack = useLayoutCirclepack()
|
||||
const layoutRandom = useLayoutRandom()
|
||||
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 workerNoverlap = useWorkerLayoutNoverlap()
|
||||
const workerForce = useWorkerLayoutForce()
|
||||
@@ -130,10 +258,23 @@ const LayoutsControl = () => {
|
||||
|
||||
const runLayout = useCallback(
|
||||
(newLayout: LayoutName) => {
|
||||
console.debug(newLayout)
|
||||
console.debug('Running layout:', newLayout)
|
||||
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]
|
||||
)
|
||||
@@ -157,7 +298,10 @@ const LayoutsControl = () => {
|
||||
</Button>
|
||||
<div>
|
||||
{layouts[layout] && 'worker' in layouts[layout] && (
|
||||
<WorkerLayoutControl layout={layouts[layout].worker!} />
|
||||
<WorkerLayoutControl
|
||||
layout={layouts[layout].worker!}
|
||||
mainLayout={layouts[layout].layout}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
|
Reference in New Issue
Block a user