Interactive Waterfall Spectral Chart

Demonstrates how to create a Waterfall chart in SciChart.js, showing chromotragraphy data with interactive selection of points.

Fullscreen

Edit

 Edit

Docs

drawExample.ts

index.html

Radix2FFT.ts

vanilla.ts

theme.ts

Copy to clipboard
Minimise
Fullscreen
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};
414

Interactive Waterfall Chart (JavaScript)

Overview

This 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.

Technical Implementation

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.

Features and Capabilities

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.

Integration and Best Practices

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.

javascript Chart Examples & Demos

See Also: Scientific & Medical Charts (10 Demos)

JavaScript Vital Signs ECG/EKG Medical Demo | SciChart.js

JavaScript Vital Signs ECG/EKG Medical Demo

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.

JavaScript Chart with Logarithmic Axis Example | SciChart

JavaScript Chart with Logarithmic Axis Example

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

LiDAR 3D Point Cloud of Geospatial Data | SciChart.js

LiDAR 3D Point Cloud of Geospatial Data

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

JavaScript Chart with Vertically Stacked Axes | SciChart

JavaScript Chart with Vertically Stacked Axes

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

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.

Realtime Audio Analyzer Bars Demo | SciChart.js Demo

Realtime Audio Analyzer Bars Demo

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

Interactive Phasor Diagram chart | Javascript Charts | SciChart.js

Phasor Diagram Chart Example

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

NEW!
JavaScript Correlation Plot | Javascript Charts | SciChart.js

JavaScript Correlation Plot

Create JavaScript Correlation Plot with high performance SciChart.js. Easily render pre-defined point types. Supports custom shapes. Get your free trial now.

NEW!
Semiconductors Dashboard | JavaScript Charts | SciChart.js

Semiconductors Dashboard

JavaScript **Semiconductors Dashboard** using SciChart.js, by leveraging the **FastRectangleRenderableSeries**, and its `customTextureOptions` property to have a custom tiling texture fill.

NEW!
Wafer Analysis Chart | JavaScript Charts | SciChart.js

Wafer Analysis Chart

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

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