The mysterious case of lines not pointing to the right direction during an animation

(Well, the guidelines asked if the title of my post was interesting, so…)

I am creating an animation that takes in some ‘path’ data and simulates people (red circles) walking on a path (gray line). Like this:

One feature is that the red lines projecting from the circles should point to their next destination on the path. The gray paths are visible for diagnostic purposes.

This, as you can see in the above image, does not happen.

While I expect something like this (although it’s also not perfect):
CleanShot 2022-03-26 at 00.09.07@2x

I get this:
CleanShot 2022-03-26 at 00.07.49@2x

This is strange behavior because the red circles (while moving) and the red lines (for pointing) use the same data. While the red lines stay on the gray path correctly, the red lines do not align with the gray path.

The problematic code is:

...
moveAgents = {
      replay;
      d3.selectAll('.agent')
        .remove()

      drawAgents()
  
      const agents = d3.selectAll('.agent')
      const timeStep = 1000
  
      _.zip( agents.nodes(), Object.entries(paths) ).forEach( ([agentNode, pathObject]) => {  // converstion to nodes is necessary for zip to work. (D3 selections are not iterable directly).

        const agent = d3.select(agentNode)
        const directionIndicator = agent.select('.agent-direction-indicator')
        const path = pathObject[1]  // pathObject[0] is a key, which is 1, 2, 3, etc...

        let currentStep = -1

        path.forEach( (position , i) => {
          
          const currentX = agent.select('circle').attr('cx')
          const currentY = agent.select('circle').attr('cy')

          const destinationX = x(position.x)
          const destinationY = y(position.y)

          const differenceX = destinationX - currentX
          const differenceY = destinationY - currentY
          
          const timeToInitializeMovement = position.time

          // Move agents along the path
          agent
            .transition()
            .duration( timeStep )
            .ease( d3.easeLinear )
            .delay( timeToInitializeMovement )
            .attr( 'transform', `translate(${differenceX}, ${differenceY})`)

          // Point the direction indicator towards the next destination on the path
          directionIndicator
            .transition()
            .duration( 0 )
            .ease( d3.easeLinear )
            .delay( timeToInitializeMovement )     
            .attr('x2', destinationX )
            .attr('y2', destinationY )

        } )  // forEach
        
      }) // zip-forEach

  return agents

}```

It is probably much easier to see it working in context, so I made a notebook here. More details are in it.

Anyone can perhaps recognize this issue and point me in the right direction?
This has been quite a pain, and I would greatly appreciate any help you might be able to offer.
Many thanks in advance!

Since the agent is a group (svg g) containing a circle, it can be confusing if you try to change the coordinates of both parent and circle at the same time. I’ve sent you a suggestion where the circle stays at <0,0> in the coordinate system of its parent.

Magic! :slight_smile:

Thank you so much for pointing out the issue and for your eloquent solution!

A couple of take-home messages for me has been:

  • One should watch out for multiple ‘move’ commands operating on the same object, directly or indirectly.

  • A group can be moved to an initial position using translate, which can save many lines of code that otherwise has to be written to specify the starting location of each object in the group. I previously thought translate was for moving objects from their existing positions.

For those who may one day visit this issue and would like to see the fixed version with Fil’s code implemented, it is here.

1 Like