Lloyd Richards Design
VISX
D3
SIMULATION

Jun 9, 2023

Play and Stop Timeline

Using the Brush from Visx; creating a timeline that is automaticaly moves with a set of controls to start and stop the timeline.

Followinf in the success of the 028 lab, I'm not going to use the @visx/brush component to create a timeline that is automaticaly moves across time. The idea is to have a timeline that is moving across time, and have some controls that can be used to start and stop the timeline.

JanFebMarAprMayJunJulAugSepOctNovDecJanCH 1CH 2CH 2CH 3

| Jan 1, 2021 - - - Feb 1, 2021 |

With a bit of work, I was able to get the timeline to move across time, and have a button that can be used to start and stop the timeline. I also added forward and rewind buttons and have a speed property which changes the number days it jumps each tick.

How it works

I started with the base <Timeline /> component from 028 and added some buttons to the bottom. These I styles and could hook into the onClick() to start and stop the animation.

const [selected, setSelected] = useState<[Date, Date]>([
  intervals[0],
  intervals[1],
]);
const [window, setWindow] = useState<[Date, Date]>([
  intervals[0],
  intervals[1],
]);
const [count, setCount] = useState<number>(0);
const [speed, setSpeed] = useState<number>(0);

The state is used to keep track of the selected and window intervals, the count of the current interval, and the speed at which the timeline moves.

const onBrushChange = (domain: Bounds | null) => {
  if (!domain) return;
  setSelected([new Date(domain.x0), new Date(domain.x1)]);
};
 
const ontTick = () => {
  if (!brushRef.current) return;
  brushRef.current.updateBrush((prevBrush) => {
    const newStart = addDays(window[0], speed);
    const newEnd = addDays(window[1], speed);
    const newExtent = brushRef.current!.getExtent(
      { x: xScale(newStart) },
      { x: xScale(newEnd) },
    );
    setWindow([newStart, newEnd]);
    const newState: BaseBrushState = {
      ...prevBrush,
      start: { y: newExtent.y0, x: newExtent.x0 },
      end: { y: newExtent.y1, x: newExtent.x1 },
      extent: newExtent,
    };
    return newState;
  });
};
 
const onReset = () => {
  setSpeed(0);
  setWindow([intervals[0], intervals[1]]);
  setSelected([intervals[0], intervals[1]]);
  setCount(0);
  if (!brushRef.current) return;
  brushRef.current.updateBrush((prevBrush) => {
    const newExtent = brushRef.current!.getExtent(
      { x: xScale(intervals[0]) },
      { x: xScale(intervals[1]) },
    );
    const newState: BaseBrushState = {
      ...prevBrush,
      start: { y: newExtent.y0, x: newExtent.x0 },
      end: { y: newExtent.y1, x: newExtent.x1 },
      extent: newExtent,
    };
    return newState;
  });
};

The onBrushChange() function is used to update the selected interval when the brush is moved. The onTick() function is used to update the window interval and move the brush. The onReset() function is used to reset the timeline to the start.

useInterval(
  () => {
    setCount(count + 1);
    ontTick();
  },
  speed != 0 ? 200 : null,
);

The useInterval() hook is from usehooks-ts is used to call the onTick() function every 200ms if the speed is not 0. This is used to animate the timeline.

Ideas for improvement

  • Add a slider to control the speed
  • Add framer motion to smooth out the animation
  • Move the animation logic into a custom hook