Demonstrates how to create multiple synchronized subcharts with an interactive overview in an Angular application using SciChart.js
drawExample.ts
index.tsx
AxisSynchroniser.ts
theme.ts
SubChartsOverviewModifier.ts
1import {
2 SciChartSurface,
3 NumericAxis,
4 NumberRange,
5 FastLineRenderableSeries,
6 XyDataSeries,
7 ZoomExtentsModifier,
8 MouseWheelZoomModifier,
9 ZoomPanModifier,
10 ESubSurfacePositionCoordinateMode,
11 Rect,
12 EPerformanceMarkType,
13 SciChartSubSurface,
14 I2DSubSurfaceOptions,
15 EAutoRange,
16 EXyDirection,
17 ENumericFormat,
18} from "scichart";
19import { SubChartsOverviewModifier } from "./SubChartsOverviewModifier";
20import { AxisSynchroniser } from "../../MultiChart/SyncMultiChart/AxisSynchroniser";
21
22const STREAM_INTERVAL_MS = 1000;
23const POINTS_ON_CHART = 180;
24const OVERVIEW_HEIGHT = 0.2;
25const SUBCHARTS_HEIGHT = 1 - OVERVIEW_HEIGHT;
26
27export type TMarkType = EPerformanceMarkType | string;
28
29export interface SubChartConfig {
30 id: string;
31 phase: number;
32 color: string;
33 title: string;
34}
35
36export interface SubChartManager {
37 updateSubCharts: (configs: SubChartConfig[]) => void;
38 addSubChart: (config: SubChartConfig) => void;
39 removeSubChart: (id: string) => void;
40 updateLayout: () => void;
41}
42
43interface SubChartRuntime {
44 subChart: SciChartSubSurface;
45 dataSeries: XyDataSeries;
46 phase: number;
47 color: string;
48 title: string;
49}
50
51const getYValue = (x: number, phase: number): number =>
52 Math.sin(x * 0.08 + phase) * 0.75 + Math.cos(x * 0.015 + phase * 0.5) * 0.25;
53
54const createLineData = (phase: number, startX: number, count: number) => {
55 const xValues: number[] = [];
56 const yValues: number[] = [];
57 for (let i = 0; i < count; i++) {
58 const x = startX + i;
59 xValues.push(x);
60 yValues.push(getYValue(x, phase));
61 }
62 return { xValues, yValues };
63};
64
65export const drawExample = async (
66 rootElement: string | HTMLDivElement,
67 initialConfigs: SubChartConfig[]
68): Promise<{ wasmContext: any; sciChartSurface: SciChartSurface; manager: SubChartManager }> => {
69 const { wasmContext, sciChartSurface } = await SciChartSurface.create(rootElement);
70
71 const subChartMap = new Map<string, SciChartSubSurface>();
72 const subChartRuntimeMap = new Map<string, SubChartRuntime>();
73
74 let nextStreamX = Math.floor(Date.now() / 1000) + 1;
75 const initialVisibleRange = new NumberRange(nextStreamX - POINTS_ON_CHART, nextStreamX - 1);
76 const axisSynchroniser = new AxisSynchroniser(initialVisibleRange);
77
78 const mainXAxis = new NumericAxis(wasmContext, {
79 id: "mainXAxis",
80 isVisible: false,
81 autoRange: EAutoRange.Always,
82 });
83 const mainYAxis = new NumericAxis(wasmContext, {
84 id: "mainYAxis",
85 isVisible: false,
86 autoRange: EAutoRange.Always,
87 });
88
89 sciChartSurface.xAxes.add(mainXAxis);
90 sciChartSurface.yAxes.add(mainYAxis);
91
92 const overviewModifier = new SubChartsOverviewModifier({
93 overviewPosition: new Rect(0, SUBCHARTS_HEIGHT, 1, OVERVIEW_HEIGHT),
94 isTransparent: true,
95 axisTitle: "Overview - All Charts",
96 labelStyle: {
97 color: "#ffffff80",
98 fontSize: 10,
99 },
100 majorTickLineStyle: {
101 color: "#ffffff80",
102 tickSize: 6,
103 strokeThickness: 1,
104 },
105 yAxisGrowBy: new NumberRange(0.1, 0.1),
106 xAxisLabelFormat: ENumericFormat.Date_HHMMSS,
107 });
108
109 sciChartSurface.chartModifiers.add(overviewModifier);
110
111 const getCurrentConfigs = (): SubChartConfig[] =>
112 Array.from(subChartRuntimeMap.entries()).map(([id, runtime]) => ({
113 id,
114 phase: runtime.phase,
115 color: runtime.color,
116 title: runtime.title,
117 }));
118
119 const createSubChart = (config: SubChartConfig, rect: Rect) => {
120 const subChartOptions: I2DSubSurfaceOptions = {
121 id: config.id,
122 position: rect,
123 coordinateMode: ESubSurfacePositionCoordinateMode.Relative,
124 };
125
126 const subChart = SciChartSubSurface.createSubSurface(sciChartSurface, subChartOptions);
127
128 // X axis uses Never autorange so user pan/zoom is not overridden by streaming data.
129 // The streaming loop advances visibleRange manually while the user is at the live edge.
130 const subXAxis = new NumericAxis(wasmContext, {
131 autoRange: EAutoRange.Never,
132 visibleRange: axisSynchroniser.visibleRange,
133 labelFormat: ENumericFormat.Date_HHMMSS,
134 cursorLabelFormat: ENumericFormat.Date_HHMMSS,
135 drawMinorGridLines: false,
136 maxAutoTicks: 6,
137 });
138
139 const subYAxis = new NumericAxis(wasmContext, {
140 autoRange: EAutoRange.Always,
141 growBy: new NumberRange(0.1, 0.1),
142 axisTitle: config.title,
143 axisTitleStyle: { fontSize: 14 },
144 drawMinorGridLines: false,
145 });
146
147 subChart.xAxes.add(subXAxis);
148 subChart.yAxes.add(subYAxis);
149
150 const data = createLineData(config.phase, nextStreamX - POINTS_ON_CHART, POINTS_ON_CHART);
151 const dataSeries = new XyDataSeries(wasmContext, {
152 xValues: data.xValues,
153 yValues: data.yValues,
154 dataSeriesName: config.title,
155 });
156
157 const lineSeries = new FastLineRenderableSeries(wasmContext, {
158 dataSeries,
159 strokeThickness: 3,
160 stroke: config.color,
161 opacity: 0.7,
162 });
163
164 axisSynchroniser.addAxis(subXAxis);
165 subChart.renderableSeries.add(lineSeries);
166
167 subChart.chartModifiers.add(
168 new ZoomPanModifier(),
169 new MouseWheelZoomModifier({ xyDirection: EXyDirection.XDirection }),
170 new ZoomExtentsModifier()
171 );
172
173 subChartMap.set(config.id, subChart);
174 subChartRuntimeMap.set(config.id, {
175 subChart,
176 dataSeries,
177 phase: config.phase,
178 color: config.color,
179 title: config.title,
180 });
181 };
182
183 const clearAllSubCharts = () => {
184 const currentSubCharts = Array.from(subChartMap.values());
185 currentSubCharts.forEach((subChart) => {
186 const xAxis = subChart.xAxes.get(0);
187 if (xAxis) {
188 axisSynchroniser.removeAxis(xAxis);
189 }
190 // SciChartSurface.removeSubChart does not fire onDetachSubSurface on modifiers, so
191 // proactively clear the renderable series first. That triggers collectionChanged on
192 // the subchart's series collection, which the overview modifier listens to and uses
193 // to disconnect its shared dataSeries before the subchart deletion deletes them.
194 const seriesToRemove = subChart.renderableSeries.asArray().slice();
195 seriesToRemove.forEach((series) => {
196 subChart.renderableSeries.remove(series, false);
197 });
198 sciChartSurface.removeSubChart(subChart);
199 });
200 subChartMap.clear();
201 subChartRuntimeMap.clear();
202 };
203
204 const recreateSubChartsWithLayout = (configs: SubChartConfig[]) => {
205 sciChartSurface.suspendUpdates();
206 try {
207 clearAllSubCharts();
208
209 if (configs.length === 0) {
210 return;
211 }
212
213 configs.forEach((config, index) => {
214 const yStart = (index / configs.length) * SUBCHARTS_HEIGHT;
215 const height = (1 / configs.length) * SUBCHARTS_HEIGHT;
216 createSubChart(config, new Rect(0, yStart, 1, height));
217 });
218 } finally {
219 sciChartSurface.resumeUpdates();
220 }
221 };
222
223 const updateSubCharts = (configs: SubChartConfig[]) => {
224 recreateSubChartsWithLayout(configs);
225 };
226
227 const addSubChart = (config: SubChartConfig) => {
228 if (subChartMap.has(config.id)) {
229 return;
230 }
231 recreateSubChartsWithLayout([...getCurrentConfigs(), config]);
232 };
233
234 const removeSubChart = (id: string) => {
235 if (!subChartMap.has(id)) {
236 return;
237 }
238 recreateSubChartsWithLayout(getCurrentConfigs().filter((cfg) => cfg.id !== id));
239 };
240
241 const updateLayout = () => {
242 recreateSubChartsWithLayout(getCurrentConfigs());
243 };
244
245 recreateSubChartsWithLayout(initialConfigs);
246 sciChartSurface.zoomExtents();
247
248 const streamInterval = window.setInterval(() => {
249 // Snapshot whether the user was at the live edge before this tick — if so, we advance
250 // the visible range together with the new data so the chart auto-scrolls. If the user
251 // has zoomed/panned away from the live edge, we leave the visible range alone.
252 const previousLastX = nextStreamX - 1;
253 const currentRange = axisSynchroniser.visibleRange;
254 const wasFollowingLive = currentRange != null && currentRange.max >= previousLastX - 0.5;
255
256 subChartRuntimeMap.forEach((runtime) => {
257 runtime.dataSeries.append(nextStreamX, getYValue(nextStreamX, runtime.phase));
258 const excess = runtime.dataSeries.count() - POINTS_ON_CHART;
259 if (excess > 0) {
260 runtime.dataSeries.removeRange(0, excess);
261 }
262 });
263
264 if (wasFollowingLive && currentRange != null) {
265 const newRange = new NumberRange(currentRange.min + 1, currentRange.max + 1);
266 if (subChartRuntimeMap.size > 0) {
267 subChartRuntimeMap.forEach((runtime) => {
268 const xAxis = runtime.subChart.xAxes.get(0);
269 if (xAxis) {
270 xAxis.visibleRange = newRange;
271 }
272 });
273 } else {
274 // No subcharts but keep the synchroniser following live so newly added charts
275 // start at the current live edge.
276 axisSynchroniser.visibleRange = newRange;
277 }
278 }
279
280 nextStreamX += 1;
281 }, STREAM_INTERVAL_MS);
282
283 sciChartSurface.addDeletable({
284 delete: () => window.clearInterval(streamInterval),
285 });
286
287 const manager: SubChartManager = {
288 updateSubCharts,
289 addSubChart,
290 removeSubChart,
291 updateLayout,
292 };
293
294 return { wasmContext, sciChartSurface, manager };
295};
296This example demonstrates how to create a multi-panel chart layout with synchronized subcharts and an overview range selector using SciChart.js within an Angular application. Each subchart displays its own data while remaining fully synchronized through shared zoom and pan interactions.
The chart is hosted inside a standalone Angular component using the scichart-angular integration. A custom initialization function dynamically creates multiple SciChartSubSurface instances, each with its own NumericAxis, FastLineRenderableSeries, and interaction modifiers such as ZoomPanModifier and MouseWheelZoomModifier.
An overview panel is added using a custom SubChartsOverviewModifier, which creates an additional subsurface at the bottom of the chart. This overview aggregates series from all subcharts and applies an OverviewRangeSelectionModifier to synchronize the visible range across every chart.
Dynamic Multi-Chart Layout: Subcharts are automatically positioned and resized based on the number of active charts.
Unified Range Selection: The overview panel provides a single control point for zooming and panning all subcharts simultaneously.
Robust Lifecycle Handling: The example carefully manages subchart creation and deletion to avoid interaction issues, ensuring stable behavior during dynamic updates.
Enterprise-Grade Performance: Leveraging SciChart.js WebAssembly rendering ensures smooth interactivity even with multiple charts and dense datasets.
This example follows recommended patterns for integrating SciChart.js into Angular, including isolating chart initialization logic and using safe update suspension during layout changes. Developers building analytical dashboards or monitoring tools can use this approach as a foundation. See the SciChart Angular Documentation and SubCharts API for further details.



Interactive Angular Smith chart for RF impedance matching — place markers, build matching networks step by step with the component chain, and switch between impedance and admittance grids.

Demonstrates how to use the SVG render layer in SciChart.js to maintain smooth cursor interaction on heavy charts with millions of points.

Angular Force Directed Graph demo by SciChart.js. Visualize network graphs with physics simulation, interactive node dragging, and hover tooltips.

Create an Angular heatmap chart showing historical orderbook levels, using the high performance SciChart.js chart library. Get free demo now.

Demonstrates 64-bit precision Date Axis in SciChart.js handling Nanoseconds to Billions of Years


Demonstrates BaseValue Axes on an Angular Chart using SciChart.js to create non-linear and custom-scaled axes