D3.js-based SVG time series charts done right to reach 60 FPS. Much much faster pan and zoom than other charts, whether canvas- or SVG-based.
Demo 1 reaches 60 FPS on desktops, recent iPhones and top Android phones. Demo 2 shows 60 FPS on desktops, about 24 FPS on iPhone and about 3 FPS on old and slow LG D90.
In comparison, dygraphs.org library basically never reaches 60 fps. Try to pan their demo at the home page by holding shift
. Note that the demos above use the same NY vs SF temperature dataset.
D3.js seem slow: stock D3 panning. But it turns out the SVG rasterization is not the bottleneck. Only 2 issues had to be fixed in that demo to reach 60 fps:
- avoid extra attribute setting on SVG lines of the grid during pan and zoom (partially already in HEAD of d3-axis)
- draw in
d3.timeout()
instead ofd3.zoom()
, that is, avoid drawing more often than screen refreshes
In the demos above, SVG DOM manipulations during grid updates seem to consume at least 20% of drawing time, so further optimization work is possible. Keep watching!
This monorepo is organized into several workspaces:
svg-time-series/
– core library for high-performance SVG time series charts. See svg-time-series/README.md for installation and usage details.segment-tree-rmq/
– generic segment tree implementation used for range queries. Its unit tests live insegment-tree-rmq/src
.samples/
– Vite-powered demos and sample code showcasing the library.
Run tests and benchmarks from the repository root:
npm test # run unit tests
npm run bench # run micro-benchmarks
- Install Node.js 20.x (npm and npx will also be installed)
npm ci
in the project root to install pinned dependenciescd samples; npx vite
to start the dev server- Open the URL in your browser. The Vite web server doesn't properly handle 404 errors, so if you see a blank page, the URL is likely incorrect.
- Navigate to
demo1.html
ordemo2.html
in the list of links in the browser.
- For a performance benchmark of core chart components, open
benchmarks/chart-components/index.html
after starting the dev server and observe the reported FPS and render time in your browser. - Run
npm run bench
to execute the Vitest micro-benchmark suite. Results are printed to the terminal, showing operations per second and relative performance for each test. - To run automated browser benchmarks with
Tachometer, execute
node scripts/run-browser-benchmarks.mjs
. The script measures first contentful paint and writes JSON results totachometer-results.json
. Inspect this file to compare performance across runs.
Charts can display any number of data series. Each series is assigned to a
Y-axis via the seriesAxes
array. Only two Y axes are supported: index 0
represents the right axis and index 1
represents the left axis. Series that
share an index use the same scale, computed from the combined minimum and
maximum of every series on that axis.
import { TimeSeriesChart, IDataSource } from "svg-time-series";
import { LegendController } from "./LegendController"; // implement your own
const source: IDataSource = {
startTime,
timeStep,
length: data.length,
// Assign the first two series to axis 0 and the third to axis 1.
seriesAxes: [0, 0, 1],
getSeries: (i, seriesIdx) => data[i][seriesIdx],
};
const chart = new TimeSeriesChart(
svg,
source,
(state, data) =>
new LegendController(legend, state, data, (ts) =>
new Date(ts).toISOString(),
),
onZoom,
onMouseMove,
);
getSeries
returns the value for the specified series index. seriesAxes
maps each series to a Y-axis, and its length determines how many series are
available from the data source.
The third argument creates a legend controller, letting you customize how legend entries are rendered, including timestamp formatting.
For a chart where all series share one Y-axis, assign each entry in
seriesAxes
to 0. A single-series chart uses seriesAxes: [0]
and renders one
path and axis.
You can change the allowed zoom range at runtime by calling
setScaleExtent
on the chart's interaction API:
// Allow zoom levels between 1x and 80x
chart.interaction.setScaleExtent([1, 80]);
- No legacy
- Very basic features
- Rasterizer-side coordinate transformations (No JS multiplication loops)
- A Range Minimum Query index for O(log(N)) autoscale (No JS minmax loops)
- No drawing or heavy CPU work in mouse handlers
- Don't change anything more often than once per screen refresh
- Only calculate and apply coordinate transformations in
requestAnimationFrame
- Take care of
requestAnimationFrame
not firing in background. Don't redraw when in background to save battery.
Description | Model | Browser | FPS | Resolution | CPU | GPU |
---|---|---|---|---|---|---|
SegmentTree reindexing | Desktop | Chrome | 94ms for 1000 repeats | i5-4670 | NVIDIA GeForce GTX 660 | |
SegmentTree reindexing | Desktop | Firefox | 861ms for 1000 repeats | i5-4670 | NVIDIA GeForce GTX 660 | |
SegmentTree reindexing | Desktop | Edge | 1255ms for 1000 repeats | i5-4670 | NVIDIA GeForce GTX 660 | |
Path drawing and transformation | Desktop | Chrome | 60 | 1680×917 | i5-4670 | NVIDIA GeForce GTX 660 |
Path drawing and transformation | Desktop | Firefox | 30 | 1680×917 | i5-4670 | NVIDIA GeForce GTX 660 |
Path drawing and transformation | Desktop | Edge | 60 | 1680×917 | i5-4670 | NVIDIA GeForce GTX 660 |
Grid drawing and transformation | Desktop | Chrome | 59.7 | 1680×917 | i5-4670 | NVIDIA GeForce GTX 660 |
Grid drawing and transformation | Desktop | Firefox | 47 | 1680×917 | i5-4670 | NVIDIA GeForce GTX 660 |
Grid drawing and transformation | Desktop | Edge | 59.7 | 1680×917 | i5-4670 | NVIDIA GeForce GTX 660 |
Demo2 without grid | Desktop | Chrome | 59 | 1680×917 | i5-4670 | NVIDIA GeForce GTX 660 |
Demo2 without grid | Desktop | Firefox | 30 | 1680×917 | i5-4670 | NVIDIA GeForce GTX 660 |
Demo2 without grid | Desktop | Edge | 59 | 1680×917 | i5-4670 | NVIDIA GeForce GTX 660 |
SVG path recreation | Desktop | Chrome | 52 | 1680×917 | i5-4670 | NVIDIA GeForce GTX 660 |
SVG path recreation | Desktop | Firefox | 22.7 | 1680×917 | i5-4670 | NVIDIA GeForce GTX 660 |
SVG path recreation | Desktop | Edge | 20.3 | 1680×917 | i5-4670 | NVIDIA GeForce GTX 660 |
SegmentTree Queries | Desktop | Chrome | 59.7 | 1680×917 | i5-4670 | NVIDIA GeForce GTX 660 |
SegmentTree Queries | Desktop | Firefox | 27.3 | 1680×917 | i5-4670 | NVIDIA GeForce GTX 660 |
SegmentTree Queries | Desktop | Edge | 60 | 1680×917 | i5-4670 | NVIDIA GeForce GTX 660 |
Stock axes panning example and:
- No drawing or heavy CPU work in mouse handlers
- Don't change anything more often than once per screen refresh
d3-axes grad and performance improvements of axis not accepted by the upstream
Stock d3 phyllotaxis pan-zoom example plus:
- No drawing or heavy CPU work in mouse handlers
- Don't change anything more often than once per screen refresh
This project is licensed under the MIT License.