Using Custom Tooltips

Demonstrates how to customise the tooltips for Rollover, Cursor and VerticalSlice modifiers using SciChart.js, High Performance JavaScript Charts.

Fullscreen

Edit

 Edit

Docs

drawExample.ts

index.html

vanilla.ts

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 - JavaScript

Overview

This JavaScript example demonstrates advanced custom tooltip implementations using SciChart.js. It showcases three distinct tooltip modes: cursor tooltips, rollover tooltips, and interactive vertical slice tooltips with dynamic color interpolation and click-to-add functionality.

Technical Implementation

The implementation creates a SciChartSurface with multiple FastLineRenderableSeries using EllipsePointMarkers for visual enhancement. The core innovation lies in the custom tooltip templates that generate dynamic SVG content. The cursorSvgTemplate creates multi-series tooltips with color interpolation based on Y-values, while the verticalSliceTooltipTemplate includes interactive close buttons. A custom ClickVerticalSliceModifier extends ChartModifierBase2D to handle mouse interactions for adding draggable vertical slices programmatically.

Features and Capabilities

The example features real-time color interpolation using the interpolateColor function that transitions between colors based on data values. Interactive vertical slices can be added via mouse clicks and removed through tooltip buttons. The implementation leverages adjustTooltipPosition for automatic tooltip placement and uses SeriesInfo objects to access real-time chart data. Multiple chart modifiers including CursorModifier, RolloverModifier, and ZoomPanModifier provide comprehensive interaction capabilities as documented in the Chart Modifier API.

Integration and Best Practices

This vanilla JavaScript implementation demonstrates proper lifecycle management through async initialization and cleanup functions. The modular approach separates data generation, tooltip templating, and interaction handling into reusable components. Performance is optimized through efficient SVG generation and proper use of the WebAssembly context for high-speed rendering.

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