Angular Overview for SubCharts with Range Selection

Demonstrates how to create multiple synchronized subcharts with an interactive overview in an Angular application using SciChart.js

4

Fullscreen

Edit

 Edit

Docs

drawExample.ts

index.tsx

AxisSynchroniser.ts

theme.ts

SubChartsOverviewModifier.ts

Copy to clipboard
Minimise
Fullscreen
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};
296

Overview for SubCharts – Angular

Overview

This 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.

Technical Implementation

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.

Features and Capabilities

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.

Integration and Best Practices

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.

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