Using Custom Tooltips

Demonstrates how to customise the tooltips for Rollover, Cursor and VerticalSlice modifiers using SciChart.js, High Performance JavaScript Charts. This also shows how to store and use data from these modifiers in React state.

Fullscreen

Edit

 Edit

Docs

drawExample.ts

index.tsx

ExampleDataProvider.ts

theme.ts

selection.ts

Copy to clipboard
Minimise
Fullscreen
1import { appTheme } from "../../../theme";
2import { ExampleDataProvider } from "../../../ExampleData/ExampleDataProvider";
3import {
4    NumericAxis,
5    NumberRange,
6    SciChartSurface,
7    XyDataSeries,
8    ENumericFormat,
9    FastLineRenderableSeries,
10    EllipsePointMarker,
11    CursorModifier,
12    ZoomPanModifier,
13    ZoomExtentsModifier,
14    MouseWheelZoomModifier,
15    SeriesInfo,
16    CursorTooltipSvgAnnotation,
17    adjustTooltipPosition,
18    RolloverModifier,
19    RolloverTooltipSvgAnnotation,
20    VerticalSliceModifier,
21    ECoordinateMode,
22    ModifierMouseArgs,
23    EChart2DModifierType,
24    ChartModifierBase2D,
25    testIsInBounds,
26} from "scichart";
27
28import { selectAll } from "./selection";
29
30function interpolateColor(
31    minValue: number,
32    maxValue: number,
33    minColor: string,
34    maxColor: string,
35    selectedValue: number
36) {
37    // Clamp the selected value to the min-max range
38    const clampedValue = Math.max(minValue, Math.min(maxValue, selectedValue));
39
40    // Calculate the ratio (0 to 1) of where the selected value falls in the range
41    const ratio = (clampedValue - minValue) / (maxValue - minValue);
42
43    // Helper function to parse hex color to RGB
44    function hexToRgb(hex: string) {
45        const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
46        return result
47            ? {
48                  r: parseInt(result[1], 16),
49                  g: parseInt(result[2], 16),
50                  b: parseInt(result[3], 16),
51              }
52            : null;
53    }
54
55    // Helper function to convert RGB to hex
56    function rgbToHex(r: number, g: number, b: number) {
57        return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
58    }
59
60    // Parse the min and max colors
61    const minRgb = hexToRgb(minColor);
62    const maxRgb = hexToRgb(maxColor);
63
64    if (!minRgb || !maxRgb) {
65        throw new Error("Invalid color format. Please use hex colors like #FF0000");
66    }
67
68    // Interpolate each RGB component
69    const r = Math.round(minRgb.r + (maxRgb.r - minRgb.r) * ratio);
70    const g = Math.round(minRgb.g + (maxRgb.g - minRgb.g) * ratio);
71    const b = Math.round(minRgb.b + (maxRgb.b - minRgb.b) * ratio);
72
73    // Return the interpolated color as hex
74    return rgbToHex(r, g, b);
75}
76
77export const drawExample = async (rootElement: string | HTMLDivElement) => {
78    let setStuff: undefined | React.Dispatch<any>;
79    let setClickStuff: undefined | React.Dispatch<any>;
80
81    // Create a SciChartSurface with X,Y Axis
82    const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
83        theme: appTheme.SciChartJsTheme,
84    });
85
86    const xAxis = sciChartSurface.xAxes.add(
87        new NumericAxis(wasmContext, {
88            growBy: new NumberRange(0.05, 0.05),
89            labelFormat: ENumericFormat.Decimal,
90            labelPrecision: 4,
91        })
92    );
93
94    const yAxis = sciChartSurface.yAxes.add(
95        new NumericAxis(wasmContext, {
96            growBy: new NumberRange(0.1, 0.1),
97            labelFormat: ENumericFormat.Decimal,
98            labelPrecision: 4,
99        })
100    );
101
102    const pointMarker1 = new EllipsePointMarker(wasmContext, {
103        width: 10,
104        height: 10,
105        fill: appTheme.VividSkyBlue,
106        strokeThickness: 2,
107        stroke: appTheme.DarkIndigo,
108    });
109
110    const pointMarker2 = new EllipsePointMarker(wasmContext, {
111        width: 10,
112        height: 10,
113        fill: appTheme.VividOrange,
114        strokeThickness: 2,
115        stroke: appTheme.DarkIndigo,
116    });
117
118    const pointMarker3 = new EllipsePointMarker(wasmContext, {
119        width: 10,
120        height: 10,
121        fill: appTheme.MutedPink,
122        strokeThickness: 2,
123        stroke: appTheme.DarkIndigo,
124    });
125
126    // Add some data
127    const data1 = ExampleDataProvider.getFourierSeriesZoomed(0.6, 0.13, 5.0, 5.15);
128    const data2 = ExampleDataProvider.getFourierSeriesZoomed(0.5, 0.5, 5.0, 5.15);
129    const data3 = ExampleDataProvider.getFourierSeriesZoomed(0.4, 1, 5.0, 5.15);
130
131    const maxData1 = Math.max(...data1.yValues);
132    const maxData2 = Math.max(...data2.yValues);
133    const maxData3 = Math.max(...data3.yValues);
134
135    const minData1 = Math.min(...data1.yValues);
136    const minData2 = Math.min(...data2.yValues);
137    const minData3 = Math.min(...data3.yValues);
138
139    const dataSeries1 = new XyDataSeries(wasmContext, {
140        xValues: data1.xValues,
141        yValues: data1.yValues,
142        dataSeriesName: "First Line Series",
143    });
144
145    const dataSeries2 = new XyDataSeries(wasmContext, {
146        xValues: data2.xValues,
147        yValues: data2.yValues,
148        dataSeriesName: "Second Line Series",
149    });
150
151    const dataSeries3 = new XyDataSeries(wasmContext, {
152        xValues: data3.xValues,
153        yValues: data3.yValues,
154        dataSeriesName: "Third Line Series",
155    });
156
157    const lineSeries1 = new FastLineRenderableSeries(wasmContext, {
158        dataSeries: dataSeries1,
159        strokeThickness: 3,
160        stroke: appTheme.VividSkyBlue,
161        pointMarker: pointMarker1,
162    });
163
164    const lineSeries2 = new FastLineRenderableSeries(wasmContext, {
165        dataSeries: dataSeries2,
166        strokeThickness: 3,
167        stroke: appTheme.VividOrange,
168        pointMarker: pointMarker2,
169    });
170
171    const lineSeries3 = new FastLineRenderableSeries(wasmContext, {
172        dataSeries: dataSeries3,
173        strokeThickness: 3,
174        stroke: appTheme.MutedPink,
175        pointMarker: pointMarker3,
176    });
177
178    sciChartSurface.renderableSeries.add(lineSeries1, lineSeries2, lineSeries3);
179
180    let sInfos: any;
181
182    const cursorSvgTemplate = (seriesInfos: SeriesInfo[], svgAnnotation: CursorTooltipSvgAnnotation) => {
183        const width = 160;
184        const height = 140;
185
186        if (!seriesInfos.length) {
187            return `<svg/>`;
188        }
189
190        // sInfos = seriesInfos;
191
192        if (setStuff) {
193            setStuff(seriesInfos);
194        }
195
196        let svgArray = seriesInfos.map((si, i) => {
197            let min;
198            let max;
199
200            if (i === 0) {
201                min = minData1;
202                max = maxData1;
203            } else if (i === 1) {
204                min = minData2;
205                max = maxData2;
206            } else {
207                min = minData3;
208                max = maxData3;
209            }
210
211            // Text inside tooltip changes color based on Y value compared to min and max for series
212            return `
213                <g transform="translate(0,${i * 40})">
214                    <rect  x="0" y="0" rx="${5}" ry="${5}" width="${width}" height="${40}" fill="${"#EDEFF1"}" stroke="white" stroke-width="2"/>
215                    <rect  y="0" rx="${5}" ry="${5}" width="${width}" height="${20}" fill="${
216                si.stroke
217            }" stroke="white" stroke-width="2"/>
218          
219                    <text y="12" font-family="Verdana" font-size="12" fill="${"white"}" text-anchor="middle" >
220                        <tspan fill="${"white"}"  x="50%" font-size="14" dy="0.2em">${si.seriesName}</tspan>
221                        <tspan fill="${interpolateColor(
222                            min,
223                            max,
224                            "#b2bec3",
225                            "#d63031",
226                            si.yValue
227                        )}"  x="50%"  dy="1.5em" font-weight="bold">y: ${si.yValue.toFixed(2)}</tspan>
228                    </text>
229                </g>`;
230        });
231
232        // this calculates and sets svgAnnotation.xCoordShift and svgAnnotation.yCoordShift. Do not set x1 or y1 at this point.
233        adjustTooltipPosition(width, height, svgAnnotation);
234
235        return `
236            <svg width="${width}" height="${height}">
237                <rect y="0" rx="${5}" ry="${5}" width="${width}" height="${140}" fill="${"white"}" stroke="white" stroke-width="2"/>
238                <text x="50%" dy="1.3em" font-family="Verdana" font-size="12" fill="${"gray"}" text-anchor="middle" >x: ${seriesInfos[0].xValue.toFixed(
239            2
240        )}, index: ${seriesInfos[0].dataSeriesIndex}</text>
241                <g transform="translate(0,20)">${svgArray}</g>
242                
243            </svg>`;
244    };
245
246    let addedSlices: VerticalSliceModifier[] = [];
247
248    const verticalSliceTooltipTemplate = (
249        id: string,
250        seriesInfo: SeriesInfo,
251        rolloverTooltip: RolloverTooltipSvgAnnotation
252    ) => {
253        const width = 160;
254        const height = 75;
255        rolloverTooltip.updateSize(width, height);
256
257        let hasBeenRemoved = false; // Add this flag
258
259        const controller = new AbortController();
260
261        const removeFunction = (e: Event) => {
262            if (hasBeenRemoved) return; // Exit early if already executed
263            hasBeenRemoved = true; // Set flag to prevent re-execution
264
265            removeNearestVerticalSlice(seriesInfo.xValue);
266
267            // Abort all listeners controlled by this controller
268            controller.abort();
269
270            // Remove the event listener after execution
271            (e.target as Element).removeEventListener("click", removeFunction);
272        };
273
274        const removeNearestVerticalSlice = (xValue: number) => {
275            let nearestSlice: VerticalSliceModifier | null = null;
276            let minDistance = Infinity;
277
278            // Find the nearest vertical slice
279            addedSlices.forEach((slice) => {
280                const distance = Math.abs(slice.x1 - xValue);
281                if (distance < minDistance) {
282                    minDistance = distance;
283                    nearestSlice = slice;
284                }
285            });
286
287            // Remove the nearest slice if found
288            if (nearestSlice) {
289                sciChartSurface.chartModifiers.remove(nearestSlice);
290                addedSlices = addedSlices.filter((slice) => slice !== nearestSlice);
291            }
292        };
293
294        let random = (Math.random() * 10000).toString().split(".")[1];
295
296        let dynamicId = `remove-button-${random}`;
297
298        // Use setTimeout to ensure the DOM element exists before attaching the event listener
299        setTimeout(() => {
300            selectAll(`.${dynamicId}`).on("click", function (event, d) {
301                removeFunction(event);
302            });
303        }, 0);
304
305        let xButton = `
306        <g transform="translate(140, 1)" style="pointer-events: all; cursor: pointer;" >
307            <rect width="18" height="18" fill="#ffffff00" stroke="#ffffff" stroke-width="1.5" rx="3"/>
308            <line x1="3" y1="16" x2="16" y2="3" stroke="#ffffff" stroke-width="1" style="pointer-events: none"/>
309            <line x1="16" y1="16" x2="3" y2="3" stroke="#ffffff" stroke-width="1" style="pointer-events: none"/>
310        </g>`;
311
312        return `
313        <svg width="${width}" height="${height}" class="${dynamicId}">
314                <rect x="0" y="0" rx="${5}" ry="${5}" width="${width}" height="${height}" fill="${"#EDEFF1"}" stroke="white" stroke-width="2"/>
315                <rect x="0" y="0" rx="${5}" ry="${5}" width="${width}" height="${21}" fill="${
316            seriesInfo.stroke
317        }" stroke="white" stroke-width="2" />
318                <rect x="0" y="40" rx="${0}" ry="${0}" width="${width}" height="${16}" fill="${"white"}" stroke="white" stroke-width="2"/>
319           
320                ${xButton}
321
322                <text y="12" font-family="Verdana" font-size="12" fill="${"white"}" text-anchor="start" >
323                    <tspan fill="${"white"}" x="2%"  font-size="14" dy="0.2em">${seriesInfo.seriesName}</tspan>
324                    <tspan fill="${"gray"}"  x="20%" dy="1.7em">x: ${seriesInfo.formattedXValue}</tspan>
325                    <tspan fill="${"gray"}"  x="20%"  dy="1.4em">y: ${seriesInfo.formattedYValue}</tspan>
326                    <tspan fill="${"gray"}"  x="20%"  dy="1.5em">index: ${seriesInfo.dataSeriesIndex}</tspan>
327                </text>
328            </svg>`;
329    };
330
331    const rolloverTooltipTemplate = (
332        id: string,
333        seriesInfo: SeriesInfo,
334        rolloverTooltip: RolloverTooltipSvgAnnotation
335    ) => {
336        const width = 160;
337        const height = 75;
338        rolloverTooltip.updateSize(width, height);
339
340        if (setStuff) {
341            setStuff(seriesInfo);
342        }
343
344        return `
345        <svg width="${width}" height="${height}">
346                <rect x="0" y="0" rx="${5}" ry="${5}" width="${width}" height="${height}" fill="${"#EDEFF1"}" stroke="white" stroke-width="2"/>
347                <rect x="0" y="0" rx="${5}" ry="${5}" width="${width}" height="${20}" fill="${
348            seriesInfo.stroke
349        }" stroke="white" stroke-width="2" />
350                <rect x="0" y="40" rx="${0}" ry="${0}" width="${width}" height="${16}" fill="${"white"}" stroke="white" stroke-width="2"/>
351                <text y="12" font-family="Verdana" font-size="12" fill="${"white"}" text-anchor="middle" >
352                    <tspan fill="${"white"}"  x="50%" font-size="14" dy="0.2em">${seriesInfo.seriesName}</tspan>
353                    <tspan fill="${"gray"}"  x="50%" dy="1.7em">x: ${seriesInfo.formattedXValue}</tspan>
354                    <tspan fill="${"gray"}"  x="50%"  dy="1.4em">y: ${seriesInfo.formattedYValue}</tspan>
355                    <tspan fill="${"gray"}"  x="50%"  dy="1.5em">index: ${seriesInfo.dataSeriesIndex}</tspan>
356                </text>
357            </svg>`;
358    };
359
360    const setData = () => {
361        dataSeries1.clear();
362        dataSeries2.clear();
363        dataSeries3.clear();
364        dataSeries1.appendRange(data1.xValues, data1.yValues);
365        dataSeries2.appendRange(data2.xValues, data2.yValues);
366        dataSeries3.appendRange(data3.xValues, data3.yValues);
367    };
368
369    // add VerticalSliceModifier on click
370    class ClickVerticalSliceModifier extends ChartModifierBase2D {
371        readonly type: EChart2DModifierType = EChart2DModifierType.Custom;
372        private sliceCounter = 0;
373
374        override modifierMouseDown(args: ModifierMouseArgs) {
375            super.modifierMouseDown(args);
376
377            const mousePoint = args.mousePoint;
378            const { left, right, top, bottom } = this.parentSurface?.seriesViewRect;
379
380            if (testIsInBounds(mousePoint.x, mousePoint.y, left, bottom, right, top)) {
381                const xCoordinate = this.parentSurface?.xAxes
382                    .get(0)
383                    .getCurrentCoordinateCalculator()
384                    .getDataValue(mousePoint.x);
385
386                // Create different colored slices
387                const colors = ["#FF6600", "#50C7E0", "#32CD32", "#FF69B4"];
388                const color = colors[this.sliceCounter % colors.length];
389
390                const verticalSlice = new VerticalSliceModifier({
391                    x1: xCoordinate,
392                    xCoordinateMode: ECoordinateMode.DataValue,
393                    isDraggable: true,
394                    showRolloverLine: true,
395                    rolloverLineStrokeThickness: 2,
396                    rolloverLineStroke: color,
397                    lineSelectionColor: color,
398                    showTooltip: true,
399                });
400
401                this.parentSurface?.chartModifiers.add(verticalSlice);
402                addedSlices.push(verticalSlice);
403                this.sliceCounter++;
404                verticalSlice.modifierMouseMove(args);
405            }
406        }
407    }
408
409    const setType = (type: string) => {
410        if (type === "cursor") {
411            setData();
412
413            sciChartSurface.chartModifiers.clear(true);
414
415            sciChartSurface.chartModifiers.add(
416                // Add the CursorModifier (crosshairs) behaviour
417                new CursorModifier({
418                    showTooltip: true,
419                    tooltipSvgTemplate: cursorSvgTemplate,
420                }),
421                // Add further zooming and panning behaviours
422                new ZoomPanModifier({ enableZoom: true }),
423                new ZoomExtentsModifier(),
424                new MouseWheelZoomModifier()
425            );
426        } else if (type === "rollover") {
427            setData();
428
429            sciChartSurface.chartModifiers.clear(true);
430
431            lineSeries1.rolloverModifierProps.tooltipTemplate = (
432                id: string,
433                seriesInfo: SeriesInfo,
434                rolloverTooltip: RolloverTooltipSvgAnnotation
435            ) => {
436                return rolloverTooltipTemplate(id, seriesInfo, rolloverTooltip);
437            };
438
439            lineSeries2.rolloverModifierProps.tooltipTemplate = (
440                id: string,
441                seriesInfo: SeriesInfo,
442                rolloverTooltip: RolloverTooltipSvgAnnotation
443            ) => {
444                return rolloverTooltipTemplate(id, seriesInfo, rolloverTooltip);
445            };
446
447            lineSeries3.rolloverModifierProps.tooltipTemplate = (
448                id: string,
449                seriesInfo: SeriesInfo,
450                rolloverTooltip: RolloverTooltipSvgAnnotation
451            ) => {
452                return rolloverTooltipTemplate(id, seriesInfo, rolloverTooltip);
453            };
454            sciChartSurface.chartModifiers.add(
455                new RolloverModifier(),
456                new ZoomPanModifier({ enableZoom: true }),
457                new ZoomExtentsModifier(),
458                new MouseWheelZoomModifier()
459            );
460        } else if (type === "verticalSlice") {
461            setData();
462
463            sciChartSurface.chartModifiers.clear(true);
464
465            lineSeries1.rolloverModifierProps.tooltipTemplate = (
466                id: string,
467                seriesInfo: SeriesInfo,
468                rolloverTooltip: RolloverTooltipSvgAnnotation
469            ) => {
470                return verticalSliceTooltipTemplate(id, seriesInfo, rolloverTooltip);
471            };
472
473            lineSeries2.rolloverModifierProps.tooltipTemplate = (
474                id: string,
475                seriesInfo: SeriesInfo,
476                rolloverTooltip: RolloverTooltipSvgAnnotation
477            ) => {
478                return verticalSliceTooltipTemplate(id, seriesInfo, rolloverTooltip);
479            };
480
481            lineSeries3.rolloverModifierProps.tooltipTemplate = (
482                id: string,
483                seriesInfo: SeriesInfo,
484                rolloverTooltip: RolloverTooltipSvgAnnotation
485            ) => {
486                return verticalSliceTooltipTemplate(id, seriesInfo, rolloverTooltip);
487            };
488
489            class SimpleChartModifier extends ChartModifierBase2D {
490                readonly type = EChart2DModifierType.Custom;
491
492                modifierMouseDown(args: ModifierMouseArgs) {
493                    super.modifierMouseDown(args);
494                    if (setStuff) {
495                        setStuff(`MouseDown at point ${args.mousePoint.x.toFixed(2)}, ${args.mousePoint.y.toFixed(2)}`);
496                    }
497                }
498
499                modifierDoubleClick(args: ModifierMouseArgs) {
500                    super.modifierDoubleClick(args);
501                    if (setStuff) {
502                        setStuff(
503                            `DoubleClick at point  ${args.mousePoint.x.toFixed(2)}, ${args.mousePoint.y.toFixed(2)}`
504                        );
505                    }
506                }
507            }
508
509            sciChartSurface.chartModifiers.add(new SimpleChartModifier());
510
511            sciChartSurface.chartModifiers.add(new ClickVerticalSliceModifier());
512            sciChartSurface.chartModifiers.add(new ZoomPanModifier({ enableZoom: true }));
513            sciChartSurface.chartModifiers.add(new ZoomExtentsModifier());
514            sciChartSurface.chartModifiers.add(new MouseWheelZoomModifier());
515        }
516    };
517
518    setType("cursor");
519
520    const callBack = (fn: (arg0: string) => any) => {
521        setStuff = fn;
522    };
523
524    return { sciChartSurface, wasmContext, setType, setData, callBack };
525};
526

Custom Tooltips Example - React

Overview

This React example showcases sophisticated custom tooltip implementations using SciChartReact components. It provides three interactive tooltip modes with seamless React state integration, demonstrating how to bridge SciChart's rendering capabilities with React's reactive data flow.

Technical Implementation

The component uses SciChartReact with an initChart function that returns callback methods for React state synchronization. The implementation features useState hooks for managing tooltip type selection and useRef hooks for preserving chart control functions. Custom tooltip templates leverage SVG generation with dynamic color interpolation and integrate click handlers that trigger React state updates. The vertical slice implementation includes a custom ClickVerticalSliceModifier that extends ChartModifierBase2D for adding interactive slices on click events.

Features and Capabilities

The example demonstrates real-time state synchronization where tooltip data flows into React components for display. Color interpolation dynamically adjusts tooltip text colors based on data values relative to series min/max ranges. Interactive vertical slices feature draggable behavior and removable tooltips with close buttons. The implementation uses multiple chart modifiers including CursorModifier with custom tooltipSvgTemplate and RolloverModifier with per-series tooltip customization as detailed in the Rollover Modifier documentation.

Integration and Best Practices

This example follows React best practices by separating chart initialization logic from presentation components. It demonstrates proper use of useEffect for side effects and useRef for preserving imperative handles. The implementation showcases effective patterns for integrating SciChart's imperative API with React's declarative paradigm, ensuring optimal performance through controlled re-renders and efficient state management.

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