There are multiple strategies to consider, each varying in effectiveness.
Ultimately, expensive operations are being performed every animation frame to calculate the new location of each point and re-render it. Therefore, it is crucial to minimize the cost of these operations.
If the frame rate drops below 60, it indicates that the CPU capacity is reaching its limit. Using frame rate as an indicator of CPU capacity allows for easier measurement compared to CPU usage.
I had many charts and theories planned for this approach, but once written out, it felt like common sense and I chose not to elaborate on it further.
The main objective is to enhance the number of transitions displayed at 60 frames per second, enabling a reduction in transitions to increase CPU capacity.
Let's work on running transitions with over 100 nodes along more than 100 paths at 60 frames per second.
D3v4
Incorporating d3v4 can offer some advantages. Synchronized transitions in version 4 have slightly improved performance times. Although transitioning with d3.transition is already efficient and inexpensive, upgrading to version 4 is worth considering.
Optimizing for different browsers by utilizing shaped nodes, transforming positions, or using cx,cy properties might yield minor gains. However, these optimizations were not implemented due to their relatively inconsequential benefits.
Canvas
SVG struggles to keep up with high-speed movements. Manipulating the DOM requires time, and additional elements slow down operations and consume more memory. While canvas may be less convenient from a programming standpoint, it proves to be faster than SVG for this particular task. Utilize detached circle elements to represent each node and transition them accordingly.
Improve efficiency by drawing two canvases: one for initial drawing and path storage (if necessary), and another to redraw each frame displaying the points. Set the datum of each circle to the length of the path it belongs to avoid calling path.getTotalLength() repetitively.
You may find examples like this helpful.
Canvas Simplified Lines
Using a detached node with SVG paths enables the use of path.getPointAtLength()
, which can be quite effective. However, curved lines significantly hinder performance. Whenever possible, opt for straight lines (even if composed of multiple segments) for substantial improvements.
As an added benefit, use context.fillRect()
instead of context.arc()
.
Pure JS and Canvas
In scenarios where D3 and detached nodes become cumbersome, consider leaving them behind and using typed arrays, context.imageData, and custom positioning formulas for nodes along paths. For quick reference, here's a simple example (100,000 nodes, 500,000 nodes, 1,000,000 nodes). These examples showcase transitions of 700,000 nodes at 10 frames per second on a slower system. Comparing against my experience with d3v3 and SVG, the difference in transition calculations and renderings per second is remarkable:
https://i.sstatic.net/CRbkc.png
canvas A employs cardinal curves and circular markers, while canvas B uses straight lines and square markers (as depicted above).
An apparatus capable of rendering 1,000 transitioning nodes at 60 frames per second will have surplus capacity when handling only 100 nodes.
By halving the number of nodes, you can free up roughly half of the CPU capacity if transition calculations dominate activity and CPU usage nears 100%. In a slow canvas scenario mentioned earlier, transitioning 200 nodes at 60 frames per second allowed for approximately 50% CPU usage; reducing nodes to 100 resulted in a pleasant ~50% CPU utilization:
https://i.sstatic.net/GehRx.png
A horizontal centerline signifies 50% CPU usage, with the transition repeated six times.
To achieve significant savings, prioritize replacing complex cardinal curves with straight lines whenever feasible. Additionally, tailor your scripts to cater specifically to your needs for optimal results.
Compare these adjustments with the inclusion of straight lines and square nodes:
https://i.sstatic.net/cYiNI.png
Again, a horizontal centerline indicates 50% CPU usage, with the transition repeated six times.
Rendering 1,000 transitioning nodes on 1,000 paths consisting of three segments showcases over a tenfold improvement compared to employing curved lines and circular markers.
Other Options
These approaches can be combined with those discussed earlier.
Avoid animating every point during each tick
If positioning all nodes during each transition tick leads to excessive CPU usage, reconsider this strategy. Instead of updating every node each tick, position only a fraction of circles, allowing each circle to retain smooth motion at 20 frames per second while reducing total calculations per frame. For canvas applications, nodes must still be rendered, but calculating positions for two-thirds of the nodes could be skipped. With SVG, modifying d3-transition to incorporate an every()
method specifying how often transition values update could simplify this process.
Caching
While caching can be beneficial under certain circumstances, front-loading calculations or data loading might cause delays in initiating animations or impact performance upon first execution. Though caching has yielded positive outcomes in specific cases, refer to another answer for further information.