JavaScript Interactive 3D Waterfall Spectral Chart

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

Fullscreen

Edit

 Edit

Docs

drawExample.ts

index.html

vanilla.ts

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

Overview

This example combines a 3D spectral waterfall with two synchronized 2D cross-section charts. The top chart renders 50 stacked spectral slices using PointLineRenderableSeries3D, while the lower charts show the selected spectrum and the current cross-section slice.

Technical Implementation

The chart is initialized by creating a SciChart3DSurface, configuring a 3D camera with CameraController, and adding MouseWheelZoomModifier3D, OrbitModifier3D, ResetCamera3DModifier, and SeriesSelectionModifier3D. Spectral data is generated with an FFT pipeline based on Radix2FFT, and the lower charts are driven by the same series-selection and data-index state.

Related Example

If you need the 2D-only waterfall variant, see the WaterfallChart example.

javascript Chart Examples & Demos

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

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

JavaScript 3D Bubble Chart

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

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

JavaScript Surface Mesh 3D Chart

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

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

JavaScript Point Line 3D Chart

Create detailed JavaScript 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 | Javascript 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.

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

JavaScript Realtime Surface Mesh 3D Chart

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

JavaScript 3D Column Chart | View 3D JavaScript Charts

JavaScript Column 3D Chart

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

JavaScript 3D Styling Demo Chart | Advanced 3D Chart Styling

JavaScript 3D Styling Demo Chart

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

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

JavaScript 3D Log-Log Scatter Chart

Create a JavaScript 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!
JavaScript Immediate Mesh 3D Chart | View 3D JavaScript Charts

JavaScript Immediate Mesh 3D Chart

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

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

JavaScript 3D Model Example

Load a 3D model in JavaScript 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.