React 64-Chart Dashboard Performance Demo

Using the SubCharts API as part of SciChart.js, this demo showcases an 8x8 grid of 64 charts updating in realtime in JavaScript.

Fullscreen

Edit

 Edit

Docs

drawExample.ts

index.tsx

theme.ts

helpers.ts

Copy to clipboard
Minimise
Fullscreen
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};
409

Sub Charts API - React

Overview

This 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.

Technical Implementation

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.

Features and Capabilities

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.

Integration and Best Practices

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.

SciChart Ltd, 16 Beaufort Court, Admirals Way, Docklands, London, E14 9XL.