JavaScript 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 Example - JavaScript

Overview

This example demonstrates a comprehensive use of the SciChart.js sub-charts API implemented using JavaScript. It creates a grid of 64 sub-charts arranged in an 8x8 layout where each sub-chart displays one or more renderable series (such as Line, Column, Band, Scatter, and Candlestick). The purpose of the example is to illustrate techniques for managing complex multi-chart dashboards with real-time data streaming while maintaining high performance.

Technical Implementation

The implementation starts with creating a main chart using the SciChartSurface API. Each sub-chart is added to the main surface via the addSubChart method and is positioned using relative coordinates calculated by a helper function. You can learn more about this technique in the SubCharts Positioning documentation. Each sub-chart is configured with its own invisible NumericAxis (customized using advanced options available in the Numeric Axis documentation) and different renderable series are added according to the series type. Data for each chart is pre-populated and updated in real time using functions such as prePopulateData and appendData. Real-time data streaming is implemented using iterative updates scheduled with setTimeout, a strategy detailed in the Realtime Updates tutorial. Performance is monitored by subscribing to the render events of a sub-chart, providing insights into average render times and maximum frames per second as described in the Performance Tips & Tricks documentation.

Features and Capabilities

Real-time Updates: The example handles streaming large datasets into multiple data series simultaneously by efficiently appending new data and removing old data when necessary, thus managing FIFO capacity effectively.

Theme Customization: The look and feel of the charts are customized by overriding theme methods (such as getStrokeColor and getFillColor) provided by SciChart.js. Refer to the SciChartJSDarkTheme documentation for more details on theme configuration.

Stacked and Multiple Series Integration: The implementation demonstrates how to integrate different renderable series types within a single sub-chart, including the use of stacked collections like StackedColumnCollection and StackedMountainCollection. This illustrates how multiple series can be synchronized and rendered together.

Sub-chart Layout: The use of helper functions to calculate sub-chart positions ensures that each sub-chart is placed correctly within the overall grid, enabling complex dashboard layouts. Each sub-chart maintains its own axis configuration while still being part of a unified main chart.

Integration and Best Practices

This example follows best practices for high-performance real-time charting applications. By leveraging techniques such as efficient data series management, real-time updates, and performance monitoring, developers can build scalable dashboards. The example’s approach to managing multiple sub-charts in a grid structure serves as a blueprint for creating complex, multi-view dashboards. For additional customization options such as advanced filtering, refer to the Data Series Realtime Updates documentation and the Custom Filters documentation.

Overall, this example provides a solid foundation for developing sophisticated real-time charting solutions using JavaScript with SciChart.js, showcasing effective performance optimization and modular chart configuration.

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