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!
- 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.
Charts can display one or two data series. By default, all series share a single
Y-axis whose scale is computed from the combined minimum and maximum of every
series. To draw series with different units, pass true
for the dualYAxis
parameter of TimeSeriesChart
, which enables independent left and right Y
scales.
import { TimeSeriesChart, IMinMax } from "svg-time-series";
function buildSegmentTreeTupleNy(
index: number,
elements: ReadonlyArray<[number, number]>,
): IMinMax {
const ny = elements[index][0];
return { min: ny, max: ny };
}
function buildSegmentTreeTupleSf(
index: number,
elements: ReadonlyArray<[number, number]>,
): IMinMax {
const sf = elements[index][1];
return { min: sf, max: sf };
}
const chart = new TimeSeriesChart(
svg,
legend,
startTime,
timeStep,
data,
buildSegmentTreeTupleNy,
buildSegmentTreeTupleSf,
true, // enable dual Y axes
onZoom,
onMouseMove,
(ts) => new Date(ts).toISOString(),
);
The last argument, formatTime
, is optional and lets you customize how
timestamps are displayed in the legend. If omitted, timestamps are formatted
with toLocaleString
.
For two series sharing a single Y-axis, pass false
for dualYAxis
:
const chartSingle = new TimeSeriesChart(
svg,
legend,
startTime,
timeStep,
data,
buildSegmentTreeTupleNy,
buildSegmentTreeTupleSf,
false, // series share one axis
onZoom,
onMouseMove,
(ts) => new Date(ts).toISOString(),
);
If you only have one series, supply data with a single value per point and omit the second builder; the chart will render a single path and axis.
- 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