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};
409The Sub Charts API example for Angular demonstrates how to integrate high-performance SciChart.js sub-charts within an Angular application. This example constructs an 8x8 grid of 64 charts that update in real time, providing an interactive dashboard capable of handling large datasets and intensive data streams.
The implementation uses JSON-based configuration settings to dynamically create and manage multiple sub-surfaces with unique axes and renderable series. Each sub-chart is configured with properties such as axis visibility, padding, and viewport borders, set up using SciChart.js API methods. Angular’s dependency injection and lifecycle hooks ensure proper initialization and cleanup of these components. For further details on initial setup and configuration, refer to Getting Started with SciChart JS. Additionally, performance optimizations are achieved by leveraging WebGL rendering techniques and controlled data streaming, as explained in Performance Optimisation of JavaScript Applications & Charts.
Key features of this example include real-time data updates, dynamic component configuration via JSON, and advanced interactive modifiers such as zooming and panning across charts. The example demonstrates how sub-charts can be efficiently updated with new data using Angular event binding and reactive programming techniques. Developers can explore best practices for real-time chart updates in the Adding Realtime Updates | JavaScript Chart Documentation guide.
The example adheres to modular component design principles in Angular, promoting reusability and efficient resource management. Angular’s dependency injection enables seamless integration of external libraries like SciChart.js, while lifecycle hooks facilitate optimal initialization and teardown of charts to avoid memory leaks. For a deeper understanding of Angular change detection strategies, see Deep dive into the OnPush change detection strategy in Angular. Furthermore, the synchronization of axes and interactive behaviors across sub-charts is managed elegantly through shared configurations and can be studied further in How to Link JavaScript Charts and Synchronise zooming, panning ....

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