Synchronized Charts in Angular Chart
31 Aug 202624 minutes to read
Synchronized charts allow multiple chart instances to share common interactions so that actions in one chart are mirrored in the others. This is useful for comparing related datasets and analyzing trends consistently across multiple visualizations. The following sections describe how to synchronize tooltip, crosshair, zooming, and selection interactions across charts.

Note: Inject the corresponding service (
TooltipService,CrosshairService,ZoomService, orSelectionService) into the componentprovidersfor the feature you want to enable.
Tooltip synchronization
The tooltip can be synchronized across multiple charts using the showTooltip and hideTooltip methods. When you hover over a data point in one chart, call showTooltip on the other charts to display related information simultaneously.
Note: To use tooltip synchronization, inject
TooltipServiceinto the componentproviders.
In the showTooltip method, specify the following parameters programmatically to enable the tooltip for a particular chart:
-
x– Specifies the pointer x-coordinate. -
y– Specifies the pointer y-coordinate.
import { ChartModule } from '@syncfusion/ej2-angular-charts'
import { DateTimeService, AreaSeriesService, LineSeriesService } from '@syncfusion/ej2-angular-charts'
import { TooltipService } from '@syncfusion/ej2-angular-charts'
import { Component, ViewChild, OnInit } from '@angular/core';
import { IMouseEventArgs, ChartComponent } from '@syncfusion/ej2-angular-charts';
import { synchronizedData } from './datasource';
import { Browser } from '@syncfusion/ej2-base';
@Component({
imports: [
ChartModule
],
providers: [ DateTimeService, AreaSeriesService, LineSeriesService, TooltipService ],
standalone: true,
selector: 'app-container',
template: `<div class="control-section">
<div class="row">
<div class="col" >
<ejs-chart #chart1 style='display:block;' id="container1" [primaryXAxis]='primaryXAxis' [primaryYAxis]='primaryYAxis1'
[title]='title1' [titleStyle]="titleStyle" [tooltip]="tooltip1"
(chartMouseLeave)= 'chart1MouseLeave($event)' (chartMouseMove)='chart1MouseMove($event)' (chartMouseUp)='chart1MouseUp($event)'>
<e-series-collection>
<e-series [dataSource]='chartData' type='Line' xName='USD' yName='EUR' [width]="width">
</e-series>
</e-series-collection>
</ejs-chart>
</div>
<div class="col" >
<ejs-chart #chart2 style='display:block;' id="container2" [primaryXAxis]='primaryXAxis' [primaryYAxis]='primaryYAxis2'
[title]='title2' [titleStyle]="titleStyle" [tooltip]="tooltip2"
(chartMouseLeave)= 'chart2MouseLeave($event)' (chartMouseMove)='chart2MouseMove($event)' (chartMouseUp)='chart2MouseUp($event)'>
<e-series-collection>
<e-series [dataSource]='chartData' type='Area' xName='USD' yName='INR' opacity=0.6
[width]="width" [border]='border'>
</e-series>
</e-series-collection>
</ejs-chart>
</div>
</div>
</div>`
})
export class AppComponent implements OnInit {
public primaryXAxis?: Object;
public primaryYAxis1?: Object
public primaryYAxis2?: Object;
public chartData?: Object[];
public title1?: string;
public title2?: string;
public titleStyle?: Object;
public tooltip1?: Object;
public tooltip2?: Object;
public border?: Object;
public width?: number;
@ViewChild('chart1')
public chart1: ChartComponent;
@ViewChild('chart2')
public chart2: ChartComponent;
public chart1MouseLeave(args: IMouseEventArgs): void {
this.chart2.hideTooltip();
};
public chart1MouseMove(args: IMouseEventArgs): void {
if ((!Browser.isDevice && !this.chart1.isTouch && !this.chart1.isChartDrag) || this.chart1.startMove) {
this.chart2.startMove = this.chart1.startMove;
this.chart2.showTooltip(args.x, args.y);
}
};
public chart1MouseUp(args: IMouseEventArgs): void {
if (Browser.isDevice && this.chart1.startMove) {
this.chart2.hideTooltip();
}
};
public chart2MouseLeave(args: IMouseEventArgs): void {
this.chart1.hideTooltip();
};
public chart2MouseMove(args: IMouseEventArgs): void {
if ((!Browser.isDevice && !this.chart2.isTouch && !this.chart2.isChartDrag) || this.chart2.startMove) {
this.chart1.startMove = this.chart2.startMove;
this.chart1.showTooltip(args.x, args.y);
}
};
public chart2MouseUp(args: IMouseEventArgs): void {
if (Browser.isDevice && this.chart2.startMove) {
this.chart1.hideTooltip();
}
};
ngOnInit(): void {
this.chartData = synchronizedData;
this.primaryXAxis = {
minimum: new Date(2023, 1, 18),
maximum: new Date(2023, 7, 18),
valueType: 'DateTime',
labelFormat: 'MMM d',
lineStyle: { width: 0 },
majorGridLines: { width: 0 },
edgeLabelPlacement: Browser.isDevice ? 'None' : 'Shift',
labelRotation: Browser.isDevice ? -45 : 0,
interval: Browser.isDevice ? 2 : 1
};
this.primaryYAxis1 = {
labelFormat: 'n2',
majorTickLines: { width: 0 },
lineStyle: { width: 0 },
minimum: 0.86,
maximum: 0.96,
interval: 0.025
};
this.primaryYAxis2 = {
labelFormat: 'n1',
majorTickLines: { width: 0 },
lineStyle: { width: 0 },
minimum: 79,
maximum: 85,
interval: 1.5
};
this.title1 = 'US to Euro';
this.title2 = 'US to INR';
this.tooltip1 = {
enable: true, fadeOutDuration: Browser.isDevice ? 2500 : 1000, shared: true, header: '', format: '<b>€${point.y}</b><br>${point.x} 2023', enableMarker: false
};
this.tooltip2 = {
enable: true, fadeOutDuration: Browser.isDevice ? 2500 : 1000, shared: true, header: '', format: '<b>₹${point.y}</b><br>${point.x} 2023', enableMarker: false
};
this.border = {
width: 2
};
this.width = 2;
this.titleStyle = {
textAlignment: 'Near'
};
}
}import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import 'zone.js';
bootstrapApplication(AppComponent).catch((err) => console.error(err));Crosshair synchronization
The crosshair can be synchronized across multiple charts using the showCrosshair and hideCrosshair methods. When you hover over one chart, call showCrosshair on the other charts to align them with the active data point. This makes it easier to compare values at the same x-position across charts.
Note: To use crosshair synchronization, inject
CrosshairServiceinto the componentproviders.
In the showCrosshair method, specify the following parameters programmatically to enable the crosshair for a particular chart:
-
x– Specifies the pointer x-coordinate. -
y– Specifies the pointer y-coordinate.
import { ChartModule } from '@syncfusion/ej2-angular-charts'
import { DateTimeService, AreaSeriesService, SplineSeriesService } from '@syncfusion/ej2-angular-charts'
import { CrosshairService } from '@syncfusion/ej2-angular-charts'
import { Component, ViewChild, OnInit } from '@angular/core';
import { IMouseEventArgs, ChartComponent } from '@syncfusion/ej2-angular-charts';
import { synchronizedData } from './datasource';
import { Browser } from '@syncfusion/ej2-base';
@Component({
imports: [
ChartModule
],
providers: [ DateTimeService, AreaSeriesService, SplineSeriesService, CrosshairService],
standalone: true,
selector: 'app-container',
template: `<div class="control-section">
<div class="row">
<div class="col" >
<ejs-chart #chart1 style='display:block;' id="container1" [primaryXAxis]='primaryXAxis' [primaryYAxis]='primaryYAxis1'
[title]='title1' [titleStyle]="titleStyle" [crosshair]='crosshair'
(chartMouseLeave)= 'chart1MouseLeave($event)' (chartMouseMove)='chart1MouseMove($event)' (chartMouseUp)='chart1MouseUp($event)'>
<e-series-collection>
<e-series [dataSource]='chartData' type='Spline' xName='USD' yName='EUR' [width]="width">
</e-series>
</e-series-collection>
</ejs-chart>
</div>
<div class="col" >
<ejs-chart #chart2 style='display:block;' id="container2" [primaryXAxis]='primaryXAxis' [primaryYAxis]='primaryYAxis2'
[title]='title2' [titleStyle]="titleStyle" [crosshair]='crosshair'
(chartMouseLeave)= 'chart2MouseLeave($event)' (chartMouseMove)='chart2MouseMove($event)' (chartMouseUp)='chart2MouseUp($event)'>
<e-series-collection>
<e-series [dataSource]='chartData' type='Area' xName='USD' yName='INR' opacity=0.6
[width]="width" [border]='border'>
</e-series>
</e-series-collection>
</ejs-chart>
</div>
</div>
</div>`
})
export class AppComponent implements OnInit {
public primaryXAxis?: Object;
public primaryYAxis1?: Object
public primaryYAxis2?: Object;
public chartData?: Object[];
public title1?: string;
public title2?: string;
public titleStyle?: Object;
public border?: Object;
public width?: number;
public crosshair?: Object;
@ViewChild('chart1')
public chart1: ChartComponent;
@ViewChild('chart2')
public chart2: ChartComponent;
public chart1MouseLeave(args: IMouseEventArgs): void {
this.chart2.hideCrosshair();
};
public chart1MouseMove(args: IMouseEventArgs): void {
if ((!Browser.isDevice && !this.chart1.isTouch && !this.chart1.isChartDrag) || this.chart1.startMove) {
this.chart2.startMove = this.chart1.startMove;
this.chart2.showCrosshair(args.x, args.y);
}
};
public chart1MouseUp(args: IMouseEventArgs): void {
if (Browser.isDevice && this.chart1.startMove) {
this.chart2.hideCrosshair();
}
};
public chart2MouseLeave(args: IMouseEventArgs): void {
this.chart1.hideCrosshair();
};
public chart2MouseMove(args: IMouseEventArgs): void {
if ((!Browser.isDevice && !this.chart2.isTouch && !this.chart2.isChartDrag) || this.chart2.startMove) {
this.chart1.startMove = this.chart2.startMove;
this.chart1.showCrosshair(args.x, args.y);
}
};
public chart2MouseUp(args: IMouseEventArgs): void {
if (Browser.isDevice && this.chart2.startMove) {
this.chart1.hideCrosshair();
}
};
ngOnInit(): void {
this.chartData = synchronizedData;
this.primaryXAxis = {
minimum: new Date(2023, 1, 18),
maximum: new Date(2023, 7, 18),
valueType: 'DateTime',
labelFormat: 'MMM d',
lineStyle: { width: 0 },
majorGridLines: { width: 0 },
edgeLabelPlacement: Browser.isDevice ? 'None' : 'Shift',
labelRotation: Browser.isDevice ? -45 : 0,
interval: Browser.isDevice ? 2 : 1,
crosshairTooltip: { enable: true },
};
this.primaryYAxis1 = {
labelFormat: 'n2',
majorTickLines: { width: 0 },
lineStyle: { width: 0 },
minimum: 0.86,
maximum: 0.96,
interval: 0.025
};
this.primaryYAxis2 = {
labelFormat: 'n1',
majorTickLines: { width: 0 },
lineStyle: { width: 0 },
minimum: 79,
maximum: 85,
interval: 1.5
};
this.title1 = 'US to Euro';
this.title2 = 'US to INR';
this.border = {
width: 2
};
this.width = 2;
this.titleStyle = {
textAlignment: 'Near'
};
this.crosshair = {
enable: true, lineType: 'Vertical', dashArray: '2,2'
};
}
}import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import 'zone.js';
bootstrapApplication(AppComponent).catch((err) => console.error(err));Zooming synchronization
Zoom levels can be synchronized across multiple charts using the zoomComplete event. In the zoomComplete event, retrieve the currentZoomFactor and currentZoomPosition values from the zoomed chart. Apply these values to the other charts by setting primaryXAxis.zoomFactor and primaryXAxis.zoomPosition so that all synchronized charts maintain the same zoom state during user interaction. You can configure the available zoom operations through the zoomSettings property (for example, enableSelectionZooming and enablePan).
Note: To use zooming synchronization, inject
ZoomServiceinto the componentproviders.
import { ChartModule } from '@syncfusion/ej2-angular-charts'
import { DateTimeService, LineSeriesService, SplineAreaSeriesService } from '@syncfusion/ej2-angular-charts'
import { ZoomService } from '@syncfusion/ej2-angular-charts'
import { Component, ViewChild, OnInit } from '@angular/core';
import { IMouseEventArgs, ChartComponent, IZoomCompleteEventArgs } from '@syncfusion/ej2-angular-charts';
import { synchronizedData } from './datasource';
import { Browser } from '@syncfusion/ej2-base';
@Component({
imports: [
ChartModule
],
providers: [ DateTimeService, LineSeriesService, SplineAreaSeriesService, ZoomService],
standalone: true,
selector: 'app-container',
template: `<div class="control-section">
<div class="row">
<div class="col" >
<ejs-chart #chart1 style='display:block;' id="container1" [primaryXAxis]='primaryXAxis' [primaryYAxis]='primaryYAxis1'
[title]='title1' [titleStyle]="titleStyle" [zoomSettings]='zoomSettings' (zoomComplete)='zoomComplete($event)' (chartMouseLeave)='chart1MouseLeave($event)'>
<e-series-collection>
<e-series [dataSource]='chartData' type='Line' xName='USD' yName='EUR' [width]="width">
</e-series>
</e-series-collection>
</ejs-chart>
</div>
<div class="col" >
<ejs-chart #chart2 style='display:block;' id="container2" [primaryXAxis]='primaryXAxis' [primaryYAxis]='primaryYAxis2'
[title]='title2' [titleStyle]="titleStyle" [zoomSettings]='zoomSettings' (zoomComplete)='zoomComplete($event)'>
<e-series-collection>
<e-series [dataSource]='chartData' type='SplineArea' xName='USD' yName='INR' opacity=0.6
[width]="width" [border]='border'>
</e-series>
</e-series-collection>
</ejs-chart>
</div>
</div>
</div>`
})
export class AppComponent implements OnInit {
public primaryXAxis?: Object;
public primaryYAxis1?: Object
public primaryYAxis2?: Object;
public chartData?: Object[];
public title1?: string;
public title2?: string;
public titleStyle?: Object;
public border?: Object;
public width?: number;
public zoomSettings?: Object;
@ViewChild('chart1')
public chart1!: ChartComponent;
@ViewChild('chart2')
public chart2!: ChartComponent;
public charts: ChartComponent[] = [];
public zoomFactor: number = 0;
public zoomPosition: number = 0;
ngAfterViewInit() {
this.charts = [this.chart1, this.chart2];
}
public chart1MouseLeave(args: IMouseEventArgs): void {
this.chart2.hideTooltip();
};
public zoomComplete(args: IZoomCompleteEventArgs): void {
if (args.axis.name === 'primaryXAxis') {
this.zoomFactor = args.currentZoomFactor;
this.zoomPosition = args.currentZoomPosition;
this.zoomCompleteFunction(args);
}
};
public zoomCompleteFunction(args: any): void {
for (let i: number = 0; i < this.charts.length; i++) {
if (args.axis.series[0].chart.element.id !== this.charts[i].element.id) {
this.charts[i].primaryXAxis.zoomFactor = this.zoomFactor;
this.charts[i].primaryXAxis.zoomPosition = this.zoomPosition;
this.charts[i].zoomModule.isZoomed = args.axis.series[0].chart.zoomModule.isZoomed;
this.charts[i].zoomModule.isPanning = args.axis.series[0].chart.zoomModule.isPanning;
}
}
};
ngOnInit(): void {
this.chartData = synchronizedData;
this.primaryXAxis = {
minimum: new Date(2023, 1, 18),
maximum: new Date(2023, 7, 18),
valueType: 'DateTime',
labelFormat: 'MMM d',
lineStyle: { width: 0 },
majorGridLines: { width: 0 },
edgeLabelPlacement: Browser.isDevice ? 'None' : 'Shift',
labelRotation: Browser.isDevice ? -45 : 0,
interval: Browser.isDevice ? 2 : 1
};
this.primaryYAxis1 = {
labelFormat: 'n2',
majorTickLines: { width: 0 },
lineStyle: { width: 0 },
minimum: 0.86,
maximum: 0.96,
interval: 0.025
};
this.primaryYAxis2 = {
labelFormat: 'n1',
majorTickLines: { width: 0 },
lineStyle: { width: 0 },
minimum: 79,
maximum: 85,
interval: 1.5
};
this.title1 = 'US to Euro';
this.title2 = 'US to INR';
this.zoomSettings = {
enableMouseWheelZooming: true,
enablePinchZooming: true,
enableScrollbar: false,
enableDeferredZooming: false,
enableSelectionZooming: true,
enablePan: true,
mode: 'X',
toolbarItems: ['Pan', 'Reset']
};
this.border = {
width: 2
};
this.width = 2;
this.titleStyle = {
textAlignment: 'Near'
};
}
}import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import 'zone.js';
bootstrapApplication(AppComponent).catch((err) => console.error(err));Selection synchronization
Selection can be synchronized across multiple charts using the selectionComplete event. In the selectionComplete event, retrieve the selected data points from the active chart’s selectedDataValues (each item exposes pointIndex and seriesIndex) and apply the same selection to the other charts by assigning the array to their selectedDataIndexes property, then calling dataBind(). This keeps selection behavior consistent across all connected charts.
Note: To use selection synchronization, inject
SelectionServiceinto the componentproviders. Additionally, enable selection on each chart by setting theselectionModeproperty (for example,Point,DragX, orDragY) and, optionally,selectionPattern(for example,BoxorNone).
import { ChartModule } from '@syncfusion/ej2-angular-charts'
import { DateTimeService, LineSeriesService, SplineSeriesService } from '@syncfusion/ej2-angular-charts'
import { SelectionService, ZoomService } from '@syncfusion/ej2-angular-charts'
import { Component, ViewChild, OnInit } from '@angular/core';
import { IMouseEventArgs, ChartComponent, IZoomCompleteEventArgs, ISelectionCompleteEventArgs } from '@syncfusion/ej2-angular-charts';
import { synchronizedData } from './datasource';
import { Browser } from '@syncfusion/ej2-base';
@Component({
imports: [
ChartModule
],
providers: [ DateTimeService, LineSeriesService, SplineSeriesService, SelectionService, ZoomService],
standalone: true,
selector: 'app-container',
template: `<div class="control-section">
<div class="row">
<div class="col" >
<ejs-chart #chart1 style='display:block;' id="container1" [primaryXAxis]='primaryXAxis' [primaryYAxis]='primaryYAxis1'
[title]='title1' [titleStyle]="titleStyle" [zoomSettings]='zoomSettings' (zoomComplete)='zoomComplete($event)' [selectionMode]="'Point'" [selectionPattern]="'Box'" (selectionComplete)='selectionComplete($event)'>
<e-series-collection>
<e-series [dataSource]='chartData' type='Line' xName='USD' yName='EUR' [width]="width">
</e-series>
</e-series-collection>
</ejs-chart>
</div>
<div class="col" >
<ejs-chart #chart2 style='display:block;' id="container2" [primaryXAxis]='primaryXAxis' [primaryYAxis]='primaryYAxis2'
[title]='title2' [titleStyle]="titleStyle" [zoomSettings]='zoomSettings' (zoomComplete)='zoomComplete($event)' [selectionMode]="'Point'" [selectionPattern]="'Box'" (selectionComplete)='selectionComplete($event)'>
<e-series-collection>
<e-series [dataSource]='chartData' type='Spline' xName='USD' yName='INR'
[width]="width" [border]='border'>
</e-series>
</e-series-collection>
</ejs-chart>
</div>
</div>
</div>`
})
export class AppComponent implements OnInit {
public primaryXAxis?: Object;
public primaryYAxis1?: Object
public primaryYAxis2?: Object;
public chartData?: Object[];
public title1?: string;
public title2?: string;
public titleStyle?: Object;
public border?: Object;
public width?: number;
public zoomSettings?: Object;
@ViewChild('chart1')
public chart1!: ChartComponent;
@ViewChild('chart2')
public chart2!: ChartComponent;
public charts: ChartComponent[] = [];
public zoomFactor: number = 0;
public zoomPosition: number = 0;
public count: number = 0;
ngAfterViewInit() {
this.charts = [this.chart1, this.chart2];
}
public zoomComplete(args: IZoomCompleteEventArgs): void {
if (args.axis.name === 'primaryXAxis') {
this.zoomFactor = args.currentZoomFactor;
this.zoomPosition = args.currentZoomPosition;
this.zoomCompleteFunction(args);
}
};
public zoomCompleteFunction(args: any): void {
for (let i: number = 0; i < this.charts.length; i++) {
if (args.axis.series[0].chart.element.id !== this.charts[i].element.id) {
this.charts[i].primaryXAxis.zoomFactor = this.zoomFactor;
this.charts[i].primaryXAxis.zoomPosition = this.zoomPosition;
this.charts[i].zoomModule.isZoomed = args.axis.series[0].chart.zoomModule.isZoomed;
this.charts[i].zoomModule.isPanning = args.axis.series[0].chart.zoomModule.isPanning;
}
}
};
public selectionComplete(args: ISelectionCompleteEventArgs): void {
this.selectionCompleteFunction(args);
}
public selectionCompleteFunction(args: any): void {
if (this.count == 0) {
for (var j = 0; j < args.selectedDataValues.length; j++) {
args.selectedDataValues[j].point = args.selectedDataValues[j].pointIndex;
args.selectedDataValues[j].series = args.selectedDataValues[j].seriesIndex;
}
for (var i = 0; i < this.charts.length; i++) {
if (args.chart.element.id !== this.charts[i].element.id) {
this.charts[i].selectedDataIndexes = args.selectedDataValues;
this.count += 1;
this.charts[i].dataBind();
}
}
this.count = 0;
}
}
ngOnInit(): void {
this.chartData = synchronizedData;
this.primaryXAxis = {
minimum: new Date(2023, 1, 18),
maximum: new Date(2023, 7, 18),
valueType: 'DateTime',
labelFormat: 'MMM d',
lineStyle: { width: 0 },
majorGridLines: { width: 0 },
edgeLabelPlacement: Browser.isDevice ? 'None' : 'Shift',
labelRotation: Browser.isDevice ? -45 : 0,
interval: Browser.isDevice ? 2 : 1
};
this.primaryYAxis1 = {
labelFormat: 'n2',
majorTickLines: { width: 0 },
lineStyle: { width: 0 },
minimum: 0.86,
maximum: 0.96,
interval: 0.025
};
this.primaryYAxis2 = {
labelFormat: 'n1',
majorTickLines: { width: 0 },
lineStyle: { width: 0 },
minimum: 79,
maximum: 85,
interval: 1.5
};
this.title1 = 'US to Euro';
this.title2 = 'US to INR';
this.zoomSettings = {
enableSelectionZooming: true,
mode: 'X'
};
this.border = {
width: 2
};
this.width = 2;
this.titleStyle = {
textAlignment: 'Near'
};
}
}import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import 'zone.js';
bootstrapApplication(AppComponent).catch((err) => console.error(err));