d3.jsdata-visualizationd3-force-directed

d3-force Make nodes move slower on position update


I'm using d3's force-layout and would like to move nodes on an event trigger.

I've created a a minimal block here showing the desired functionality.

As you can see, when the button is clicked, the nodes move rapidly to their new position. I'd like to slow this down. I've played with combinations of (what I thought to be) relevant attributes specified in the documentation (e.g. alpha, alphaDecay, velocityDecay), but to no avail.

To recap: how do I make the nodes move slower on position updates?

Thanks!


Solution

  • You probably want to use a high velocity decay. A value of 0.9 will slow the ticks to 0.1 of their velocity, "after the application of any forces during a tick, each node’s velocity is multiplied by 1 - [velocity]decay.(docs)". This will certainly slow down movement. A value of 0.9 probably is overkill in most situations.

    However, in conjunction with this, we need to ensure alpha decay is low: if alpha decay is too high, the simulation will have cooled prior to the nodes reaching their endpoint. I used 0.0005 in this example below,

    Finally, to address the jumpiness of the simulation on recentering, I have lowered the alpha to reduce the appearance of jittering, of course the lower you move alpha, the lower alphaDecay must be to support a simulation cooling off period of the same duration

    Used below:

    const width = 500
        const height = 500
        const svg = d3.select("svg")
    
        const trtCenter = width / 5
    	const cntrlCenter = width / 1.5
        
        let sampleData = d3.range(24).map((d,i) => ({r: 40 - i * 0.5}))
        
        // define force
        let force = d3.forceSimulation()
        	.force('charge', d3.forceManyBody().strength(1))
        	force.force('x', d3.forceX().strength(.3).x( width / 2))
    		force.force('y', d3.forceY().strength(.3).y(height / 3.5))
        	.force('collision', d3.forceCollide(d => 12))    	
        	.nodes(sampleData)
        	.on('tick', changeNetwork)
    
        let dots = svg.selectAll('.dot')
        	.data(sampleData)
        	.enter()
        	.append('g')
        	.attr('class', 'dot')
        	.attr('group', (d,i) => i % 2 == 0 ? 'trt' : 'ctrl')
        	.append('circle')
        	.attr('r', 10)
        	.attr('fill', (d,i) => i % 2 == 0 ? 'pink' : 'olive')
        	.attr('stroke', 'black')
        	.attr('stroke-width', .4)
    
        function nodeTreatmentPos(d) {
    	  return d.index % 2 == 0 ? trtCenter : cntrlCenter;
    	}
    
    
       	function changeNetwork() {
          d3.selectAll('g.dot')
          	.attr('transform', d=> `translate(${d.x}, ${d.y})`)
        }
      
        // 
    	function moveNodes() {
    	  force.force('center', null)
    		.force('collision', d3.forceCollide(d => 12))
    		.alphaDecay(.0005)
    		.velocityDecay(0.6)
    		force.force('x', d3.forceX().strength(1).x(nodeTreatmentPos))
    		force.force('y', d3.forceY().strength(1).y(height / 3.5))
            force.alpha(.1).restart();
    	}
    
    	// force for center
    	function moveCenter() {
    	force//.force('center', null)
    		.force('collision', d3.forceCollide(d => 12))
    		.alphaDecay(.0005)
    		.velocityDecay(0.6)
    		force.force('x', d3.forceX().strength(1).x( width / 2))
    		force.force('y', d3.forceY().strength(1).y(height / 3.5))
    		force.alpha(.1).restart();
    		}
    
    	// resolve locations of node on cliks
    	let toCenter = true;
    
    	d3.select('#clickMe')
    		.on('click', function() {
    			toCenter === true ? moveNodes() : moveCenter()
    			toCenter = !toCenter
    		})
    <script src='https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js'></script>
    <button id="clickMe" type="button">Move Nodes</button>
    <svg id="svg" width="1200" height="500"></svg>