Using the SubCharts API as part of SciChart.js, this demo showcases an 8x8 grid of 64 charts updating in realtime in JavaScript.
drawExample.ts
index.tsx
theme.ts
helpers.ts
1import {
2 appendData,
3 createRenderableSeries,
4 generateData,
5 getDataSeriesTypeForRenderableSeries,
6 getSubChartPositionIndexes,
7 prePopulateData,
8} from "./helpers";
9
10import {
11 BaseDataSeries,
12 EAnnotationLayer,
13 ESubSurfacePositionCoordinateMode,
14 EDataSeriesType,
15 EAutoRange,
16 ENumericFormat,
17 EHorizontalAnchorPoint,
18 EMultiLineAlignment,
19 ESeriesType,
20 IRenderableSeries,
21 INumericAxisOptions,
22 I2DSubSurfaceOptions,
23 MouseWheelZoomModifier,
24 NativeTextAnnotation,
25 NumericAxis,
26 NumberRange,
27 Rect,
28 RightAlignedOuterVerticallyStackedAxisLayoutStrategy,
29 SciChartSubSurface,
30 SciChartSurface,
31 StackedColumnCollection,
32 StackedColumnRenderableSeries,
33 StackedMountainCollection,
34 StackedMountainRenderableSeries,
35 Thickness,
36 TSciChart,
37 ZoomExtentsModifier,
38 ZoomPanModifier,
39} from "scichart";
40import { appTheme } from "../../../theme";
41
42export type TMessage = {
43 title: string;
44 detail: string;
45};
46
47const axisOptions: INumericAxisOptions = {
48 useNativeText: true,
49 isVisible: false,
50 drawMajorBands: false,
51 drawMinorGridLines: false,
52 drawMinorTickLines: false,
53 drawMajorTickLines: false,
54 drawMajorGridLines: false,
55 labelStyle: { fontSize: 8 },
56 labelFormat: ENumericFormat.Decimal,
57 labelPrecision: 0,
58 autoRange: EAutoRange.Always,
59};
60
61// theme overrides
62const sciChartTheme = appTheme.SciChartJsTheme;
63
64export const drawGridExample = async (
65 rootElement: string | HTMLDivElement,
66 updateMessages: (newMessages: TMessage[]) => void
67) => {
68 const subChartsNumber = 64;
69 const columnsNumber = 8;
70 const rowsNumber = 8;
71
72 const dataSettings = {
73 seriesCount: 3,
74 pointsOnChart: 5000,
75 sendEvery: 16,
76 initialPoints: 20,
77 };
78
79 const originalGetStrokeColor = sciChartTheme.getStrokeColor;
80 let counter = 0;
81 sciChartTheme.getStrokeColor = (index: number, max: number, context: TSciChart) => {
82 const currentIndex = counter % subChartsNumber;
83 counter += 3;
84 return originalGetStrokeColor.call(sciChartTheme, currentIndex, subChartsNumber, context);
85 };
86
87 const originalGetFillColor = sciChartTheme.getFillColor;
88 sciChartTheme.getFillColor = (index: number, max: number, context: TSciChart) => {
89 const currentIndex = counter % subChartsNumber;
90 counter += 3;
91 return originalGetFillColor.call(sciChartTheme, currentIndex, subChartsNumber, context);
92 };
93 ///
94
95 const { wasmContext, sciChartSurface: mainSurface } = await SciChartSurface.createSingle(rootElement, {
96 theme: sciChartTheme,
97 });
98
99 const mainXAxis = new NumericAxis(wasmContext, {
100 isVisible: false,
101 id: "mainXAxis",
102 });
103
104 mainSurface.xAxes.add(mainXAxis);
105 const mainYAxis = new NumericAxis(wasmContext, {
106 isVisible: false,
107 id: "mainYAxis",
108 });
109 mainSurface.yAxes.add(mainYAxis);
110
111 const seriesTypes = [
112 ESeriesType.LineSeries,
113 // ESeriesType.BubbleSeries,
114 //ESeriesType.StackedColumnSeries,
115 ESeriesType.ColumnSeries,
116 //ESeriesType.StackedMountainSeries,
117 ESeriesType.BandSeries,
118 ESeriesType.ScatterSeries,
119 ESeriesType.CandlestickSeries,
120 // ESeriesType.TextSeries
121 ];
122
123 const subChartPositioningCoordinateMode = ESubSurfacePositionCoordinateMode.Relative;
124
125 const subChartsMap: Map<
126 SciChartSubSurface,
127 { seriesType: ESeriesType; dataSeriesType: EDataSeriesType; dataSeriesArray: BaseDataSeries[] }
128 > = new Map();
129
130 const xValues = Array.from(new Array(dataSettings.initialPoints).keys());
131
132 const initSubChart = (seriesType: ESeriesType, subChartIndex: number) => {
133 // calculate sub-chart position and sizes
134 const { rowIndex, columnIndex } = getSubChartPositionIndexes(subChartIndex, columnsNumber);
135 const width = 1 / columnsNumber;
136 const height = 1 / rowsNumber;
137
138 const position = new Rect(columnIndex * width, rowIndex * height, width, height);
139
140 // sub-surface configuration
141 const subChartOptions: I2DSubSurfaceOptions = {
142 id: `subChart-${subChartIndex}`,
143 theme: sciChartTheme,
144 position,
145 parentXAxisId: mainXAxis.id,
146 parentYAxisId: mainYAxis.id,
147 coordinateMode: subChartPositioningCoordinateMode,
148 padding: Thickness.fromNumber(1),
149 viewportBorder: {
150 color: "rgba(150, 74, 148, 0.51)",
151 border: 2,
152 },
153 };
154
155 // create sub-surface
156 const subChartSurface = SciChartSubSurface.createSubSurface(mainSurface, subChartOptions);
157
158 // add axes to the sub-surface
159 const subChartXAxis = new NumericAxis(wasmContext, {
160 ...axisOptions,
161 id: `${subChartSurface.id}-XAxis`,
162 growBy: new NumberRange(0.0, 0.0),
163 useNativeText: true,
164 });
165
166 subChartSurface.xAxes.add(subChartXAxis);
167
168 const subChartYAxis = new NumericAxis(wasmContext, {
169 ...axisOptions,
170 id: `${subChartSurface.id}-YAxis`,
171 growBy: new NumberRange(0.01, 0.1),
172 useNativeText: true,
173 autoRange: EAutoRange.Always,
174 });
175 subChartSurface.yAxes.add(subChartYAxis);
176
177 // add series to sub-surface
178 const dataSeriesArray: BaseDataSeries[] = new Array(dataSettings.seriesCount);
179 const dataSeriesType = getDataSeriesTypeForRenderableSeries(seriesType);
180
181 let stackedCollection: IRenderableSeries;
182 const positive = [ESeriesType.StackedColumnSeries, ESeriesType.StackedMountainSeries].includes(seriesType);
183
184 for (let i = 0; i < dataSettings.seriesCount; i++) {
185 const { dataSeries, rendSeries } = createRenderableSeries(
186 wasmContext,
187 seriesType,
188 subChartXAxis.id,
189 subChartYAxis.id
190 );
191
192 subChartXAxis.visibleRange = new NumberRange(0, dataSeries.count());
193
194 dataSeriesArray[i] = dataSeries;
195
196 // add series to the sub-chart and apply additional configurations per series type
197 if (seriesType === ESeriesType.StackedColumnSeries) {
198 if (i === 0) {
199 stackedCollection = new StackedColumnCollection(wasmContext, {
200 dataPointWidth: 1,
201 xAxisId: subChartXAxis.id,
202 yAxisId: subChartYAxis.id,
203 });
204 subChartSurface.renderableSeries.add(stackedCollection);
205 }
206 (rendSeries as StackedColumnRenderableSeries).stackedGroupId = i.toString();
207 (stackedCollection as StackedColumnCollection).add(rendSeries as StackedColumnRenderableSeries);
208 } else if (seriesType === ESeriesType.StackedMountainSeries) {
209 if (i === 0) {
210 stackedCollection = new StackedMountainCollection(wasmContext, {
211 xAxisId: subChartXAxis.id,
212 yAxisId: subChartYAxis.id,
213 });
214 subChartSurface.renderableSeries.add(stackedCollection);
215 }
216 (stackedCollection as StackedMountainCollection).add(rendSeries as StackedMountainRenderableSeries);
217 } else if (seriesType === ESeriesType.ColumnSeries) {
218 // create Stacked Y Axis
219 if (i === 0) {
220 subChartSurface.layoutManager.rightOuterAxesLayoutStrategy =
221 new RightAlignedOuterVerticallyStackedAxisLayoutStrategy();
222 rendSeries.yAxisId = subChartYAxis.id;
223 } else {
224 const additionalYAxis = new NumericAxis(wasmContext, {
225 ...axisOptions,
226 id: `${subChartSurface.id}-YAxis${i}`,
227 });
228 subChartSurface.yAxes.add(additionalYAxis);
229 rendSeries.yAxisId = additionalYAxis.id;
230 }
231
232 subChartSurface.renderableSeries.add(rendSeries);
233 } else {
234 subChartSurface.renderableSeries.add(rendSeries);
235 }
236
237 // Generate points
238 prePopulateData(dataSeries, dataSeriesType, xValues, positive);
239
240 subChartSurface.zoomExtents(0);
241 }
242
243 subChartsMap.set(subChartSurface, { seriesType, dataSeriesType, dataSeriesArray });
244
245 return positive;
246 };
247
248 // generate the subcharts grid
249 for (let subChartIndex = 0; subChartIndex < subChartsNumber; ++subChartIndex) {
250 const seriesType = seriesTypes[subChartIndex % seriesTypes.length];
251 initSubChart(seriesType, subChartIndex);
252 }
253
254 // setup for realtime updates
255 let isRunning: boolean = false;
256 const newMessages: TMessage[] = [];
257 let loadStart = 0;
258 let avgRenderTime: number = 0;
259
260 let dataGenerationStart: DOMHighResTimeStamp;
261 let dataGenerationEnd: DOMHighResTimeStamp;
262 let dataAppendStart: DOMHighResTimeStamp;
263 let dataAppendEnd: DOMHighResTimeStamp;
264 let renderStart: DOMHighResTimeStamp;
265 let renderEnd: DOMHighResTimeStamp;
266 let lastRenderEnd: DOMHighResTimeStamp;
267
268 let lastPaintEnd: DOMHighResTimeStamp;
269 let paintEnd: DOMHighResTimeStamp;
270
271 const dataStore = new Map(
272 mainSurface.subCharts.map((subChart) => [
273 subChart,
274 Array.from(Array(dataSettings.seriesCount)).map((_: any) => null as any),
275 ])
276 );
277
278 const updateCharts = () => {
279 if (!isRunning) {
280 return;
281 }
282
283 loadStart = new Date().getTime();
284 dataGenerationStart = performance.now();
285 subChartsMap.forEach(({ seriesType, dataSeriesArray, dataSeriesType }, subSurface) => {
286 const pointsToUpdate = Math.round(Math.max(1, dataSeriesArray[0].count() / 50));
287 for (let i = 0; i < dataSettings.seriesCount; i++) {
288 dataStore.get(subSurface)[i] = generateData(
289 seriesType,
290 dataSeriesArray[i],
291 dataSeriesType,
292 i,
293 dataSettings.pointsOnChart,
294 pointsToUpdate
295 );
296 }
297 });
298
299 dataAppendStart = performance.now();
300 subChartsMap.forEach(({ seriesType, dataSeriesArray, dataSeriesType }, subSurface) => {
301 const pointsToUpdate = Math.round(Math.max(1, dataSeriesArray[0].count() / 50));
302 for (let i = 0; i < dataSettings.seriesCount; i++) {
303 const data = dataStore.get(subSurface)[i];
304 appendData(
305 seriesType,
306 dataSeriesArray[i],
307 dataSeriesType,
308 i,
309 dataSettings.pointsOnChart,
310 pointsToUpdate,
311 data
312 );
313 }
314 });
315 dataAppendEnd = performance.now();
316 };
317 mainSurface.preRenderAll.subscribe(() => {
318 renderStart = performance.now();
319 });
320 // mainSurface.painted.subscribe(() => {
321 // lastPaintEnd = paintEnd;
322 // paintEnd = performance.now();
323 // });
324
325 // render time info calculation
326 mainSurface.renderedToDestination.subscribe(() => {
327 if (!isRunning || loadStart === 0) return;
328 lastRenderEnd = renderEnd;
329 renderEnd = performance.now();
330 avgRenderTime = renderEnd - renderStart;
331 const charts = Array.from(subChartsMap.values());
332 const totalPoints = charts[0].dataSeriesArray[0].count() * dataSettings.seriesCount * charts.length;
333
334 // Number of data points on the chart including all of the series on sub-charts
335 newMessages.push({
336 title: `Points`,
337 detail: `${totalPoints}`,
338 });
339
340 // Data points generation time. In a real app data will likely be fetched from a server instead
341 newMessages.push({
342 title: `Generate`,
343 detail: `${(dataAppendStart - dataGenerationStart).toFixed(1)}ms`,
344 });
345
346 // DataSeries collection update time
347 newMessages.push({
348 title: `Append`,
349 detail: `${(dataAppendEnd - dataAppendStart).toFixed(1)}ms`,
350 });
351
352 // SciChart's engine render time of the last frame
353 newMessages.push({
354 title: `Render`,
355 detail: `${avgRenderTime.toFixed(2)}ms`,
356 });
357
358 // // Current FPS value
359 // newMessages.push({
360 // title: `FPS`,
361 // detail: `${(1000 / (renderEnd - lastRenderEnd)).toFixed(1)}`,
362 // });
363
364 // Potentially achievable FPS value considering that data generation step could be omitted on client,
365 // data is sent at a corresponding delay,
366 // and display has an appropriate refresh rate
367 newMessages.push({
368 title: `Max FPS`,
369 detail: `${(1000 / (dataAppendEnd - dataAppendStart + avgRenderTime)).toFixed(1)}`,
370 });
371
372 updateMessages(newMessages);
373 newMessages.length = 0;
374 });
375
376 let timer: NodeJS.Timeout;
377 // Buttons for chart
378 const startUpdate = () => {
379 console.log("start streaming");
380 avgRenderTime = 0;
381 loadStart = 0;
382 isRunning = true;
383 timer = setInterval(updateCharts, dataSettings.sendEvery);
384 };
385
386 const stopUpdate = () => {
387 console.log("stop streaming");
388 isRunning = false;
389 clearInterval(timer);
390 };
391
392 const setLabels = (show: boolean) => {
393 subChartsMap.forEach((v, k) => {
394 k.xAxes.get(0).isVisible = show;
395 k.yAxes.asArray().forEach((y) => (y.isVisible = show));
396 });
397 };
398
399 return {
400 wasmContext,
401 sciChartSurface: mainSurface,
402 controls: {
403 startUpdate,
404 stopUpdate,
405 setLabels,
406 },
407 };
408};
409This example demonstrates how to integrate SciChart.js into a React application by constructing an 8x8 grid of 64 sub-charts that update in real-time. By leveraging functional components and React hooks such as useRef and useState, the example provides a robust framework for building high-performance chart dashboards. For futher details please see What is the SubCharts API? and Using SubCharts to create a Large Dashboard.
The implementation establishes a main SciChartSurface and then dynamically creates multiple sub-surfaces using the SciChart SubCharts API. Each sub-chart is configured via JSON-based options that determine properties like axes, layout positions, and chart padding. React is used to manage the lifecycle of these charts via the <SciChartReact/> component, ensuring proper initialization and cleanup. For more detailed guidance on integrating SciChart.js into React, developers can consult Creating a SciChart React Component from the Ground Up.
Key features include real-time data updates, dynamic sub-chart composition, and interactive modifiers. The charts are updated in real-time using high-frequency data streams, a technique detailed in Adding Realtime Updates | JavaScript Chart Documentation - SciChart. Advanced interactive functionalities such as zooming and panning are provided by modifiers like MouseWheelZoomModifier and ZoomPanModifier, enhancing user engagement and data exploration.
The example adheres to best practices for React integration by employing functional components, state management, and efficient cleanup of event subscriptions. Performance optimization is achieved by minimizing unnecessary re-renders and managing WebGL resources effectively, a strategy that aligns with insights from React Charts with SciChart.js: Introducing “SciChart React”. Additionally, the implementation demonstrates best practices in event handling and cleanup, ensuring stable performance even with high-frequency updates, as discussed in ReactJS Sweep Line: Optimizing SciChartJS Performance, Reusing WasmContext for Multiple Charts.
Overall, the Sub Charts API example in React provides a practical blueprint for building complex, interactive chart dashboards that efficiently handle real-time data streams while maintaining high rendering performance.

Demonstrates a custom modifier which can convert from single chart to grid layout and back.