Creates an interactive React 3D waterfall spectral chart with two synchronized 2D cross-section charts.
drawExample.ts
index.tsx
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 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.
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.
For the 2D-only version, see the WaterfallChart example.

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

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.

Create detailed React 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 React 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 React 3D Column Chart using SciChart's 5-star rated JavaScript chart library. Supports large datasets

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

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.

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.

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.