Demonstrates how to customise the tooltips for Rollover, Cursor and VerticalSlice modifiers using SciChart.js, High Performance JavaScript Charts.
drawExample.ts
index.html
vanilla.ts
ExampleDataProvider.ts
theme.ts
selection.ts
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};
526This 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.
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.
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.
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.

Demonstrates Hit-Testing a JavaScript Chart - point and click on the chart and get feedback about what data-points were clicked

Demonstrates adding Tooltips on mouse-move to a JavaScript Chart with SciChart.js RolloverModifier

Demonstrates adding a Cursor (Crosshair) to a JavaScript Chart with SciChart.js CursorModifier

Demonstrates adding Tooltips at certain positions to a JavaScript Chart with SciChart.js VerticalSliceModifier

Demonstrates using MetaData in a JavaScript Chart - add custom data to points for display or to drive visual customisation

Demonstrates Hit-Testing a JavaScript Chart - point and click on the chart and get feedback about what data-points were clicked

Demonstrates the DatapointSelectionModifier, which provides a UI to select one or many data points, and works with DataPointSelectionPaletteProvider to change the appearance of selected points