Performance problem with 'Point-along-path' d3 visualization

I recently explored a d3 visualization where a point moves along a path, following the code example provided at https://bl.ocks.org/mbostock/1705868. During this movement, I observed that the CPU usage ranges from 7 to 11%.

In my current project, there are approximately 100 paths, and on each path, points (represented by circles) need to move from source to destination. This high number of simultaneous movements results in over 90% CPU consumption.

I attempted the following solution:

function translateAlong(path) {
    var length = path.getTotalLength();
    return function(d, i, a) {
        return function(t) {
            var point = path.getPointAtLength(t * length);
            return "translate(" + point.x + "," + point.y + ")";
        };
     };
}

var movingCircle = mapNode.append('circle')
    .attr('r', 2)
    .attr('fill', 'white')

movingCircle.transition()
    .duration(10000)
    .ease("linear")
    .attrTween("transform", translateAlong(path.node()))
    .each("end", function() {
        this.remove();
    });

How can we optimize this process to reduce CPU usage? Your insights are appreciated. Thank you.

Answer №1

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.

Answer №2

Update:

  • Here is the original code. (peak CPU around %99 for 100 points at 2.7Ghz i7)
  • Here is my optimized version. (peak CPU around 20% for 100 points at 2.7Ghz i7)

I have achieved an average speed improvement of 5 times.

The main bottleneck seems to be the frequent call to the getPointAtLength method every 17ms. To enhance performance, I recommend caching points in advance and calculating them only once with a specified resolution (such as dividing into 1000 parts).

  • Avoid excessive DOM method calls within requestAnimationFrame.

In the default scenario, there are 2 function calls - one when invoking getPointAtLength and then another during setting the translate operation.

You can replace the existing translateAlong function with the modified version below:

 // Updated function definitions go here

Apply this slight tweak to the tweening line:

.tween("transform", translateAlong(path.node()))

Remember, setting attributes is unnecessary; calling is sufficient. Check out the outcome at: http://jsfiddle.net/ibowankenobi/8kx04y29/

Please provide feedback on whether these changes resulted in any noticeable improvements.

Answer №3

To achieve this effect, consider using svg:animateMotion to move an element along a specified path. Take a look at the example provided in the documentation. In simple terms, you would need:

<svg>
  <path id="path1" d="...">
    <circle cx="" cy="" r="10" fill="red">
      <animateMotion dur="10s" repeatCount="0">
        <mpath xlink:href="#path1" />
      </animateMotion>
    </circle>
  </path>
</svg>

Although I haven't done performance testing on this method, utilizing built-in SVG functionality should provide optimal results.

Browser Compatibility

Following a suggestion from @ibrahimtanyalcin, it's worth noting that this feature is not supported in Internet Explorer or Microsoft Edge browsers.

Answer №4

My observations on computer performance:

  • @mbostock is utilizing 8% of CPU.
  • @ibrahimtanyalcin is using 11% CPU.
  • @Ian's CPU usage stands at 10%.

For @mbostock and @ibrahimtanyalcin, the CPU is consumed by the transition and transform updates.

If I incorporate 100 of these animations into a single SVG file, the breakdown is as follows:

  • @mbostock takes up 50% of CPU (fully utilizing one core).
  • @Ian's CPU usage is at 40%.

All animations appear to run smoothly without issues.

To enhance performance, one strategy could be to introduce a pause in the transform update function, as suggested here.

Edit

In an insightful response by Andrew Reid (source), several optimizations were highlighted.

I conducted a modified version of the Canvas+JS test with 100,000 iterations focusing solely on the calculation aspect within a 4000ms timeframe. Flexibility was explored through toggling order=false, controlled by t.

To ensure consistency, a custom random number generator was implemented for each run of the adapted code.

The block-based variation yielded 200 iterations.

Adhering to the guidance regarding parseInt():

Avoid substituting Math.floor() with parseInt().

The switch from parseInt() to Math.floor() resulted in 218 iterations.

Upon identification of a seemingly redundant line within the inner loop:

let p = new Int16Array(2);

Replacing it with:

let p;

elevated the performance to 300 iterations.

Implementing these adjustments achieved a higher capability to handle additional points while maintaining a 60 Hz frame rate.

Further experimentation unveiled that certain strategies led to decreased efficiency.

An interesting finding was that pre-calculating segment lengths and simplifying the calculation of segment with basic array lookups proved slower than the combined approach involving array operations such as Math.pow, additions, and Math.sqrt.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Unable to properly display the message in Angular

<!DOCTYPE html> <html ng-app> <head> <script data-require="angular.js@*" data-semver="1.4.3" src="https://code.angularjs.org/1.4.3/angular.js"></script> <link rel="stylesheet" href="style.css" /> ...

Leveraging the power of Promise creation

I am facing an issue where I am trying to utilize the getPromise function with various URLs in order to obtain different promises, but encountering undefined values in the success function of the second promise. var http=require('http'); var URL ...

What is the reason for the clearInterval consistently functioning after the completion of the function?

Just starting out in web programming and currently delving into javascript and jquery ajax.. This snippet represents my javascript code. The refresh variable controls an interval for updating the chat by fetching chats from the database and displaying the ...

Setting the default tab in easyTabs to be all-powerful

My ajax content is being loaded into a div-container through various navigation links, and I am using the script below to establish the defaultTab: $(document).ready( function() { $("#tab-container").easytabs({updateHash: true, defaultTab: "li:eq(3)"}); ...

Combining multiple storageStates in a playwright context for efficient loading

I am trying to load multiple storageStates into a single context in playwright, but I am facing some issues. When using the following code: const context = await browser.newContext({ storageState: "telegram.json",storageState: "google. ...

What is the best jQuery library to add to my website?

I have included several jQuery scripts on my website for various functionalities such as sticky header, anchors, and animations. I am wondering if it is necessary to include all of them or if I can just include one or two? Here are the jQuery scripts I ha ...

Personalizing File Selection

What is the process for customizing file uploads? <%= f.file_field :image, class: 'inputfile' %> <label for="image">Choose an image</label> I am looking to replace "choose an image" with "choose a file" ...

Retrieving the value from a vuetify v-text-field to use in another text field

Looking to suggest a username based on the user's first and last name. The concept is that after the user fills out the first name and last name fields, the value in the username field will be a combination of their first and last names. firstName = B ...

What You See Is What You Get - A versatile tool for editing both text and

As a developer, I have encountered a common need that StackOverflow addresses. Situation I am in the process of creating a website where users can post code examples and articles using an admin system. These posts may be created by me or registered fron ...

What is the process of transferring information from a form to mongodb?

I have created a nodejs project with the following structure: https://i.stack.imgur.com/JiMmd.png api.js contains: const express = require('express'); const router = express.Router(); const add = require('../model/myModel'); router.g ...

On what occasion is a DOM element considered "prepared"?

Here's a question that might make you think twice: $(document).ready(function() { }); Sometimes, the simplest questions lead to interesting discussions. Imagine having a list of elements like this: <body> <p>Paragraph</p> < ...

npm is consolidating all dependencies and their sub-dependencies into a single unified directory

My project has a package.json file with approximately 20 dependencies. Upon running npm install, all the dependencies and sub-dependencies end up in the main node_modules directory, resulting in hundreds of modules rather than just my intended 20. The sub- ...

Guide on transmitting data from a child component to a parent object in Vue.js?

When I attempt to utilize a child component with custom form inputs and emit those values to the parent component, I encounter an issue where one input value disappears when another input value is entered. Let me showcase some code: Child Component <tem ...

Filter products by pressing the "Enter" key while using the search input in

I have a list of products and I want to only display products that have a description or name containing the typed word when enter is clicked on the search input. Here is what I tried in my Search component: const Search = (props) => { return ( &l ...

Organize the array following the guidelines of a card game with a versatile approach

deck = ['Jack', 8, 2, 6, 'King', 5, 3, 'Queen', "Jack", "Queen", "King"] <!- Desired Result = [2,3,5,6,8,'Jack','Queen','King'] Explore the challenge: Arrange the ...

Exploring Material UI choices and retrieving the selected option's value

Could someone help me with this issue? When I console.log target, the output is as follows: <li tabindex="-1" role="option" id="mui-21183-option-0" data-option-index="0" aria-disabled="false" aria-select ...

utilize the getStaticProps function within the specified component

I recently started a project using Next.js and TypeScript. I have a main component that is called in the index.js page, where I use the getStaticProps function. However, when I log the prop object returned by getStaticProps in my main component, it shows a ...

Clicking on the ajax tab control in asp.net displays a single rectangular box - how can this be removed?

When using tab control in Ajax, I encountered an issue where a blue rectangle box appeared when clicking or opening the page. How can I remove this unwanted box? ...

EJS variable not detected by Visual Studio IDE in JavaScript file

Working on a Node.js project with the express framework and utilizing EJS as the template engine, my IDE of choice is Visual Studio. Encountering an issue when using EJS variables within the same ejs file. Though it renders correctly and functions perfect ...

One the year is chosen, it will be automatically hidden and no longer available for selection

<div ng-repeat="localcost in vm.project.localCosts" layout="column"> <md-select name="localcost_{{$index}}"ng-model="localcost.year" flex> <md-option ng-repeat="years in vm.getYears()" ng-value="years">{{years}}< ...