React Interactive 3D Waterfall Spectral Chart

Creates an interactive React 3D waterfall spectral chart with two synchronized 2D cross-section charts.

Fullscreen

Edit

 Edit

Docs

drawExample.ts

index.tsx

Radix2FFT.ts

theme.ts

Copy to clipboard
Minimise
Fullscreen
1import {
2    CameraController,
3    ECameraProjectionMode,
4    EAutoRange,
5    EAxisAlignment,
6    ELegendOrientation,
7    FastLineRenderableSeries,
8    FastMountainRenderableSeries,
9    GradientParams,
10    LegendModifier,
11    MouseWheelZoomModifier3D,
12    NumberRange,
13    NumericAxis,
14    NumericAxis3D,
15    OrbitModifier3D,
16    Point,
17    PointLineRenderableSeries3D,
18    ResetCamera3DModifier,
19    SciChart3DSurface,
20    SciChartJsNavyTheme,
21    SciChartSurface,
22    SeriesSelectionModifier3D,
23    TLegendItem,
24    Thickness,
25    Vector3,
26    XyDataSeries,
27    XyzDataSeries3D,
28    XyzSeriesInfo3D,
29} from "scichart";
30import { Radix2FFT } from "../../../FeaturedApps/ScientificCharts/AudioAnalyzer/Radix2FFT";
31import { appTheme } from "../../../theme";
32
33export const divMainChart3DId = "sciChart1_3d";
34export const divCrossSection1 = "sciChart2";
35export const divCrossSection2 = "sciChart3";
36
37const SERIES_COUNT = 50;
38const DEFAULT_SERIES_POSITION = 0.68;
39const DEFAULT_SLICE_POSITION = 0.72;
40
41const createSpectralData = (n: number) => {
42    const spectraSize = 1024;
43    const timeData = new Array(spectraSize);
44
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    const transform = new Radix2FFT(spectraSize);
54    const yValues = transform.run(timeData).slice(0, 300);
55
56    for (let i = 0; i < yValues.length; i++) {
57        yValues[i] =
58            yValues[i] < -30 || yValues[i] > -5 ? (yValues[i] < -30 ? -30 : Math.random() * 9 - 6) : yValues[i];
59    }
60    yValues[0] = -30;
61
62    const xValues = yValues.map((_, index) => index);
63    return { xValues, yValues };
64};
65
66const parseSeriesIndex = (series: { id?: string } | undefined): number | undefined => {
67    const rawId = series?.id;
68    if (!rawId || rawId.length < 2 || rawId[0] !== "S") {
69        return undefined;
70    }
71
72    const parsed = Number(rawId.substring(1));
73    return Number.isInteger(parsed) ? parsed : undefined;
74};
75
76const resolve3DDataIndex = (hitTestInfo: XyzSeriesInfo3D | undefined) => {
77    if (!hitTestInfo?.isHit || !Number.isInteger(hitTestInfo.dataSeriesIndex) || hitTestInfo.dataSeriesIndex < 0) {
78        return undefined;
79    }
80    return hitTestInfo.dataSeriesIndex;
81};
82
83// This function returns methods for initializing the example
84export const getChartsInitializationAPI = () => {
85    const theme = new SciChartJsNavyTheme();
86    const spectralData = Array.from({ length: SERIES_COUNT }, (_, i) => createSpectralData(i));
87    const defaultSelectedSeriesIndex = Math.min(
88        SERIES_COUNT - 1,
89        Math.max(0, Math.floor(SERIES_COUNT * DEFAULT_SERIES_POSITION))
90    );
91    const defaultSelectedSliceIndex = Math.min(
92        spectralData[0].xValues.length - 1,
93        Math.max(0, Math.floor(spectralData[0].xValues.length * DEFAULT_SLICE_POSITION))
94    );
95
96    let selectedSeriesIndex: number | undefined = defaultSelectedSeriesIndex;
97    let hoveredSeriesIndex: number | undefined;
98    let selectedDataIndex: number | undefined = defaultSelectedSliceIndex;
99
100    let mainChart3DSurface: SciChart3DSurface;
101    const mainChart3DSeries: PointLineRenderableSeries3D[] = [];
102    let mainChartSelectionModifier3D: SeriesSelectionModifier3D;
103
104    let crossSectionSelectedDataSeries: XyDataSeries;
105    let crossSectionHoveredDataSeries: XyDataSeries;
106    let crossSectionSliceSeries: XyDataSeries;
107    let crossSectionLegendModifier: LegendModifier;
108
109    const updateCrossSectionSelectedSeries = () => {
110        crossSectionSelectedDataSeries?.clear();
111        if (selectedSeriesIndex === undefined) {
112            return;
113        }
114
115        const selected = spectralData[selectedSeriesIndex];
116        crossSectionSelectedDataSeries.appendRange(selected.xValues, selected.yValues);
117    };
118
119    const updateCrossSectionHoveredSeries = () => {
120        crossSectionHoveredDataSeries?.clear();
121        if (hoveredSeriesIndex === undefined) {
122            return;
123        }
124
125        const hovered = spectralData[hoveredSeriesIndex];
126        crossSectionHoveredDataSeries.appendRange(hovered.xValues, hovered.yValues);
127    };
128
129    const updateCrossSectionSlice = () => {
130        crossSectionSliceSeries?.clear();
131        if (selectedDataIndex === undefined) {
132            return;
133        }
134
135        const maxIndex = spectralData[0].yValues.length - 1;
136        const safeIndex = Math.max(0, Math.min(maxIndex, selectedDataIndex));
137
138        for (let i = 0; i < spectralData.length; i++) {
139            crossSectionSliceSeries.append(i, spectralData[i].yValues[safeIndex]);
140        }
141    };
142
143    const update3DSeriesStyles = () => {
144        for (let i = 0; i < mainChart3DSeries.length; i++) {
145            const isSelected = i === selectedSeriesIndex;
146            const isHovered = i === hoveredSeriesIndex;
147            const series = mainChart3DSeries[i];
148            series.opacity = isSelected ? 1 : isHovered ? 0.8 : 0.1;
149            series.strokeThickness = isSelected ? 6 : isHovered ? 5 : 4;
150            series.stroke = isSelected ? "#ff6600" : isHovered ? "#FFBE93" : "#64BAE4";
151        }
152    };
153
154    const syncSharedState = () => {
155        update3DSeriesStyles();
156        updateCrossSectionSelectedSeries();
157        updateCrossSectionHoveredSeries();
158        updateCrossSectionSlice();
159        crossSectionLegendModifier?.sciChartLegend?.invalidateLegend();
160    };
161
162    const initMainChart3D = async (rootElement: string | HTMLDivElement) => {
163        const { sciChart3DSurface, wasmContext } = await SciChart3DSurface.create(rootElement, {
164            theme,
165        });
166
167        mainChart3DSurface = sciChart3DSurface;
168        mainChart3DSurface.worldDimensions = new Vector3(300, 100, 150);
169        mainChart3DSurface.camera = new CameraController(wasmContext, {
170            position: new Vector3(-50, 175, 210),
171            target: new Vector3(0, 50, 0),
172            projectionMode: ECameraProjectionMode.Orthogonal,
173            orthoWidth: 450,
174            orthoHeight: 330,
175        });
176
177        mainChart3DSurface.xAxis = new NumericAxis3D(wasmContext, {
178            axisTitle: "Frequency (Hz)",
179            drawMinorGridLines: false,
180            drawMajorGridLines: false,
181            labelPrecision: 0,
182            titleOffset: 10,
183        });
184
185        mainChart3DSurface.yAxis = new NumericAxis3D(wasmContext, {
186            axisTitle: "Power (dB)",
187            drawMinorGridLines: false,
188            drawMajorGridLines: false,
189            labelPrecision: 0,
190            titleOffset: 30,
191        });
192
193        mainChart3DSurface.zAxis = new NumericAxis3D(wasmContext, {
194            axisTitle: "Time (s)",
195            drawMinorGridLines: false,
196            drawMajorGridLines: false,
197            labelPrecision: 0,
198            titleOffset: 30,
199        });
200
201        for (let i = 0; i < SERIES_COUNT; i++) {
202            const { xValues, yValues } = spectralData[i];
203            const zValues = new Array(xValues.length).fill(i);
204
205            const dataSeries = new XyzDataSeries3D(wasmContext, {
206                xValues,
207                yValues,
208                zValues,
209                dataSeriesName: `Spectra ${i}`,
210            });
211
212            const lineSeries = new PointLineRenderableSeries3D(wasmContext, {
213                id: `S${i}`,
214                dataSeries,
215                stroke: "#64BAE4",
216                strokeThickness: 4,
217                opacity: 0.1,
218                isLineStrip: true,
219                isAntiAliased: true,
220            });
221
222            mainChart3DSeries.push(lineSeries);
223            mainChart3DSurface.renderableSeries.add(lineSeries);
224        }
225
226        mainChartSelectionModifier3D = new SeriesSelectionModifier3D({
227            enableHover: true,
228            enableSelection: true,
229            hitTestRadius: 6,
230            prioritizeClosestToCamera: true,
231            onSelectionChanged: args => {
232                selectedSeriesIndex = parseSeriesIndex(args.selectedSeries[0]);
233                const dataIndex = resolve3DDataIndex(args.hitTestInfo as XyzSeriesInfo3D);
234                if (dataIndex !== undefined) {
235                    selectedDataIndex = dataIndex;
236                }
237                syncSharedState();
238            },
239            onHoverChanged: args => {
240                hoveredSeriesIndex = parseSeriesIndex(args.hoveredSeries[0]);
241                const dataIndex = resolve3DDataIndex(args.hitTestInfo as XyzSeriesInfo3D);
242                if (dataIndex !== undefined) {
243                    selectedDataIndex = dataIndex;
244                }
245                syncSharedState();
246            },
247        });
248
249        mainChart3DSurface.chartModifiers.add(
250            new MouseWheelZoomModifier3D(),
251            new OrbitModifier3D(),
252            new ResetCamera3DModifier(),
253            mainChartSelectionModifier3D
254        );
255
256        return { sciChartSurface: sciChart3DSurface, wasmContext };
257    };
258
259    const initCrossSectionLeft = async (rootElement: string | HTMLDivElement) => {
260        const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
261            disableAspect: true,
262            theme,
263        });
264
265        sciChartSurface.xAxes.add(
266            new NumericAxis(wasmContext, {
267                autoRange: EAutoRange.Always,
268                drawMinorGridLines: false,
269            })
270        );
271        sciChartSurface.yAxes.add(
272            new NumericAxis(wasmContext, {
273                autoRange: EAutoRange.Never,
274                axisAlignment: EAxisAlignment.Left,
275                visibleRange: new NumberRange(-30, 10),
276                drawMinorGridLines: false,
277            })
278        );
279
280        crossSectionSelectedDataSeries = new XyDataSeries(wasmContext, { dataSeriesName: "Selected Spectrum" });
281        sciChartSurface.renderableSeries.add(
282            new FastLineRenderableSeries(wasmContext, {
283                stroke: "#ff6600",
284                strokeThickness: 3,
285                dataSeries: crossSectionSelectedDataSeries,
286            })
287        );
288
289        crossSectionHoveredDataSeries = new XyDataSeries(wasmContext, { dataSeriesName: "Hovered Spectrum" });
290        sciChartSurface.renderableSeries.add(
291            new FastMountainRenderableSeries(wasmContext, {
292                stroke: "#64BAE477",
293                strokeThickness: 3,
294                strokeDashArray: [2, 2],
295                fillLinearGradient: new GradientParams(new Point(0, 0), new Point(0, 1), [
296                    { color: "#64BAE455", offset: 0 },
297                    { color: "#64BAE400", offset: 1 },
298                ]),
299                dataSeries: crossSectionHoveredDataSeries,
300                zeroLineY: -999,
301            })
302        );
303
304        crossSectionLegendModifier = new LegendModifier({
305            showCheckboxes: false,
306            orientation: ELegendOrientation.Horizontal,
307        });
308        crossSectionLegendModifier.sciChartLegend.getLegendItemHTML = (
309            orientation: ELegendOrientation,
310            showCheckboxes: boolean,
311            showSeriesMarkers: boolean,
312            item: TLegendItem
313        ): string => {
314            const display = orientation === ELegendOrientation.Vertical ? "flex" : "inline-flex";
315            let str = `<span class=\"scichart__legend-item\" style=\"display:${display};align-items:center;margin-right:4px;padding:0 4px 0 5px;white-space:nowrap;gap:6px\">`;
316
317            if (showCheckboxes) {
318                const checked = item.checked ? "checked" : "";
319                str += `<input ${checked} type=\"checkbox\" id=\"${item.id}\">`;
320            }
321
322            if (showSeriesMarkers) {
323                const isHoveredLegend = item.name === "Hovered Spectrum";
324                const dashAttr = isHoveredLegend ? `stroke-dasharray=\"2,2\"` : "";
325                str += `<svg xmlns=\"http://www.w3.org/2000/svg\" for=\"${item.id}\" style=\"width:18px;height:12px\" viewBox=\"0 0 18 12\"><line x1=\"1\" y1=\"6\" x2=\"17\" y2=\"6\" stroke=\"${item.color}\" stroke-width=\"2\" ${dashAttr} /></svg>`;
326            }
327
328            str += `<label for=\"${item.id}\">${item.name}</label>`;
329            str += `</span>`;
330            return str;
331        };
332        crossSectionLegendModifier.isEnabled = false;
333        sciChartSurface.chartModifiers.add(crossSectionLegendModifier);
334
335        return { sciChartSurface, wasmContext };
336    };
337
338    const initCrossSectionRight = async (rootElement: string | HTMLDivElement) => {
339        const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
340            disableAspect: true,
341            theme,
342            title: "Cross Section Slice",
343            titleStyle: {
344                fontSize: 13,
345                padding: Thickness.fromNumber(10),
346            },
347        });
348
349        sciChartSurface.xAxes.add(
350            new NumericAxis(wasmContext, {
351                autoRange: EAutoRange.Always,
352                drawMinorGridLines: false,
353            })
354        );
355        sciChartSurface.yAxes.add(
356            new NumericAxis(wasmContext, {
357                autoRange: EAutoRange.Never,
358                axisAlignment: EAxisAlignment.Left,
359                visibleRange: new NumberRange(-30, 5),
360                drawMinorGridLines: false,
361            })
362        );
363
364        crossSectionSliceSeries = new XyDataSeries(wasmContext);
365        sciChartSurface.renderableSeries.add(
366            new FastMountainRenderableSeries(wasmContext, {
367                stroke: "#64BAE4",
368                strokeThickness: 3,
369                strokeDashArray: [2, 2],
370                fillLinearGradient: new GradientParams(new Point(0, 0), new Point(0, 1), [
371                    { color: "#64BAE477", offset: 0 },
372                    { color: "#64BAE433", offset: 1 },
373                ]),
374                dataSeries: crossSectionSliceSeries,
375                zeroLineY: -999,
376            })
377        );
378
379        return { sciChartSurface, wasmContext };
380    };
381
382    const configureAfterInit = () => {
383        crossSectionLegendModifier.isEnabled = true;
384        syncSharedState();
385    };
386
387    return {
388        initMainChart3D,
389        initCrossSectionLeft,
390        initCrossSectionRight,
391        configureAfterInit,
392    };
393};
394

Interactive 3D Waterfall Spectral Chart in React

Overview

This React example renders the 3D spectral waterfall and the two lower 2D cross-section charts in a single synchronized layout. It uses <SciChartReact/> and ChartGroupLoader so the top 3D chart and the lower series/slice views are initialized together.

Technical Implementation

The implementation creates a SciChart3DSurface, sets a custom camera, and adds 3D modifiers for orbiting, zooming, and selection. The data is generated with Radix2FFT, then mapped into a stack of PointLineRenderableSeries3D series in the 3D scene. The lower charts read from the same shared selection state to display the selected spectrum and cross-section slice.

Related Example

For the 2D-only version, see the WaterfallChart example.

react Chart Examples & Demos

See Also: JavaScript 3D Chart Types (11 Demos)

React 3D Bubble Chart | 3D JavaScript Charts | View Now

React 3D Bubble Chart

Create detailed React 3D Bubble Chart using SciChart's 5-star rated JavaScript chart library. Supports large datasets. Get your free demo now.

React 3D Surface Mesh Chart | View 3D JavaScript Charts

React Surface Mesh 3D Chart

Design a React 3D Surface Mesh Chart with SciChart.js - feature-rich JavaScript chart library. Represent 2D data in a 3D map. Get your free demo.

React 3D Point Line Chart | View 3D JavaScript Charts

React Point Line 3D Chart

Create detailed React 3D Line Chart using SciChart's 5-star rated JavaScript chart library. Supports large datasets. Get your free demo now.

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.

Tenor Curves Demo | React Charts | SciChart.js Demo

Tenor Curves Demo

Demonstrating the capability of SciChart.js to create a composite 2D &amp; 3D Chart application. An example like this could be used to visualize Tenor curves in a financial setting, or other 2D/3D data combined on a single screen.

React Realtime 3D Surface Mesh Chart | View 3D JavaScript Charts

React Realtime Surface Mesh 3D Chart

Design a React 3D Surface Mesh Chart with SciChart.js - feature-rich JavaScript chart library. Represent 2D data in a 3D map. Get your free demo.

React 3D Column Chart | View 3D JavaScript Charts | SciChart

React Column 3D Chart

Create detailed React 3D Column Chart using SciChart's 5-star rated JavaScript chart library. Supports large datasets

React 3D Styling Demo | Advanced 3D Chart Styling | SciChart

React 3D Styling Demo

Create advanced React 3D Styling Demo using SciChart's 5-star rated JavaScript chart library. Explore axis styling, plane visibility and multiple point markers.

React 3D Log-Log Scatter Chart | 3D JavaScript Charts | View Now

React 3D Log-Log Scatter Chart

Create a React 3D Log-Log Scatter Chart using SciChart's high-performance WebGL chart library. Features LogarithmicAxis3D on X and Y axes. Get your free demo now.

NEW!
React Immediate Mesh 3D Chart | View 3D JavaScript Charts

React Immediate Mesh 3D Chart

Draw custom lit 3D geometry in React with SciChart.js using ImmediateLitMeshContext. Emit per-vertex positions, normals, texture coordinates and colours from a custom 3D scene entity.

NEW!
React 3D Model Example | View 3D JavaScript Charts | SciChart

React 3D Model Example

Load a 3D model in React with SciChart.js. Add the model to a SciChart3DSurface as a 3D scene entity, frame it with the camera, then orbit and zoom it. Any 3D model can be loaded.

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