High Performance SVG Cursor & Rollover

Demonstrates the Decoupled Render Loop. Even with 1 Million Points and a complex Gradient Palette causing frame drops on the main chart, the SVG Cursor remains smooth at 60 FPS because it does not trigger a WebGL redraw.

Fullscreen

Edit

 Edit

Docs

drawExample.ts

index.html

vanilla.ts

theme.ts

Copy to clipboard
Minimise
Fullscreen
1import {
2    SciChartSurface,
3    NumericAxis,
4    XyDataSeries,
5    XyScatterRenderableSeries,
6    EllipsePointMarker,
7    CursorModifier,
8    ZoomExtentsModifier,
9    MouseWheelZoomModifier,
10    DefaultPaletteProvider,
11    IPointMarkerPaletteProvider,
12    parseColorToUIntArgb,
13    RolloverModifier,
14    ENumericFormat,
15    NumberRange,
16    ChartModifierBase2D,
17    XySeriesInfo,
18    EXyDirection,
19} from "scichart";
20import { appTheme } from "../../../theme";
21
22// Helper to extract RGB channels from a UInt ARGB color
23const extractColorChannels = (uintColor: number) => {
24    return {
25        a: (uintColor >> 24) & 0xff,
26        r: (uintColor >> 16) & 0xff,
27        g: (uintColor >> 8) & 0xff,
28        b: uintColor & 0xff,
29    };
30};
31
32class GradientPaletteProvider extends DefaultPaletteProvider implements IPointMarkerPaletteProvider {
33    private readonly minY: number;
34    private readonly maxY: number;
35
36    // Color Channel Data
37    private startColor: { r: number; g: number; b: number; a: number };
38    private endColor: { r: number; g: number; b: number; a: number };
39
40    constructor(minY: number, maxY: number, startHex: string, endHex: string) {
41        super();
42        this.minY = minY;
43        this.maxY = maxY;
44
45        // Pre-calculate color channels to avoid expensive parsing per point
46        this.startColor = extractColorChannels(parseColorToUIntArgb(startHex));
47        this.endColor = extractColorChannels(parseColorToUIntArgb(endHex));
48    }
49
50    public override overridePointMarkerArgb(xValue: number, yValue: number, index: number) {
51        // Calculate the fraction (0 to 1) of the current Y value within the range
52        let fraction = (yValue - this.minY) / (this.maxY - this.minY);
53        if (fraction < 0) fraction = 0;
54        if (fraction > 1) fraction = 1;
55
56        // Lerp
57        const r = Math.floor(this.startColor.r + (this.endColor.r - this.startColor.r) * fraction);
58        const g = Math.floor(this.startColor.g + (this.endColor.g - this.startColor.g) * fraction);
59        const b = Math.floor(this.startColor.b + (this.endColor.b - this.startColor.b) * fraction);
60
61        // Reassemble into UInt ARGB
62        const colorUint = (255 << 24) | (r << 16) | (g << 8) | b;
63
64        return {
65            stroke: colorUint,
66            fill: colorUint,
67        };
68    }
69}
70
71export const drawExample = async (rootElement: string | HTMLDivElement) => {
72    const { wasmContext, sciChartSurface } = await SciChartSurface.create(rootElement, {
73        theme: appTheme.SciChartJsTheme,
74    });
75
76    sciChartSurface.xAxes.add(
77        new NumericAxis(wasmContext, {
78            labelFormat: ENumericFormat.Engineering,
79        })
80    );
81    sciChartSurface.yAxes.add(
82        new NumericAxis(wasmContext, {
83            growBy: new NumberRange(0.01, 0.01),
84        })
85    );
86
87    // 1. Data Generation (Bounded Cloud)
88    const count = 1_000_000;
89    const xValues = new Float64Array(count);
90    const yValues = new Float64Array(count);
91
92    const Y_MIN = -500;
93    const Y_MAX = 500;
94
95    for (let i = 0; i < count; i++) {
96        xValues[i] = i;
97        yValues[i] = Y_MIN + Math.random() * (Y_MAX - Y_MIN);
98    }
99
100    // 2. Create Scatter Series with Gradient Palette
101    const scatterSeries = new XyScatterRenderableSeries(wasmContext, {
102        dataSeries: new XyDataSeries(wasmContext, {
103            xValues,
104            yValues,
105            containsNaN: false,
106            isSorted: true,
107            dataSeriesName: "Bounded Cloud Series",
108        }),
109        pointMarker: new EllipsePointMarker(wasmContext, {
110            width: 1,
111            height: 1,
112            strokeThickness: 0.5,
113        }),
114        paletteProvider: new GradientPaletteProvider(
115            Y_MIN,
116            Y_MAX,
117            appTheme.Indigo, // Start (Low Y)
118            appTheme.VividOrange // End (High Y)
119        ),
120    });
121    sciChartSurface.renderableSeries.add(scatterSeries);
122
123    sciChartSurface.chartModifiers.add(
124        new ZoomExtentsModifier(),
125        new MouseWheelZoomModifier({
126            xyDirection: EXyDirection.XDirection,
127        })
128    );
129
130    // 3. State Management for Tool Modifiers
131    let activeModifier: ChartModifierBase2D | null = null;
132    let isCursorMode = true;
133    let isSvgMode = true;
134
135    const rebuildActiveModifier = () => {
136        if (activeModifier) {
137            sciChartSurface.chartModifiers.remove(activeModifier);
138            activeModifier.delete();
139            activeModifier = null;
140        }
141
142        const accentColor = "#5e7dba";
143
144        if (isCursorMode) {
145            activeModifier = new CursorModifier({
146                isSvgOnly: isSvgMode,
147                showTooltip: true,
148                crosshairStroke: accentColor,
149                axisLabelFill: accentColor,
150                tooltipContainerBackground: accentColor,
151            });
152        } else {
153            activeModifier = new RolloverModifier({
154                isSvgOnly: isSvgMode,
155                showTooltip: true,
156                showAxisLabel: true,
157                rolloverLineStroke: accentColor,
158                tooltipDataTemplate: (seriesInfo: XySeriesInfo): string[] => {
159                    const valuesWithLabels: string[] = [];
160                    const xySeriesInfo = seriesInfo as XySeriesInfo;
161                    valuesWithLabels.push(`X: ${xySeriesInfo.formattedXValue}`);
162                    valuesWithLabels.push(`Y: ${xySeriesInfo.formattedYValue}`);
163                    return valuesWithLabels;
164                },
165            });
166            (activeModifier as RolloverModifier).rolloverLineAnnotation.axisLabelFill = accentColor;
167        }
168
169        sciChartSurface.chartModifiers.add(activeModifier);
170    };
171    rebuildActiveModifier();
172
173    // 4. Control Functions
174    const setSvgMode = (useSvg: boolean) => {
175        isSvgMode = useSvg;
176        rebuildActiveModifier();
177    };
178    const toggleUseCursorOrRollover = (useCursor: boolean) => {
179        isCursorMode = useCursor;
180        rebuildActiveModifier();
181    };
182
183    return {
184        sciChartSurface,
185        controls: {
186            setSvgMode,
187            toggleUseCursorOrRollover,
188        },
189    };
190};
191

High Performance SVG Cursor - JavaScript

Overview

When rendering massive datasets (like the 1 Million Point Scatter chart in this demo), the WebGL rendering engine may drop below 60 FPS due to the sheer volume of data and pixel shading required. We have made it possible to use paletteProvider with much bigger datasets while retaining performance. And even if you push this to its limit, you can still get smooth tooltip behaviour thanks to SVG cursor.

In standard charting libraries, the Cursor or Tooltip is often part of the main render loop. This means if the chart draws at 15 FPS, your mouse cursor lags at 15 FPS.

SciChart.js solves this with a Decoupled Render Loop.

The Solution: SVG vs Native

This demo allows you to toggle between isSvgOnly: true (SVG Mode) and isSvgOnly: false (Native Mode) on the Cursor and Rollover Modifiers.

  • Native Mode: The crosshairs and tooltips are drawn on the main canvas (or composited). Every mouse move triggers a full chart re-render. If the data is heavy, the cursor feels "heavy" and laggy.
  • SVG Mode: The modifiers draw to a separate HTML/SVG layer floating above the WebGL canvas. Moving the mouse updates the SVG layer without invalidating the WebGL scene.

Result: The chart might be static or updating slowly, but your interaction remains buttery smooth at 60 FPS.

Technical Implementation

The key property is isSvgOnly on the modifier options:

new CursorModifier({
    isSvgOnly: true, // Independent render loop
    showTooltip: true,
    // ...
});

We also implement a custom GradientPaletteProvider which calculates color per-point based on Y-Value. This simulates a "heavy" rendering load to better illustrate the performance benefits of the SVG cursor.

javascript Chart Examples & Demos

See Also: Performance Demos & Showcases (12 Demos)

Realtime JavaScript Chart Performance Demo | SciChart.js

Realtime JavaScript Chart Performance Demo

This demo showcases the incredible realtime performance of our JavaScript charts by updating the series with millions of data-points!

Load 500 Series x 500 Points Performance Demo | SciChart

Load 500 Series x 500 Points Performance Demo

This demo showcases the incredible performance of our JavaScript Chart by loading 500 series with 500 points (250k points) instantly!

Load 1 Million Points Performance Demo | SciChart.js Demo

Load 1 Million Points Performance Demo

This demo showcases the incredible performance of our JavaScript Chart by loading a million points instantly.

Realtime Ghosted Traces | Javascript Charts | SciChart.js Demo

Realtime Ghosted Traces

This demo showcases the realtime performance of our JavaScript Chart by animating several series with thousands of data-points at 60 FPS

Realtime Audio Spectrum Analyzer Chart | SciChart.js Demo

Realtime Audio Spectrum Analyzer Chart Example

See the frequency of recordings with the JavaScript audio spectrum analyzer example from SciChart. This real-time visualizer demo uses a Fourier Transform.

Oil & Gas Explorer JavaScript Dashboard | SciChart.js

Oil & Gas Explorer JavaScript Dashboard

Demonstrates how to create Oil and Gas Dashboard

Client/Server Websocket Data Streaming | SciChart.js Demo

Client/Server Websocket Data Streaming

This demo showcases the incredible realtime performance of our JavaScript charts by updating the series with millions of data-points!

Server Traffic Dashboard | Javascript Charts | SciChart.js Demo

Server Traffic Dashboard

This dashboard demo showcases the incredible realtime performance of our JavaScript charts by updating the series with millions of data-points!

Rich Interactions Showcase | Javascript Charts | SciChart.js

Rich Interactions Showcase

This demo showcases the incredible realtime performance of our JavaScript charts by updating the series with millions of data-points!

Dynamic Layout Showcase | Javascript Charts | SciChart.js Demo

Dynamic Layout Showcase

Demonstrates a custom modifier which can convert from single chart to grid layout and back.

Dragabble Event Markers | Javascript Charts | SciChart.js Demo

Dragabble Event Markers

Demonstrates how to repurpose a Candlestick Series into dragabble, labled, event markers

JavaScript Population Pyramid | Javascript Charts | SciChart.js

JavaScript Population Pyramid

Population Pyramid of Europe and Africa

SciChart Ltd, 16 Beaufort Court, Admirals Way, Docklands, London, E14 9XL.