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    getDataSeriesTypeForRenderableSeries,
5    getSubChartPositionIndexes,
6    prePopulateData,
7} from "./helpers";
8
9import {
10    BaseDataSeries,
11    EAnnotationLayer,
12    ECoordinateMode,
13    EDataSeriesType,
14    EAutoRange,
15    ENumericFormat,
16    EHorizontalAnchorPoint,
17    EMultiLineAlignment,
18    ESeriesType,
19    IRenderableSeries,
20    INumericAxisOptions,
21    I2DSubSurfaceOptions,
22    MouseWheelZoomModifier,
23    NativeTextAnnotation,
24    NumericAxis,
25    NumberRange,
26    Rect,
27    RightAlignedOuterVerticallyStackedAxisLayoutStrategy,
28    SciChartSubSurface,
29    SciChartSurface,
30    StackedColumnCollection,
31    StackedColumnRenderableSeries,
32    StackedMountainCollection,
33    StackedMountainRenderableSeries,
34    Thickness,
35    TSciChart,
36    ZoomExtentsModifier,
37    ZoomPanModifier,
38} from "scichart";
39import { appTheme } from "../../../theme";
40
41export type TMessage = {
42    title: string;
43    detail: string;
44};
45
46const axisOptions: INumericAxisOptions = {
47    useNativeText: true,
48    isVisible: false,
49    drawMajorBands: false,
50    drawMinorGridLines: false,
51    drawMinorTickLines: false,
52    drawMajorTickLines: false,
53    drawMajorGridLines: false,
54    labelStyle: { fontSize: 8 },
55    labelFormat: ENumericFormat.Decimal,
56    labelPrecision: 0,
57    autoRange: EAutoRange.Always,
58};
59
60// theme overrides
61const sciChartTheme = appTheme.SciChartJsTheme;
62
63export const drawGridExample = async (
64    rootElement: string | HTMLDivElement,
65    updateMessages: (newMessages: TMessage[]) => void
66) => {
67    const subChartsNumber = 64;
68    const columnsNumber = 8;
69    const rowsNumber = 8;
70
71    const dataSettings = {
72        seriesCount: 3,
73        pointsOnChart: 5000,
74        sendEvery: 30,
75        initialPoints: 20,
76    };
77
78    const originalGetStrokeColor = sciChartTheme.getStrokeColor;
79    let counter = 0;
80    sciChartTheme.getStrokeColor = (index: number, max: number, context: TSciChart) => {
81        const currentIndex = counter % subChartsNumber;
82        counter += 3;
83        return originalGetStrokeColor.call(sciChartTheme, currentIndex, subChartsNumber, context);
84    };
85
86    const originalGetFillColor = sciChartTheme.getFillColor;
87    sciChartTheme.getFillColor = (index: number, max: number, context: TSciChart) => {
88        const currentIndex = counter % subChartsNumber;
89        counter += 3;
90        return originalGetFillColor.call(sciChartTheme, currentIndex, subChartsNumber, context);
91    };
92    ///
93
94    const { wasmContext, sciChartSurface: mainSurface } = await SciChartSurface.createSingle(rootElement, {
95        theme: sciChartTheme,
96    });
97
98    const mainXAxis = new NumericAxis(wasmContext, {
99        isVisible: false,
100        id: "mainXAxis",
101    });
102
103    mainSurface.xAxes.add(mainXAxis);
104    const mainYAxis = new NumericAxis(wasmContext, {
105        isVisible: false,
106        id: "mainYAxis",
107    });
108    mainSurface.yAxes.add(mainYAxis);
109
110    const seriesNamesMap: { [key in ESeriesType]?: string } = {
111        [ESeriesType.LineSeries]: "Line",
112        [ESeriesType.StackedColumnSeries]: "Stacked Column",
113        [ESeriesType.ColumnSeries]: "Column",
114        [ESeriesType.StackedMountainSeries]: "Mountain",
115        [ESeriesType.BandSeries]: "Band",
116        [ESeriesType.CandlestickSeries]: "Candle",
117    };
118
119    const seriesTypes = [
120        ESeriesType.LineSeries,
121        // ESeriesType.BubbleSeries,
122        //ESeriesType.StackedColumnSeries,
123        ESeriesType.ColumnSeries,
124        //ESeriesType.StackedMountainSeries,
125        ESeriesType.BandSeries,
126        ESeriesType.ScatterSeries,
127        ESeriesType.CandlestickSeries,
128        // ESeriesType.TextSeries
129    ];
130
131    const subChartPositioningCoordinateMode = ECoordinateMode.Relative;
132
133    const subChartsMap: Map<
134        SciChartSubSurface,
135        { seriesType: ESeriesType; dataSeriesType: EDataSeriesType; dataSeriesArray: BaseDataSeries[] }
136    > = new Map();
137
138    const xValues = Array.from(new Array(dataSettings.initialPoints).keys());
139
140    const initSubChart = (seriesType: ESeriesType, subChartIndex: number) => {
141        // calculate sub-chart position and sizes
142        const { rowIndex, columnIndex } = getSubChartPositionIndexes(subChartIndex, columnsNumber);
143        const width = 1 / columnsNumber;
144        const height = 1 / rowsNumber;
145
146        const position = new Rect(columnIndex * width, rowIndex * height, width, height);
147
148        // sub-surface configuration
149        const subChartOptions: I2DSubSurfaceOptions = {
150            id: `subChart-${subChartIndex}`,
151            theme: sciChartTheme,
152            position,
153            parentXAxisId: mainXAxis.id,
154            parentYAxisId: mainYAxis.id,
155            coordinateMode: subChartPositioningCoordinateMode,
156            subChartPadding: Thickness.fromNumber(1),
157            viewportBorder: {
158                color: "rgba(150, 74, 148, 0.51)",
159                border: 2,
160            },
161            // title: seriesNamesMap[seriesType],
162            titleStyle: {
163                placeWithinChart: true,
164                fontSize: 12,
165                padding: Thickness.fromString("10 4 0 4"),
166                color: appTheme.ForegroundColor,
167            },
168        };
169
170        // create sub-surface
171        const subChartSurface = mainSurface.addSubChart(subChartOptions);
172
173        // add axes to the sub-surface
174        const subChartXAxis = new NumericAxis(wasmContext, {
175            ...axisOptions,
176            id: `${subChartSurface.id}-XAxis`,
177            growBy: new NumberRange(0.0, 0.0),
178            useNativeText: true,
179        });
180
181        subChartSurface.xAxes.add(subChartXAxis);
182
183        const subChartYAxis = new NumericAxis(wasmContext, {
184            ...axisOptions,
185            id: `${subChartSurface.id}-YAxis`,
186            growBy: new NumberRange(0.01, 0.1),
187            useNativeText: true,
188        });
189        subChartSurface.yAxes.add(subChartYAxis);
190
191        // add series to sub-surface
192        const dataSeriesArray: BaseDataSeries[] = new Array(dataSettings.seriesCount);
193        const dataSeriesType = getDataSeriesTypeForRenderableSeries(seriesType);
194
195        let stackedCollection: IRenderableSeries;
196        const positive = [ESeriesType.StackedColumnSeries, ESeriesType.StackedMountainSeries].includes(seriesType);
197
198        for (let i = 0; i < dataSettings.seriesCount; i++) {
199            const { dataSeries, rendSeries } = createRenderableSeries(
200                wasmContext,
201                seriesType,
202                subChartXAxis.id,
203                subChartYAxis.id
204            );
205
206            dataSeriesArray[i] = dataSeries;
207
208            // add series to the sub-chart and apply additional configurations per series type
209            if (seriesType === ESeriesType.StackedColumnSeries) {
210                if (i === 0) {
211                    stackedCollection = new StackedColumnCollection(wasmContext, {
212                        dataPointWidth: 1,
213                        xAxisId: subChartXAxis.id,
214                        yAxisId: subChartYAxis.id,
215                    });
216                    subChartSurface.renderableSeries.add(stackedCollection);
217                }
218                (rendSeries as StackedColumnRenderableSeries).stackedGroupId = i.toString();
219                (stackedCollection as StackedColumnCollection).add(rendSeries as StackedColumnRenderableSeries);
220            } else if (seriesType === ESeriesType.StackedMountainSeries) {
221                if (i === 0) {
222                    stackedCollection = new StackedMountainCollection(wasmContext, {
223                        xAxisId: subChartXAxis.id,
224                        yAxisId: subChartYAxis.id,
225                    });
226                    subChartSurface.renderableSeries.add(stackedCollection);
227                }
228                (stackedCollection as StackedMountainCollection).add(rendSeries as StackedMountainRenderableSeries);
229            } else if (seriesType === ESeriesType.ColumnSeries) {
230                // create Stacked Y Axis
231                if (i === 0) {
232                    subChartSurface.layoutManager.rightOuterAxesLayoutStrategy =
233                        new RightAlignedOuterVerticallyStackedAxisLayoutStrategy();
234                    rendSeries.yAxisId = subChartYAxis.id;
235                } else {
236                    const additionalYAxis = new NumericAxis(wasmContext, {
237                        ...axisOptions,
238                        id: `${subChartSurface.id}-YAxis${i}`,
239                    });
240                    subChartSurface.yAxes.add(additionalYAxis);
241                    rendSeries.yAxisId = additionalYAxis.id;
242                }
243
244                subChartSurface.renderableSeries.add(rendSeries);
245            } else {
246                subChartSurface.renderableSeries.add(rendSeries);
247            }
248
249            // Generate points
250            prePopulateData(dataSeries, dataSeriesType, xValues, positive);
251
252            subChartSurface.zoomExtents(0);
253        }
254
255        subChartsMap.set(subChartSurface, { seriesType, dataSeriesType, dataSeriesArray });
256
257        return positive;
258    };
259
260    // generate the subcharts grid
261    for (let subChartIndex = 0; subChartIndex < subChartsNumber; ++subChartIndex) {
262        const seriesType = seriesTypes[subChartIndex % seriesTypes.length];
263        initSubChart(seriesType, subChartIndex);
264    }
265
266    // setup for realtime updates
267    let isRunning: boolean = false;
268    const newMessages: TMessage[] = [];
269    let loadStart = 0;
270    let loadCount: number = 0;
271    let avgRenderTime: number = 0;
272
273    const updateCharts = () => {
274        if (!isRunning) {
275            return;
276        }
277        loadStart = new Date().getTime();
278        subChartsMap.forEach(({ seriesType, dataSeriesArray, dataSeriesType }) => {
279            for (let i = 0; i < dataSettings.seriesCount; i++) {
280                const pointsToUpdate = Math.round(Math.max(1, dataSeriesArray[i].count() / 50));
281                appendData(
282                    seriesType,
283                    dataSeriesArray[i],
284                    dataSeriesType,
285                    i,
286                    dataSettings.pointsOnChart,
287                    pointsToUpdate
288                );
289            }
290        });
291
292        setTimeout(updateCharts, dataSettings.sendEvery);
293    };
294
295    // render time info calculation
296    const lastSubChart = Array.from(subChartsMap.keys())[subChartsNumber - 1];
297    lastSubChart.rendered.subscribe(() => {
298        if (!isRunning || loadStart === 0) return;
299        const reDrawTime = new Date().getTime() - loadStart;
300        avgRenderTime = (avgRenderTime * loadCount + reDrawTime) / (loadCount + 1);
301        const charts = Array.from(subChartsMap.values());
302        const totalPoints = charts[0].dataSeriesArray[0].count() * 3 * charts.length;
303        newMessages.push({
304            title: `Total Points `,
305            detail: `${totalPoints}`,
306        });
307        newMessages.push({
308            title: `Average Render Time `,
309            detail: `${avgRenderTime.toFixed(2)} ms`,
310        });
311        newMessages.push({
312            title: `Max FPS `,
313            detail: `${Math.min(60, 1000 / avgRenderTime).toFixed(1)}`,
314        });
315        updateMessages(newMessages);
316        newMessages.length = 0;
317    });
318
319    // Buttons for chart
320    const startUpdate = () => {
321        console.log("start streaming");
322        loadCount = 0;
323        avgRenderTime = 0;
324        loadStart = 0;
325        isRunning = true;
326        updateCharts();
327    };
328
329    const stopUpdate = () => {
330        console.log("stop streaming");
331        isRunning = false;
332        if (mainSurface.chartModifiers.size() === 0) {
333            mainSurface.chartModifiers.add(
334                new MouseWheelZoomModifier(),
335                new ZoomPanModifier({ enableZoom: true }),
336                new ZoomExtentsModifier()
337            );
338        }
339    };
340
341    const setLabels = (show: boolean) => {
342        subChartsMap.forEach((v, k) => {
343            k.xAxes.get(0).isVisible = show;
344            k.yAxes.asArray().forEach((y) => (y.isVisible = show));
345        });
346    };
347
348    return {
349        wasmContext,
350        sciChartSurface: mainSurface,
351        controls: {
352            startUpdate,
353            stopUpdate,
354            setLabels,
355        },
356    };
357};
358

See Also: Subcharts API (1 Demo)

Dynamic Layout Showcase | SciChart.js Demo

Dynamic Layout Showcase

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

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