Demonstrates how to create a Waterfall chart in SciChart.js, showing chromotragraphy data with interactive selection of points.
drawExample.ts
index.html
Radix2FFT.ts
vanilla.ts
theme.ts
1import {
2 AnnotationDragDeltaEventArgs,
3 CustomAnnotation,
4 DefaultPaletteProvider,
5 EAutoRange,
6 EAxisAlignment,
7 ECoordinateMode,
8 EHorizontalAnchorPoint,
9 ELegendOrientation,
10 EStrokePaletteMode,
11 EVerticalAnchorPoint,
12 EXyDirection,
13 FastLineRenderableSeries,
14 FastMountainRenderableSeries,
15 GradientParams,
16 IRenderableSeries,
17 LegendModifier,
18 libraryVersion,
19 MouseWheelZoomModifier,
20 NumberRange,
21 NumericAxis,
22 Point,
23 SciChartJsNavyTheme,
24 SciChartSurface,
25 SeriesSelectionModifier,
26 Thickness,
27 TSciChart,
28 XyDataSeries,
29 ZoomExtentsModifier,
30 ZoomPanModifier,
31} from "scichart";
32import { Radix2FFT } from "../AudioAnalyzer/Radix2FFT";
33import { appTheme } from "../../../theme";
34
35export const divMainChartId = "sciChart1";
36export const divCrossSection1 = "sciChart2";
37export const divCrossSection2 = "sciChart3";
38
39// This function generates some spectral data for the waterfall chart
40const createSpectralData = (n: number) => {
41 const spectraSize = 1024;
42 const timeData = new Array(spectraSize);
43
44 // Generate some random data with spectral components
45 for (let i = 0; i < spectraSize; i++) {
46 timeData[i] =
47 2.0 * Math.sin((2 * Math.PI * i) / (20 + n * 0.2)) +
48 5 * Math.sin((2 * Math.PI * i) / (10 + n * 0.01)) +
49 10 * Math.sin((2 * Math.PI * i) / (5 + n * -0.002)) +
50 2.0 * Math.random();
51 }
52
53 // Do a fourier-transform on the data to get the frequency domain
54 const transform = new Radix2FFT(spectraSize);
55 const yValues = transform.run(timeData).slice(0, 300); // We only want the first N points just to make the example cleaner
56
57 // This is just setting a floor to make the data cleaner for the example
58 for (let i = 0; i < yValues.length; i++) {
59 yValues[i] =
60 yValues[i] < -30 || yValues[i] > -5 ? (yValues[i] < -30 ? -30 : Math.random() * 9 - 6) : yValues[i];
61 }
62 yValues[0] = -30;
63
64 // we need x-values (sequential numbers) for the frequency data
65 const xValues = yValues.map((value, index) => index);
66
67 return { xValues, yValues };
68};
69
70// class CustomOffsetAxis extends NumericAxis {
71// constructor(wasmContext: TSciChart, options: INumericAxisOptions) {
72// super(wasmContext, options);
73// }
74
75// public customOffset: number = 0;
76
77// public get offset(): number {
78// return this.customOffset;
79// }
80
81// public set offset(value: number) {
82// // do nothing
83// }
84// }
85
86// tslint:disable-next-line:max-classes-per-file
87class CrossSectionPaletteProvider extends DefaultPaletteProvider {
88 public selectedIndex: number = -1;
89 public shouldUpdate: boolean = true;
90
91 public override shouldUpdatePalette(): boolean {
92 return this.shouldUpdate;
93 }
94
95 public override overrideStrokeArgb(xValue: number, yValue: number, index: number, opacity: number): number {
96 if (index === this.selectedIndex || index + 1 === this.selectedIndex || index - 1 === this.selectedIndex) {
97 return 0xffff8a42;
98 }
99 return undefined;
100 }
101}
102
103// This function returns methods for initializing the example
104export const getChartsInitializationAPI = () => {
105 const theme = new SciChartJsNavyTheme();
106
107 let mainChartSurface: SciChartSurface;
108 let mainChartSelectionModifier: SeriesSelectionModifier;
109 const crossSectionPaletteProvider = new CrossSectionPaletteProvider();
110 let dragMeAnnotation: CustomAnnotation;
111
112 // This function creates the main chart with waterfall series
113 // To do this, we create N series, each with its own X,Y axis with a different X,Y offset
114 // all axis other than the first are hidden
115 const initMainChart = async (rootElement: string | HTMLDivElement) => {
116 const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
117 disableAspect: true,
118 theme,
119 });
120
121 mainChartSurface = sciChartSurface;
122
123 const seriesCount = 50;
124 for (let i = 0; i < seriesCount; i++) {
125 // Create one yAxis per series
126 const yAxis = new NumericAxis(wasmContext, {
127 id: "Y" + i,
128 axisAlignment: EAxisAlignment.Left,
129 maxAutoTicks: 5,
130 drawMinorGridLines: false,
131 visibleRange: new NumberRange(-50, 60),
132 isVisible: i === seriesCount - 1,
133 overrideOffset: 3 * -i,
134 });
135 sciChartSurface.yAxes.add(yAxis);
136
137 // Create a shared, default xaxis
138 const xAxis = new NumericAxis(wasmContext, {
139 id: "X" + i,
140 axisAlignment: EAxisAlignment.Bottom,
141 maxAutoTicks: 5,
142 drawMinorGridLines: false,
143 growBy: new NumberRange(0, 0.2),
144 isVisible: i === seriesCount - 1,
145 overrideOffset: 2 * i,
146 });
147 sciChartSurface.xAxes.add(xAxis);
148
149 // Create some data for the example
150 const { xValues, yValues } = createSpectralData(i);
151 mainChartSurface.rendered.subscribe(() => {
152 // Don't recalculate the palette unless the selected index changes
153 crossSectionPaletteProvider.shouldUpdate = false;
154 });
155 const lineSeries = new FastLineRenderableSeries(wasmContext, {
156 id: "S" + i,
157 xAxisId: "X" + i,
158 yAxisId: "Y" + i,
159 stroke: "#64BAE4",
160 strokeThickness: 1,
161 dataSeries: new XyDataSeries(wasmContext, { xValues, yValues, dataSeriesName: `Spectra ${i}` }),
162 paletteProvider: crossSectionPaletteProvider,
163 });
164 // Insert series in reverse order so the ones at the bottom of the chart are drawn first
165 // sciChartSurface.renderableSeries.insert(0, lineSeries);
166 sciChartSurface.renderableSeries.add(lineSeries);
167 }
168
169 // Add an annotation which can be dragged horizontally to update the bottom right chart
170 dragMeAnnotation = new CustomAnnotation({
171 svgString: `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="82">
172 <g>
173 <line x1="50%" y1="10" x2="50%" y2="27" stroke="#FFBE93" stroke-dasharray="2,2" />
174 <circle cx="50%" cy="10" r="5" fill="#FFBE93" />
175 <rect x="2" y="27" rx="10" ry="10" width="96" height="30" fill="#64BAE433" stroke="#64BAE4" stroke-width="2" />
176 <text x="50%" y="42" fill="${appTheme.TextColor}" text-anchor="middle" alignment-baseline="middle" >Drag me!</text>
177 </g>
178 </svg>`,
179 x1: 133,
180 y1: -25,
181 xAxisId: "X0",
182 yAxisId: "Y0",
183 isEditable: true,
184 annotationsGripsFill: "Transparent",
185 annotationsGripsStroke: "Transparent",
186 selectionBoxStroke: "Transparent",
187 horizontalAnchorPoint: EHorizontalAnchorPoint.Center,
188 verticalAnchorPoint: EVerticalAnchorPoint.Top,
189 });
190 sciChartSurface.annotations.add(dragMeAnnotation);
191
192 // Place an annotation with further instructions in the top right of the chart
193 const promptAnnotation = new CustomAnnotation({
194 svgString: `<svg xmlns="http://www.w3.org/2000/svg" width="130" height="82">
195 <g>
196 <line x1="5" y1="77" x2="40" y2="33" stroke="#ffffff" stroke-dasharray="2,2" />
197 <circle cx="5" cy="77" r="5" fill="#ffffff" />
198 <g>
199 <rect x="10" y="2" width="118" height="30" rx="10" ry="10" fill="#64BAE433" stroke="#64BAE4" stroke-width="2" />
200 <text x="68" y="19" fill="${appTheme.TextColor}" text-anchor="middle" alignment-baseline="middle" font-size="12">Hover/click chart</text>
201 </g>
202 </g>
203 </svg>`,
204 xAxisId: "X0",
205 yAxisId: "Y0",
206 isEditable: false,
207 xCoordinateMode: ECoordinateMode.Relative,
208 yCoordinateMode: ECoordinateMode.Relative,
209 horizontalAnchorPoint: EHorizontalAnchorPoint.Right,
210 verticalAnchorPoint: EVerticalAnchorPoint.Top,
211 x1: 0.9,
212 y1: 0.1,
213 });
214
215 sciChartSurface.annotations.add(promptAnnotation);
216
217 // Add zooming behaviours
218 sciChartSurface.chartModifiers.add(
219 new ZoomPanModifier({ enableZoom: true, xyDirection: EXyDirection.XDirection }),
220 new MouseWheelZoomModifier({ xyDirection: EXyDirection.XDirection }),
221 new ZoomExtentsModifier({ xyDirection: EXyDirection.XDirection })
222 );
223
224 const updateSeriesSelectionState = (series: IRenderableSeries) => {
225 series.stroke = series.isSelected ? appTheme.TextColor : series.isHovered ? "#FFBE93" : "#64BAE4";
226 series.strokeThickness = series.isSelected || series.isHovered ? 3 : 1;
227 };
228
229 let prevSelectedSeries: IRenderableSeries = sciChartSurface.renderableSeries.get(0);
230 // Add selection behaviour
231 mainChartSelectionModifier = new SeriesSelectionModifier({
232 enableHover: true,
233 enableSelection: true,
234 hitTestRadius: 5,
235 onSelectionChanged: (args) => {
236 if (args.selectedSeries.length > 0) {
237 prevSelectedSeries = args.selectedSeries[0];
238 args.allSeries.forEach(updateSeriesSelectionState);
239 } else {
240 prevSelectedSeries.isSelected = true;
241 }
242 },
243 onHoverChanged: (args) => {
244 args.allSeries.forEach(updateSeriesSelectionState);
245 },
246 });
247 sciChartSurface.chartModifiers.add(mainChartSelectionModifier);
248 return { sciChartSurface };
249 };
250
251 let crossSectionSelectedSeries: IRenderableSeries;
252 let crossSectionHoveredSeries: IRenderableSeries;
253 let crossSectionSliceSeries: XyDataSeries;
254 let crossSectionLegendModifier: LegendModifier;
255
256 // In the bottom left chart, add two series to show the currently hovered/selected series on the main chart
257 // These will be updated in the selection callback below
258 const initCrossSectionLeft = async (rootElement: string | HTMLDivElement) => {
259 const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
260 disableAspect: true,
261 theme,
262 });
263
264 sciChartSurface.xAxes.add(
265 new NumericAxis(wasmContext, {
266 autoRange: EAutoRange.Always,
267 drawMinorGridLines: false,
268 })
269 );
270 sciChartSurface.yAxes.add(
271 new NumericAxis(wasmContext, {
272 autoRange: EAutoRange.Never,
273 axisAlignment: EAxisAlignment.Left,
274 visibleRange: new NumberRange(-30, 5),
275 drawMinorGridLines: false,
276 })
277 );
278
279 crossSectionSelectedSeries = new FastLineRenderableSeries(wasmContext, {
280 stroke: "#ff6600",
281 strokeThickness: 3,
282 });
283 sciChartSurface.renderableSeries.add(crossSectionSelectedSeries);
284 crossSectionHoveredSeries = new FastMountainRenderableSeries(wasmContext, {
285 stroke: "#64BAE477",
286 strokeThickness: 3,
287 strokeDashArray: [2, 2],
288 fillLinearGradient: new GradientParams(new Point(0, 0), new Point(0, 1), [
289 { color: "#64BAE455", offset: 0 },
290 { color: "#64BAE400", offset: 1 },
291 ]),
292 dataSeries: crossSectionSliceSeries,
293 zeroLineY: -999,
294 });
295 sciChartSurface.renderableSeries.add(crossSectionHoveredSeries);
296
297 // Add a legend to the bottom left chart
298 crossSectionLegendModifier = new LegendModifier({
299 showCheckboxes: false,
300 orientation: ELegendOrientation.Horizontal,
301 });
302 crossSectionLegendModifier.isEnabled = false;
303 sciChartSurface.chartModifiers.add(crossSectionLegendModifier);
304
305 return { sciChartSurface };
306 };
307
308 const initCrossSectionRight = async (rootElement: string | HTMLDivElement) => {
309 const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
310 disableAspect: true,
311 theme,
312 title: "Cross Section Slice",
313 titleStyle: {
314 fontSize: 13,
315 padding: Thickness.fromNumber(10),
316 },
317 });
318
319 sciChartSurface.xAxes.add(
320 new NumericAxis(wasmContext, {
321 autoRange: EAutoRange.Always,
322 drawMinorGridLines: false,
323 })
324 );
325 sciChartSurface.yAxes.add(
326 new NumericAxis(wasmContext, {
327 autoRange: EAutoRange.Never,
328 axisAlignment: EAxisAlignment.Left,
329 visibleRange: new NumberRange(-30, 5),
330 drawMinorGridLines: false,
331 })
332 );
333
334 crossSectionSliceSeries = new XyDataSeries(wasmContext);
335 sciChartSurface.renderableSeries.add(
336 new FastMountainRenderableSeries(wasmContext, {
337 stroke: "#64BAE4",
338 strokeThickness: 3,
339 strokeDashArray: [2, 2],
340 fillLinearGradient: new GradientParams(new Point(0, 0), new Point(0, 1), [
341 { color: "#64BAE477", offset: 0 },
342 { color: "#64BAE433", offset: 1 },
343 ]),
344 dataSeries: crossSectionSliceSeries,
345 zeroLineY: -999,
346 })
347 );
348
349 return { sciChartSurface };
350 };
351
352 const configureAfterInit = () => {
353 // Link interactions together
354 mainChartSelectionModifier.selectionChanged.subscribe((args) => {
355 const selectedSeries = args.selectedSeries[0]?.dataSeries;
356 if (selectedSeries) {
357 crossSectionSelectedSeries.dataSeries = selectedSeries;
358 }
359 crossSectionLegendModifier.isEnabled = true;
360 crossSectionLegendModifier.sciChartLegend?.invalidateLegend();
361 });
362 mainChartSelectionModifier.hoverChanged.subscribe((args) => {
363 const hoveredSeries = args.hoveredSeries[0]?.dataSeries;
364 if (hoveredSeries) {
365 crossSectionHoveredSeries.dataSeries = hoveredSeries;
366 }
367 crossSectionLegendModifier.sciChartLegend?.invalidateLegend();
368 });
369
370 // Add a function to update drawing the cross-selection when the drag annotation is dragged
371 const updateDragAnnotation = () => {
372 // Don't allow to drag vertically, only horizontal
373 dragMeAnnotation.y1 = -25;
374
375 // Find the index to the x-values that the axis marker is on
376 // Note you could just loop getNativeXValues() here but the wasmContext.NumberUtil function does it for you
377 const dataIndex = mainChartSurface.webAssemblyContext2D.NumberUtil.FindIndex(
378 mainChartSurface.renderableSeries.get(0).dataSeries.getNativeXValues(),
379 dragMeAnnotation.x1,
380 mainChartSurface.webAssemblyContext2D.SCRTFindIndexSearchMode.Nearest,
381 true
382 );
383
384 crossSectionPaletteProvider.selectedIndex = dataIndex;
385 crossSectionPaletteProvider.shouldUpdate = true;
386 mainChartSurface.invalidateElement();
387 if (crossSectionSliceSeries) {
388 crossSectionSliceSeries.clear();
389 console.log("crossSectionSliceSeries:", crossSectionSliceSeries);
390 for (let i = 0; i < mainChartSurface.renderableSeries.size(); i++) {
391 crossSectionSliceSeries.append(
392 i,
393 mainChartSurface.renderableSeries.get(i).dataSeries.getNativeYValues().get(dataIndex)
394 );
395 }
396 } else {
397 console.error("crossSectionSliceSeries is not defined");
398 }
399 };
400
401 // Run it once
402 updateDragAnnotation();
403
404 //Run it when user drags the annotation
405 dragMeAnnotation.dragDelta.subscribe((args: AnnotationDragDeltaEventArgs) => {
406 updateDragAnnotation();
407 });
408
409 mainChartSurface.renderableSeries.get(0).isSelected = true;
410 };
411
412 return { initMainChart, initCrossSectionLeft, initCrossSectionRight, configureAfterInit };
413};
414This example demonstrates an interactive waterfall chart implementation using SciChart.js in a JavaScript environment. The chart visualizes spectral data generated by performing a Fourier transform on simulated time series data with the help of the Radix2FFT class. The waterfall design is achieved by layering multiple series – each with its own custom offset for the X and Y axes – across a main chart, while two additional cross-section charts update dynamically based on user interaction.
The implementation creates the waterfall effect by configuring a unique pair of numeric axes for each of the fifty series using properties such as the overrideOffset. Each series’ data is generated by simulating time-based signals and then applying a Fourier transform to extract a limited number of spectral components. Data rendering is optimized using the FastLineRenderableSeries, which is designed for high performance with large datasets. In addition, a custom palette provider is implemented to dynamically adjust the stroke colors of the series – for instance, to highlight values around a user-draggable annotation. For details on custom palette providers and extending the chart’s appearance, refer to the PaletteProvider API.
Interactive elements are integrated through chart modifiers such as ZoomPanModifier, MouseWheelZoomModifier, and the SeriesSelectionModifier. The example uses a draggable CustomAnnotation – implemented via SciChart.js’s Editable Annotations – to allow users to select a specific spectral slice. This selection is then synchronized with the cross-section charts using the Series Selection functionality. Additionally, the setup for multiple axes is illustrated in the Adding Multiple Axis Tutorial.
Multi-Axis Configuration: Each series in the waterfall chart uses its own X and Y axes with distinct offsets, a technique that produces the layered waterfall effect. More details on configuring multiple axes can be found in the Tutorial on Adding Multiple Axes.
Performance Optimization: By using the FastLineRenderableSeries, the example ensures a smooth rendering experience even with numerous series and large datasets. Developers looking to learn more about performance optimizations in SciChart.js should consult the Performance Tips & Tricks documentation.
Interactive Annotations and Cross-Chart Communication: The example leverages interactive, draggable annotations to update cross-sectional views in real time. This synchronization across multiple chart surfaces demonstrates how to implement cross-chart interactions using the Annotations API Overview and the SeriesSelectionModifier detailed in the Series Selection documentation.
This implementation is done entirely with JavaScript, which means that it directly utilizes SciChart.js’s robust API without additional framework abstractions such as Angular or React. Developers are encouraged to incorporate efficient data processing techniques – for example, generating spectral data through Fourier transforms – and to structure the chart using modular functions that update only when necessary. For enhanced visual presentation, advanced rendering features like gradient fills in area series can be explored in the Mountain / Area Series Documentation.
Overall, this example provides a comprehensive guide to building interactive and high-performance waterfall charts with SciChart.js, showcasing the ability to customize series coloring, manage multiple axes, and synchronize interactive updates across chart surfaces in a pure JavaScript environment.

In this example we are simulating four channels of data showing that SciChart.js can be used to draw real-time ECG/EKG charts and graphs to monitor heart reate, body temperature, blood pressure, pulse rate, SPO2 blood oxygen, volumetric flow and more.

Demonstrates Logarithmic Axis on a JavaScript Chart using SciChart.js. SciChart supports logarithmic axis with scientific or engineering notation and positive and negative values

Demonstrating the capability of SciChart.js to create JavaScript 3D Point Cloud charts and visualize LiDAR data from the UK Defra Survey.

Demonstrates Vertically Stacked Axes on a JavaScript Chart using SciChart.js, allowing data to overlap

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

Demonstrating the capability of SciChart.js to create a JavaScript Audio Analyzer Bars and visualize the Fourier-Transform of an audio waveform in realtime.

See the JavaScript Phasor Diagram example to combine a Cartesian surface with a Polar subsurface. Get seamless JS integration with SciChart. View demo now.

Create JavaScript Correlation Plot with high performance SciChart.js. Easily render pre-defined point types. Supports custom shapes. Get your free trial now.
JavaScript **Semiconductors Dashboard** using SciChart.js, by leveraging the **FastRectangleRenderableSeries**, and its `customTextureOptions` property to have a custom tiling texture fill.

JavaScript **Wafer Analysis Chart** using SciChart.js, by leveraging the **FastRectangleRenderableSeries**, and crossfilter to enable live filtering.