Building AG Charts: Efficient JavaScript Charting with Tree-Based Scene Graphs
AG Charts is a high-performance, canvas-based JavaScript charting library, designed for creating complex and interactive JavaScript charts. Initially built to power Integrated Charts in AG Grid (our React Data Table library), it now stands alone with over 1 million npm downloads per month.
In this post, we’ll explore how AG Charts leverages a tree-based scene graph abstraction to efficiently render complex JavaScript charts, offering a powerful solution for developers building data visualizations in JavaScript, React, Angular and Vue.
Challenges in JavaScript Charting with HTML Canvas
Rendering graphics on the web can be a double-edged sword. While the HTML Canvas API provides a solid platform for drawing complex shapes and images, it comes with performance considerations, such as:
- Rendering Overhead: Canvas operations involve manipulating the pixels of a bitmap, which means that drawing, compositing, and transformations require direct interaction with the GPU or software rendering pipeline.
- Browser API Overhead: Each canvas API call incurs a certain amount of overhead due to interaction with the browser's rendering engine.
- Repainting and Redrawing: When you draw on a canvas, the entire pixel area being modified has to be recalculated and redrawn, especially for complex shapes or animations.
- Single-threaded Execution: JavaScript and canvas rendering both run on the main thread. Intensive canvas operations can block the main thread, leading to slower response times and reduced performance for other JavaScript tasks.
- Lack of Hardware Acceleration in Some Cases: While some canvas operations can be hardware accelerated, not all operations benefit from it. This inconsistency can lead to performance bottlenecks, especially with complex or frequent drawing operations.
Given this, we needed a solution that would allow us to work with the canvas as efficiently as possible.
Enter the scene graph.
What is a Scene Graph?
A tree-based scene graph is a hierarchical data structure used in computer graphics to represent the spatial and logical relationships between objects in a scene. Each node in the tree corresponds to an object or entity in the scene, and the parent-child relationships between nodes define transformations such as translation, rotation, and scaling relative to their parents.
In AG Charts, the scene graph is an abstraction layer over the HTML Canvas, providing a higher-level API for constructing and manipulating chart elements, such as markers, lines, circles and shapes. The scene graph also provides a layer where we can perform caching and optimise canvas rendering, negating some of the aforementioned performance considerations.
Why a Scene Graph is Essential for JavaScript Chart Libraries
- Higher-Order Abstraction for Rendering: The scene graph in AG Charts was designed to simplify the low-level drawing operations required by the Canvas API. Instead of working directly with pixels, chart renderings could work with higher-order constructs like markers, lines, circles, and shapes.
- Caching and Optimization: One key advantage of the scene graph is its ability to cache and optimize canvas rendering. Since canvas operations are typically slower than JavaScript operations, caching results and minimizing redraws are essential for maintaining performance.
- Support for Matrix Transformations: Initially, we added support for matrix transformations on the base node class of all scene graph nodes, which includes rotation, scaling and translation operations.
- Coordinate Conversion Utilities: We also implemented methods for converting points and boxes to and from canvas coordinates. These utilities were used liberally throughout our codebase to manage positioning and layout tasks.
How the Scene Graph Was Implemented in AG Charts
Our scene graph consists of nodes representing different chart elements, with a base node class that includes properties for positioning, styling, and rendering. Each node type—whether a line, shape, or marker—inherits from this base class and implements its own rendering logic.
The base node class also includes support for matrix transformations. Matrix transformations are mathematical operations used to manipulate the position, scale, and rotation of objects within a 2D space. These transformations are represented as matrices, which, when applied to points or objects, alter their geometric properties. The main types of transformations are:
- Translation: Shifts an object from one position to another.
- Scaling: Changes the size of an object either uniformly or non-uniformly.
- Rotation: Rotates an object around a specific point or axis.
In a scene graph, these transformations are applied hierarchically. Each node in the graph represents an object or a group of objects and holds its transformation matrix. The final transformation of a child node is the result of its own transformation combined with the transformations of all its parent nodes.
In addition to matrix transformations, we also added methods to convert to/from canvas coordinates for points and boxes, which were liberally used around the codebase.
Continuous Improvement of AG Charts JavaScript Charts
Whilst our initial implementation of the scene graph provided many benefits, as AG Charts continues to evolve we've noticed areas for further improvement, namely:
- Overhead from Matrix Transformations: Although matrix transformations were supported by default, they were unnecessary for most rendering tasks. In practice, 95% of our scene graph nodes do not require these transformations, leading to unnecessary computational overhead.
- Complexity in Coordinate Conversions: The frequent use of coordinate conversion methods added complexity to the codebase, making it harder to maintain and optimize.
- Performance Bottlenecks: As our library grew, we noticed that matrix transformations were consuming a non-trivial amount of execution time, even for nodes where these transformations were not needed.
Our Approach to Optimizing the Scene Graph
To address these challenges, we implemented a series of changes aimed at simplifying the scene graph and reducing performance overhead:
- Selective Application of Matrix Transformations: We removed all matrix support from the existing scene graph nodes and added mixin classes. This allowed us to selectively re-add matrix transforms to the specific parts of the codebase where they were still needed.
- Simplifying Coordinate Transformations: We refactored the base scene-graph nodes by replacing methods that returned canvas coordinates with helper functions using local coordinates, reducing costly matrix operations.
- Improving Hit-Testing Performance: We streamlined hit testing (determining whether a mouse or touch event intersects with a specific object drawn on the canvas) by converting to local coordinates upfront and applying transforms as we traverse the scene graph, avoiding the need for continuous coordinate conversions.
- Refactoring and Cleanup: As a result of these simplifications and improvements, we were able to generally refactor and clean up redundant operations throughout our codebase, reducing our overall tech debt, and thereby improving maintainability as well as performance.
Key Pull Requests and Changes
These optimizations were implemented across several pull requests. Given AG Charts is entirely open-source, you can dig into these PRs:
- PR #2234: Clean up the Scene Graph, pt.1
- PR #2242: Remove translation support from Node.ts
- PR #2245: Fix bbox caching & simplify coordinate translations
- PR #2269: Clean up the Scene Graph, pt.2
Results and Performance Gains
These changes have led to several positive outcomes:
- Reduced Overhead from Unnecessary Matrix Transforms: Matrix transformations are now only applied where needed, mostly in rendering leaf nodes of the scene graph, reducing unnecessary computational costs.
- Faster Hit-Testing and Rendering: Hit-testing operations are now cheaper and faster, thanks to optimized coordinate handling.
- Performance Improvements in Benchmarks: Internal benchmarking shows notable performance gains in large-scale use cases, demonstrating the effectiveness of these optimizations:
Conclusion
By refining our use of matrix transformations and optimizing the scene graph, we've significantly improved the performance of AG Charts. These changes are now part of the latest (10.2) release, and we're excited to see how they enhance your charting experience. Stay tuned for more updates as we continue to innovate and improve AG Charts!
Want to build JavaScript charts with AG Charts? Get Started today by visiting our documentation: