Creates an interactive JavaScript 3D waterfall spectral chart with two synchronized 2D cross-section charts.
drawExample.ts
index.html
vanilla.ts
Radix2FFT.ts
theme.ts
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};
394This 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.
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.
If you need the 2D-only waterfall variant, see the WaterfallChart example.

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

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.

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

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

Demonstrating the capability of SciChart.js to create a composite 2D & 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.

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.

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

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

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.

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.

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.