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 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 mainSurface.rendered.subscribe(() => {
297 if (!isRunning || loadStart === 0) return;
298 const reDrawTime = new Date().getTime() - loadStart;
299 avgRenderTime = (avgRenderTime * loadCount + reDrawTime) / (loadCount + 1);
300 const charts = Array.from(subChartsMap.values());
301 const totalPoints = charts[0].dataSeriesArray[0].count() * 3 * charts.length;
302 newMessages.push({
303 title: `Total Points `,
304 detail: `${totalPoints}`,
305 });
306 newMessages.push({
307 title: `Average Render Time `,
308 detail: `${avgRenderTime.toFixed(2)} ms`,
309 });
310 newMessages.push({
311 title: `Max FPS `,
312 detail: `${Math.min(60, 1000 / avgRenderTime).toFixed(1)}`,
313 });
314 updateMessages(newMessages);
315 newMessages.length = 0;
316 });
317
318 // Buttons for chart
319 const startUpdate = () => {
320 console.log("start streaming");
321 loadCount = 0;
322 avgRenderTime = 0;
323 loadStart = 0;
324 isRunning = true;
325 updateCharts();
326 };
327
328 const stopUpdate = () => {
329 console.log("stop streaming");
330 isRunning = false;
331 if (mainSurface.chartModifiers.size() === 0) {
332 mainSurface.chartModifiers.add(
333 new MouseWheelZoomModifier(),
334 new ZoomPanModifier({ enableZoom: true }),
335 new ZoomExtentsModifier()
336 );
337 }
338 };
339
340 const setLabels = (show: boolean) => {
341 subChartsMap.forEach((v, k) => {
342 k.xAxes.get(0).isVisible = show;
343 k.yAxes.asArray().forEach((y) => (y.isVisible = show));
344 });
345 };
346
347 return {
348 wasmContext,
349 sciChartSurface: mainSurface,
350 controls: {
351 startUpdate,
352 stopUpdate,
353 setLabels,
354 },
355 };
356};
357
Demonstrates a custom modifier which can convert from single chart to grid layout and back.