Demonstrates how to create multiple synchronized subcharts with an interactive overview using SciChart.js
drawExample.ts
index.html
AxisSynchroniser.ts
vanilla.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 build a multi-panel chart layout in JavaScript using SciChart.js, where several subcharts are vertically stacked and synchronized through a shared interactive overview. The overview chart displays aggregated data from all subcharts and allows users to zoom and pan all charts simultaneously using a draggable range selector.
The chart is initialized by creating a single SciChartSurface, then dynamically adding multiple SciChartSubSurface instances. Each subchart owns its own NumericAxis pair and FastLineRenderableSeries, while an AxisSynchroniser keeps all X-axes aligned. Data is generated using XyDataSeries, simulating phase-shifted sine waves.
An interactive overview is implemented using a custom SubChartsOverviewModifier. This modifier creates an additional subsurface occupying the bottom 20% of the chart, clones renderable series from each subchart, and attaches an OverviewRangeSelectionModifier to control the visible range across all subcharts.
Dynamic SubChart Management: Subcharts can be added, removed, or fully recreated at runtime without triggering interaction issues, using safe lifecycle management with suspendUpdates().
Synchronized Zooming and Panning: Mouse wheel zoom, drag panning, and programmatic zooming are automatically synchronized across all subcharts via a shared X-axis range.
Interactive Overview Panel: The overview aggregates all visible data and provides a draggable selection window that controls the visible range of every subchart simultaneously.
High-Performance Rendering: All charts leverage SciChart.js WebAssembly rendering, ensuring smooth performance even with multiple charts and large datasets.
This example demonstrates best practices for managing multiple subcharts using SciChartSubSurface, including safe cleanup, axis synchronization, and modifier coordination. Developers can extend this pattern to build advanced dashboards, signal analysis tools, or monitoring applications. For more information, see the SubSurfaces API and Performance Tips & Tricks.

Create an interactive JavaScript trading charts for technical analysis. Trading Drawing Tools Demo, which shows how to use Polylines, Extended Lines, Rays, Channels, Pitchforks, Pitchfans, Fibonnaci Retracements, Measure, Stop Loss and Take Profit chart drawing tools for Technical Analysis.

An example of using JavaScript FreehandDrawingModifier for arbitrary drawing on trading and financial charts. Can be used for drawing trends, arrow, markers, text, etc.

Interactive JavaScript 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.

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

Create a Javascript 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 a JavaScript Chart using SciChart.js to create non-linear and custom-scaled axes such as log-like scales