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.

Synchronized charts preview

Note: Inject the corresponding service (TooltipService, CrosshairService, ZoomService, or SelectionService) into the component providers for 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 TooltipService into the component providers.

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 CrosshairService into the component providers.

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 ZoomService into the component providers.

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 SelectionService into the component providers. Additionally, enable selection on each chart by setting the selectionMode property (for example, Point, DragX, or DragY) and, optionally, selectionPattern (for example, Box or None).

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));