HelpBot Assistant

How can I help you?

Synchronized Charts in EJ2 TypeScript Chart control

3 Feb 202624 minutes to read

Synchronized charts allow multiple chart instances to share common interactions so that actions performed on one chart are reflected across the others. This approach is useful for comparing related datasets and analyzing trends consistently across multiple visualizations.

Tooltip synchronization

The tooltip can be synchronized across multiple charts by using the showTooltip and hideTooltip methods. When the user hovers over a data point in one chart, the showTooltip method can be invoked for the other charts to display the corresponding tooltip information simultaneously.

In the showTooltip method, specify the following parameters programmatically to enable the tooltip for a specific chart:

  • x – The x-value of the data point or the x-coordinate.
  • y – The y-value of the data point or the y-coordinate.
import { Chart, AreaSeries, LineSeries, DateTime, Tooltip, IMouseEventArgs, ITooltipRenderEventArgs, ILegendClickEventArgs } from '@syncfusion/ej2-charts';
Chart.Inject(AreaSeries, LineSeries, DateTime, Tooltip);
import { Browser } from '@syncfusion/ej2-base';
import { synchronizedData } from './datasource.ts';

let charts: Chart[] = [];

let chart: Chart = new Chart({
    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
    },

    primaryYAxis: {
        labelFormat: 'n2',
        majorTickLines: { width: 0 },
        lineStyle: { width: 0 },
        minimum: 0.86,
        maximum: 0.96,
        interval: 0.025
    },
    chartArea: { border: { width: 0 } },

    series: [
        {
            type: 'Line', dataSource: synchronizedData, xName: 'USD', width: 2, yName: 'EUR', emptyPointSettings: { mode: 'Drop' }
        }
    ],
    chartMouseLeave: (args: IMouseEventArgs) => {
        chart1.hideTooltip();
    },
    chartMouseMove: (args: IMouseEventArgs) => {
        if ((!Browser.isDevice && !chart.isTouch && !chart.isChartDrag) || chart.startMove) {
            chart1.startMove = chart.startMove;
            chart1.showTooltip(args.x, args.y);
        }
    },
    chartMouseUp: function (args: IMouseEventArgs) {
        if (Browser.isDevice && chart.startMove) {
            chart1.hideTooltip();
        }
    },
    title: 'US to EURO',
    titleStyle: { textAlignment: 'Near' },
    tooltip: { enable: true, fadeOutDuration: Browser.isDevice ? 2500 : 1000, shared: true, header: '', format: '<b>€${point.y}</b> <br> ${point.x} 2023', enableMarker: false },
});
chart.appendTo('#container1');
charts.push(chart);

let chart1: Chart = new Chart({
    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
    },
    primaryYAxis: {
        labelFormat: 'n1',
        majorTickLines: { width: 0 },
        lineStyle: { width: 0 },
        minimum: 79,
        maximum: 85,
        interval: 1.5
    },
    chartArea: { border: { width: 0 } },
    series: [
        {
            type: 'Area', dataSource: synchronizedData, xName: 'USD', width: 2, yName: 'INR', opacity: 0.6, border: { width: 2 }
        }
    ],
    chartMouseMove: (args: IMouseEventArgs) => {
        if ((!Browser.isDevice && !chart1.isTouch && !chart1.isChartDrag) || chart1.startMove) {
            chart.startMove = chart1.startMove;
            chart.showTooltip(args.x, args.y);
        }
    },
    chartMouseLeave: (args: IMouseEventArgs) => {
        chart.hideTooltip();
    },
    chartMouseUp: function (args: IMouseEventArgs) {
        if (Browser.isDevice || chart1.startMove) {
            chart.hideTooltip();
        }
    },
    title: 'US to INR',
    titleStyle: { textAlignment: 'Near' },
    tooltip: { enable: true, fadeOutDuration: Browser.isDevice ? 2500 : 1000, shared: true, header: '', format: '<b>₹${point.y}</b> <br> ${point.x} 2023', enableMarker: false },
});
chart1.appendTo('#container2');
charts.push(chart1);
<!DOCTYPE html>
<html lang="en">

<head>
    <title>EJ2 Animation</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="Typescript UI Controls" />
    <meta name="author" content="Syncfusion" />
    <link href="index.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.38/system.js"></script>
    <script src="systemjs.config.js"></script>
<script src="https://cdn.syncfusion.com/ej2/syncfusion-helper.js" type ="text/javascript"></script>
</head>
<style>
    #control-container {
        padding: 1px !important;
    }

    .row {
        display: flex;
    }

    .col {
        width: 50%;
        margin: 10px;
        height: 270px;
    }
</style>

<body>
    <div id='loader'>Loading....</div>
    <div class="control-section">
        <div class="row">
            <div class="col" id="container1"></div>
            <div class="col" id="container2"></div>
        </div>
    </div>
</body>

</html>

Crosshair synchronization

The crosshair can be synchronized across multiple charts by using the showCrosshair and hideCrosshair methods. When the user moves the pointer over one chart, calling the showCrosshair method on the other charts aligns the crosshair position across all synchronized charts, making data comparison easier.

In the showCrosshair method, specify the following parameters to render the crosshair for a particular chart:

  • x – Specifies the x-value of the data point or the x-coordinate.
  • y – Specifies the y-value of the data point or the y-coordinate.
import { Chart, AreaSeries, SplineSeries, DateTime, Crosshair, IMouseEventArgs } from '@syncfusion/ej2-charts';
Chart.Inject(AreaSeries, SplineSeries, DateTime, Crosshair);
import { Browser } from '@syncfusion/ej2-base';
import { synchronizedData } from './datasource.ts';

let charts: Chart[] = [];

let chart: Chart = new Chart({
    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 },
    },

    primaryYAxis: {
        labelFormat: 'n2',
        majorTickLines: { width: 0 },
        lineStyle: { width: 0 },
        minimum: 0.86,
        maximum: 0.96,
        interval: 0.025
    },
    chartArea: { border: { width: 0 } },

    series: [
        {
            type: 'Spline', dataSource: synchronizedData, xName: 'USD', width: 2, yName: 'EUR', emptyPointSettings: { mode: 'Drop' }
        }
    ],
    chartMouseLeave: (args: IMouseEventArgs) => {
        chart1.hideCrosshair();
    },
    chartMouseMove: (args: IMouseEventArgs) => {
        if ((!Browser.isDevice && !chart.isTouch && !chart.isChartDrag) || chart.startMove) {
            chart1.startMove = chart.startMove;
            chart1.showCrosshair(args.x, args.y);
        }
    },
    chartMouseUp: function (args: IMouseEventArgs) {
        if (Browser.isDevice && chart.startMove) {
            chart1.hideCrosshair();
        }
    },
    title: 'US to EURO',
    titleStyle: { textAlignment: 'Near' },
    crosshair: { enable: true, lineType: 'Vertical', dashArray: '2,2' }
});
chart.appendTo('#container1');
charts.push(chart);

let chart1: Chart = new Chart({
    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 }
    },
    primaryYAxis: {
        labelFormat: 'n1',
        majorTickLines: { width: 0 },
        lineStyle: { width: 0 },
        minimum: 79,
        maximum: 85,
        interval: 1.5
    },
    chartArea: { border: { width: 0 } },
    series: [
        {
            type: 'Area', dataSource: synchronizedData, xName: 'USD', width: 2, yName: 'INR', opacity: 0.6, border: { width: 2 }
        }
    ],
    chartMouseMove: (args: IMouseEventArgs) => {
        if ((!Browser.isDevice && !chart1.isTouch && !chart1.isChartDrag) || chart1.startMove) {
            chart.startMove = chart1.startMove;
            chart.showCrosshair(args.x, args.y);
        }
    },
    chartMouseLeave: (args: IMouseEventArgs) => {
        chart.hideCrosshair();
    },
    chartMouseUp: function (args: IMouseEventArgs) {
        if (Browser.isDevice || chart1.startMove) {
            chart.hideCrosshair();
        }
    },
    title: 'US to INR',
    titleStyle: { textAlignment: 'Near' },
    crosshair: { enable: true, lineType: 'Vertical', dashArray: '2,2' }
});
chart1.appendTo('#container2');
charts.push(chart1);
<!DOCTYPE html>
<html lang="en">

<head>
    <title>EJ2 Animation</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="Typescript UI Controls" />
    <meta name="author" content="Syncfusion" />
    <link href="index.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.38/system.js"></script>
    <script src="systemjs.config.js"></script>
<script src="https://cdn.syncfusion.com/ej2/syncfusion-helper.js" type ="text/javascript"></script>
</head>
<style>
    #control-container {
        padding: 1px !important;
    }

    .row {
        display: flex;
    }

    .col {
        width: 50%;
        margin: 10px;
        height: 270px;
    }
</style>

<body>
    <div id='loader'>Loading....</div>
    <div class="control-section">
        <div class="row">
            <div class="col" id="container1"></div>
            <div class="col" id="container2"></div>
        </div>
    </div>
</body>

</html>

Zooming synchronization

Zoom levels can be synchronized across multiple charts by using the zoomComplete event. In the zoomComplete event, retrieve the zoomFactor and zoomPosition values from the zoomed chart.

import { Chart, SplineAreaSeries, LineSeries, DateTime, Zoom, ZoomSettings, IZoomCompleteEventArgs, IMouseEventArgs, ITooltipRenderEventArgs, ILegendClickEventArgs } from '@syncfusion/ej2-charts';
Chart.Inject(SplineAreaSeries, LineSeries, DateTime, Zoom);
import { Browser } from '@syncfusion/ej2-base';
import { synchronizedData } from './datasource.ts';
import { Axis } from '@syncfusion/ej2/charts';

let charts: Chart[] = [];
let zoomFactor: number = 0;
let zoomPosition: number = 0;

let chart: Chart = new Chart({
    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
    },

    primaryYAxis: {
        labelFormat: 'n2',
        majorTickLines: { width: 0 },
        lineStyle: { width: 0 },
        minimum: 0.86,
        maximum: 0.96,
        interval: 0.025
    },
    chartArea: { border: { width: 0 } },

    series: [
        {
            type: 'Line', dataSource: synchronizedData, xName: 'USD', width: 2, yName: 'EUR', emptyPointSettings: { mode: 'Drop' }
        }
    ],
    zoomSettings: {
        enableMouseWheelZooming: true,
        enablePinchZooming: true,
        enableScrollbar: false,
        enableDeferredZooming: false,
        enableSelectionZooming: true,
        enablePan: true,
        mode: 'X',
        toolbarItems: ['Pan', 'Reset']
    },
    zoomComplete: (args: IZoomCompleteEventArgs) => {
        if (args.axis.name === 'primaryXAxis') {
            zoomFactor = args.currentZoomFactor;
            zoomPosition = args.currentZoomPosition;
            zoomCompleteFunction(args);
        }
    },
    title: 'US to EURO',
    titleStyle: { textAlignment: 'Near' },
});
chart.appendTo('#container1');
charts.push(chart);

let chart1: Chart = new Chart({
    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
    },
    primaryYAxis: {
        labelFormat: 'n1',
        majorTickLines: { width: 0 },
        lineStyle: { width: 0 },
        minimum: 79,
        maximum: 85,
        interval: 1.5
    },
    chartArea: { border: { width: 0 } },
    series: [
        {
            type: 'SplineArea', dataSource: synchronizedData, xName: 'USD', width: 2, yName: 'INR', opacity: 0.6, border: { width: 2 }
        }
    ],
    zoomSettings: {
        enableMouseWheelZooming: true,
        enablePinchZooming: true,
        enableScrollbar: false,
        enableDeferredZooming: false,
        enableSelectionZooming: true,
        enablePan: true,
        mode: 'X',
        toolbarItems: ['Pan', 'Reset']
    },
    zoomComplete: (args: IZoomCompleteEventArgs) => {
        if (args.axis.name === 'primaryXAxis') {
            zoomFactor = args.currentZoomFactor;
            zoomPosition = args.currentZoomPosition;
            zoomCompleteFunction(args);
        }
    },
    title: 'US to INR',
    titleStyle: { textAlignment: 'Near' },
});
chart1.appendTo('#container2');
charts.push(chart1);
function zoomCompleteFunction(args: IZoomCompleteEventArgs): void {
    for (let i: number = 0; i < charts.length; i++) {
        if ((args.axis as Axis).series[0].chart.element.id !== charts[i].element.id) {
            charts[i].primaryXAxis.zoomFactor = zoomFactor;
            charts[i].primaryXAxis.zoomPosition = zoomPosition;
            charts[i].zoomModule.isZoomed = (args.axis as Axis).series[0].chart.zoomModule.isZoomed;
            charts[i].zoomModule.isPanning = (args.axis as Axis).series[0].chart.zoomModule.isPanning;
        }
    }
}
<!DOCTYPE html>
<html lang="en">

<head>
    <title>EJ2 Animation</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="Typescript UI Controls" />
    <meta name="author" content="Syncfusion" />
    <link href="index.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.38/system.js"></script>
    <script src="systemjs.config.js"></script>
<script src="https://cdn.syncfusion.com/ej2/syncfusion-helper.js" type ="text/javascript"></script>
</head>
<style>
    #control-container {
        padding: 1px !important;
    }

    .row {
        display: flex;
    }

    .col {
        width: 50%;
        margin: 10px;
        height: 270px;
    }
</style>

<body>
    <div id='loader'>Loading....</div>
    <div class="control-section">
        <div class="row">
            <div class="col" id="container1"></div>
            <div class="col" id="container2"></div>
        </div>
    </div>
</body>

</html>

Selection synchronization

Selection can be synchronized across multiple charts by using the selectionComplete event. In the selectionComplete event, retrieve the selected data values or region from the active chart and apply the same selection state to the other charts.

import { Chart, SplineSeries, LineSeries, DateTime, Highlight, Zoom, ZoomSettings, IZoomCompleteEventArgs, Selection, ISelectionCompleteEventArgs, IMouseEventArgs, ITooltipRenderEventArgs, ILegendClickEventArgs } from '@syncfusion/ej2-charts';
Chart.Inject(SplineSeries, LineSeries, DateTime, Zoom, Highlight, Selection);
import { Browser } from '@syncfusion/ej2-base';
import { synchronizedData } from './datasource.ts';
import { Axis } from '@syncfusion/ej2/charts';

let charts: Chart[] = [];
let zoomFactor: number = 0;
let zoomPosition: number = 0;
let count: number = 0;

let chart: Chart = new Chart({
    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
    },

    primaryYAxis: {
        labelFormat: 'n2',
        majorTickLines: { width: 0 },
        lineStyle: { width: 0 },
        minimum: 0.86,
        maximum: 0.96,
        interval: 0.025
    },
    chartArea: { border: { width: 0 } },

    series: [
        {
            type: 'Line', dataSource: synchronizedData, xName: 'USD', width: 2, yName: 'EUR', emptyPointSettings: { mode: 'Drop' }
        }
    ],
    zoomSettings: {
        enableSelectionZooming: true,
        mode: 'X'
    },
    zoomComplete: (args: IZoomCompleteEventArgs) => {
        if (args.axis.name === 'primaryXAxis') {
            zoomFactor = args.currentZoomFactor;
            zoomPosition = args.currentZoomPosition;
            zoomCompleteFunction(args);
        }
    },
    selectionComplete: function (args: ISelectionCompleteEventArgs | any) {
        selectionCompleteFunction(args);
    },
    selectionPattern: 'Box',
    selectionMode: 'Point',
    title: 'US to EURO',
    titleStyle: { textAlignment: 'Near' }
});
chart.appendTo('#container1');
charts.push(chart);

let chart1: Chart = new Chart({
    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
    },
    primaryYAxis: {
        labelFormat: 'n1',
        majorTickLines: { width: 0 },
        lineStyle: { width: 0 },
        minimum: 79,
        maximum: 85,
        interval: 1.5
    },
    chartArea: { border: { width: 0 } },
    series: [
        {
            type: 'Spline', dataSource: synchronizedData, xName: 'USD', width: 2, yName: 'INR', border: { width: 2 }
        }
    ],
    zoomSettings: {
        enableSelectionZooming: true,
        mode: 'X'
    },
    zoomComplete: (args: IZoomCompleteEventArgs) => {
        if (args.axis.name === 'primaryXAxis') {
            zoomFactor = args.currentZoomFactor;
            zoomPosition = args.currentZoomPosition;
            zoomCompleteFunction(args);
        }
    },
    selectionComplete: function (args: ISelectionCompleteEventArgs | any) {
        selectionCompleteFunction(args);
    },
    selectionPattern: 'Box',
    selectionMode: 'Point',
    title: 'US to INR',
    titleStyle: { textAlignment: 'Near' }
});
chart1.appendTo('#container2');
charts.push(chart1);
function zoomCompleteFunction(args: IZoomCompleteEventArgs): void {
    for (let i: number = 0; i < charts.length; i++) {
        if ((args.axis as Axis).series[0].chart.element.id !== charts[i].element.id) {
            charts[i].primaryXAxis.zoomFactor = zoomFactor;
            charts[i].primaryXAxis.zoomPosition = zoomPosition;
            charts[i].zoomModule.isZoomed = (args.axis as Axis).series[0].chart.zoomModule.isZoomed;
            charts[i].zoomModule.isPanning = (args.axis as Axis).series[0].chart.zoomModule.isPanning;
        }
    }
}

function selectionCompleteFunction(args: ISelectionCompleteEventArgs | any): void {
    if (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 < charts.length; i++) {
            if (args.chart.element.id !== charts[i].element.id) {
                charts[i].selectedDataIndexes = args.selectedDataValues;
                count += 1;
                charts[i].dataBind();
            }
        }
        count = 0;
    }
}
<!DOCTYPE html>
<html lang="en">

<head>
    <title>EJ2 Animation</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="Typescript UI Controls" />
    <meta name="author" content="Syncfusion" />
    <link href="index.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.38/system.js"></script>
    <script src="systemjs.config.js"></script>
<script src="https://cdn.syncfusion.com/ej2/syncfusion-helper.js" type ="text/javascript"></script>
</head>
<style>
    #control-container {
        padding: 1px !important;
    }

    .row {
        display: flex;
    }

    .col {
        width: 50%;
        margin: 10px;
        height: 270px;
    }
</style>

<body>
    <div id='loader'>Loading....</div>
    <div class="control-section">
        <div class="row">
            <div class="col" id="container1"></div>
            <div class="col" id="container2"></div>
        </div>
    </div>
</body>

</html>

See Also