Search code examples
javascripttypescriptamcharts

Keep amcharts series from overlapping


I have a chart setup with three series that each have their own y-axis. Currently they are displaying on either side of the graph, as shown here: Current Graph

This is a little busier than I would like the graph to be, as sometimes the series can mix together. How can I make it so that the y-axes don't overlap, something like the following: Desired Graph

Edit: Here's a working example

/**
 * ---------------------------------------
 * This demo was created using amCharts 4.
 *
 * For more information visit:
 * https://www.amcharts.com/
 *
 * Documentation is available at:
 * https://www.amcharts.com/docs/v4/
 * ---------------------------------------
 */

// Apply chart themes
am4core.useTheme(am4themes_animated);

// Create chart instance
var chart = am4core.create("chartdiv", am4charts.XYChart);
chart.paddingRight = 20;

var dateAxis = chart.xAxes.push(new am4charts.DateAxis());
dateAxis.baseInterval = {
  'timeUnit': 'second',
  'count': 1
};
dateAxis.dateFormats.setKey('minute', 'h:mm a');
dateAxis.tooltipDateFormat = '[bold]MM-dd-yy, h:mm:ss a[/]';
dateAxis.renderer.grid.template.location = 0;

chart.yAxes.push(new am4charts.ValueAxis());
chart.yAxes.push(new am4charts.ValueAxis());
chart.yAxes.push(new am4charts.ValueAxis());

chart.cursor = new am4charts.XYCursor();
chart.legend = new am4charts.Legend();


var data = [
  {name:'value1', value: 2, timestamp: '2018-01-01T15:00:00.00Z'},
  {name:'value2', value: 5, timestamp: '2018-01-01T15:00:00.00Z'},
  {name:'value3', value: 8, timestamp: '2018-01-01T15:00:00.00Z'},
  {name:'value1', value: 4, timestamp: '2018-01-01T15:00:15.00Z'},
  {name:'value2', value: 2, timestamp: '2018-01-01T15:00:15.00Z'},
  {name:'value3', value: 5, timestamp: '2018-01-01T15:00:15.00Z'},
  {name:'value1', value: 9, timestamp: '2018-01-01T15:00:30.00Z'},
  {name:'value2', value: 11, timestamp: '2018-01-01T15:00:30.00Z'},
  {name:'value3', value: 6, timestamp: '2018-01-01T15:00:30.00Z'},
  {name:'value1', value: 4, timestamp: '2018-01-01T15:00:45.00Z'},
  {name:'value2', value: 5, timestamp: '2018-01-01T15:00:45.00Z'},
  {name:'value3', value: 13, timestamp: '2018-01-01T15:00:45.00Z'},
  {name:'value1', value: 1, timestamp: '2018-01-01T15:01:00.00Z'},
  {name:'value2', value: 7, timestamp: '2018-01-01T15:01:00.00Z'},
  {name:'value3', value: 11, timestamp: '2018-01-01T15:01:00.00Z'},
];
var names = ['value1','value2', 'value3'];

names.forEach((name, index) => {
  var opposite = index % 2 == 0;
  var series = this.chart.series.push(new am4charts.LineSeries());
  var yAxis = this.chart.yAxes.getIndex(index);
  series.yAxis = yAxis;
  series.name = name;
  series.dataFields.dateX = 'date';
  series.dataFields.valueY = 'value';
  series.data = data.filter(d => d.name === name).map( d => {
    return {date: new Date(d.timestamp), value: d.value};
  });
  switch (name) {
    case 'value1':
      series.stroke = am4core.color('#9E842F');
      yAxis.minY = 0;
      yAxis.maxY = 31;
      break;
    case 'value2':
      series.stroke = am4core.color('#196D6F');
      yAxis.minY = -20;
      yAxis.maxY = 80;
      break;
    case 'value3':
      series.stroke = am4core.color('#553786');
      yAxis.minY = 0;
      yAxis.maxY = 100;
      break;
  }
  series.tooltip.getFillFromObject = false;
  series.tooltip.background.fill = series.stroke;
  series.tooltipText = '{name}: [bold]{valueY}[/]';

  yAxis.cursorTooltipEnabled = false;
  yAxis.renderer.line.strokeOpacity = 1;
  yAxis.renderer.line.strokeWidth = 2;
  yAxis.renderer.line.stroke = series.stroke;
  yAxis.renderer.labels.template.fill = series.stroke;
  yAxis.renderer.opposite = opposite;
  yAxis.renderer.grid.template.disabled = true;

  var bullet = series.bullets.push(new am4charts.CircleBullet());
  bullet.width = 5;
  bullet.height = 5;
  bullet.fill = series.stroke;
});

var scrollbarX = new am4charts.XYChartScrollbar();
chart.series.values.forEach(s => {
  scrollbarX.series.push(s);
});
chart.scrollbarX = scrollbarX;
chart.scrollbarX.parent = this.chart.bottomAxesContainer;

chart.validateData();

Solution

  • There isn't a way to do this without creating separate charts, however if you create separate charts as sub container objects within the same div, you can sync the cursor and zoom across each instance. There is an example of this right in the github repo where it creates three separate instances and uses events to sync cursors together.

    Demo reproduced below using your data:

    am4core.useTheme(am4themes_animated);
    
    var container = am4core.create("chartdiv", am4core.Container);
    container.width = am4core.percent(100);
    container.height = am4core.percent(100);
    container.layout = "vertical";
    
    
    
    var chartCount = 3;
    var data = [
      {name:'value1', value: 2, timestamp: '2018-01-01T15:00:00.00Z'},
      {name:'value2', value: 5, timestamp: '2018-01-01T15:00:00.00Z'},
      {name:'value3', value: 8, timestamp: '2018-01-01T15:00:00.00Z'},
      {name:'value1', value: 4, timestamp: '2018-01-01T15:00:15.00Z'},
      {name:'value2', value: 2, timestamp: '2018-01-01T15:00:15.00Z'},
      {name:'value3', value: 5, timestamp: '2018-01-01T15:00:15.00Z'},
      {name:'value1', value: 9, timestamp: '2018-01-01T15:00:30.00Z'},
      {name:'value2', value: 11, timestamp: '2018-01-01T15:00:30.00Z'},
      {name:'value3', value: 6, timestamp: '2018-01-01T15:00:30.00Z'},
      {name:'value1', value: 4, timestamp: '2018-01-01T15:00:45.00Z'},
      {name:'value2', value: 5, timestamp: '2018-01-01T15:00:45.00Z'},
      {name:'value3', value: 13, timestamp: '2018-01-01T15:00:45.00Z'},
      {name:'value1', value: 1, timestamp: '2018-01-01T15:01:00.00Z'},
      {name:'value2', value: 7, timestamp: '2018-01-01T15:01:00.00Z'},
      {name:'value3', value: 11, timestamp: '2018-01-01T15:01:00.00Z'},
    ];
    var charts = [];
    var cursorShowDisposers = [];
    
    // create chart instances
    for (var i = 0; i < chartCount; i++) {
      makeChart(data.filter((x) => x.name == 'value' + (i + 1)), 'value' + (i + 1));
    }
    
    let legend = new am4charts.Legend();
    legend.parent = container;
    legend.interactionsEnabled = false;
    legend.data = [{
      "name": "value1",
      "fill":"#9E842F"
    }, {
      "name": "value2",
      "fill": "#196D6F"
    }, {
      "name": "value3",
      "fill": "#553786"
    }];
    
    initCursorListeners();
    
    // after the charts are made, add scrollbar to the first one
    var firstChart = charts[0];
    firstChart.scrollbarX = new am4core.Scrollbar();
    firstChart.zoomOutButton.disabled = false;
    
    // enable date axis labels for the last one
    var lastChart = charts[charts.length - 1];
    var lastDateAxis = lastChart.xAxes.getIndex(0);
    lastDateAxis.renderer.labels.template.disabled = false;
    lastDateAxis.cursorTooltipEnabled = true;
    
    
    
    // create chart
    function makeChart(data, name) {
      var chart = container.createChild(am4charts.XYChart);
      charts.push(chart);
    
      chart.data = data;
      chart.zoomOutButton.disabled = true;
      chart.padding(10, 15, 10, 15);
    
      var dateAxis = chart.xAxes.push(new am4charts.DateAxis());
      dateAxis.renderer.grid.template.location = 0;
      dateAxis.renderer.labels.template.disabled = true;
      dateAxis.tooltip.animationDuration = 0; 
      dateAxis.cursorTooltipEnabled = false;
      dateAxis.dateFormatter.inputDateFormat = 'i';
      dateAxis.baseInterval = {
        'timeUnit': 'second',
        'count': 1
      };
      
      var valueAxis = chart.yAxes.push(new am4charts.ValueAxis());
      valueAxis.tooltip.disabled = true;
      valueAxis.tooltip.disabled = true;
      valueAxis.renderer.minWidth = 60;
      
      var series = chart.series.push(new am4charts.LineSeries());
      series.dataFields.dateX = "timestamp";
      series.dataFields.valueY = "value";
      series.name = name;
      series.interpolationDuration = 0;
    
      switch (name) {
        case 'value1':
          series.stroke = am4core.color('#9E842F');
          valueAxis.minY = 0;
          valueAxis.maxY = 31;
          break;
        case 'value2':
          series.stroke = am4core.color('#196D6F');
          valueAxis.minY = -20;
          valueAxis.maxY = 80;
          break;
        case 'value3':
          series.stroke = am4core.color('#553786');
          valueAxis.minY = 0;
          valueAxis.maxY = 100;
          break;
      }
      series.tooltip.getFillFromObject = false;
      series.tooltip.background.fill = series.stroke;
      series.tooltipText = '{name}: [bold]{valueY}[/]';
    
      var bullet = series.bullets.push(new am4charts.CircleBullet());
      bullet.width = 5;
      bullet.height = 5;
      bullet.fill = series.stroke;
      
      var cursor = new am4charts.XYCursor();
      cursor.lineY.disabled = true;
      cursor.xAxis = dateAxis;
      chart.cursor = cursor;
    
    
      // whenever any of the charts is zoomed, we should zoom all other charts
      dateAxis.events.on("selectionextremeschanged", function (event) {
        syncDateAxes(event.target);
      })
    }
    
    
    function initCursorListeners() {
      cursorShowDisposers = [];
      for (var i = 0; i < charts.length; i++) {
        var chart = charts[i];
        var cursor = chart.cursor;
        cursor.interactionsEnabled = true;
    
        cursorShowDisposers.push(cursor.events.on("shown", function (event) {
          handleShowCursor(event.target);
        }));
      }
    }
    
    var shownCursorChangeDisposer;
    var shownCursorZoomStartedDisposer;
    var shownCursorZoomEndedDisposer;
    
    function handleShowCursor(shownCursor) {
      // disable mouse for all other cursors
      for (var i = 0; i < charts.length; i++) {
        var chart = charts[i];
        var cursor = chart.cursor;
        if (cursor != shownCursor) {
          cursor.interactionsEnabled = false;
        }
        // remove show listener
        cursorShowDisposers[i].dispose();
      }
    
      // add change disposer to the hovered chart cursor
      shownCursorChangeDisposer = shownCursor.lineX.events.on("positionchanged", function (event) {
        syncCursors(shownCursor);
      });
    
    
      shownCursorZoomStartedDisposer = shownCursor.events.on("zoomstarted", function (event) {
    
        for (var i = 0; i < charts.length; i++) {
          var chart = charts[i];
          var cursor = chart.cursor;
          if (cursor != event.target) {
            var point = { x: event.target.point.x, y: 0 };
            cursor.triggerDown(point);
          }
        }
      });
    
      shownCursorZoomEndedDisposer = shownCursor.events.on("zoomended", function (event) {
        for (var i = 0; i < charts.length; i++) {
          var chart = charts[i];
          var cursor = chart.cursor;
          if (cursor != event.target) {
            var point = { x: event.target.point.x, y: 0 };
            cursor.triggerUp(point);
          }
        }
      });
    
    
      shownCursor.events.once("hidden", function (event) {
        shownCursorChangeDisposer.dispose();
        shownCursorZoomStartedDisposer.dispose();
        shownCursorZoomEndedDisposer.dispose();
    
        for (var i = 0; i < charts.length; i++) {
          var chart = charts[i];
          var cursor = chart.cursor;
          cursor.hide(0);
    
          cursorShowDisposers[i].dispose();
        }
    
        initCursorListeners();
      });
    }
    
    function syncCursors(syncWithCursor) {
      for (var i = 0; i < charts.length; i++) {
        var chart = charts[i];
        var cursor = chart.cursor;
    
        var point = { x: syncWithCursor.point.x, y: 0 };
    
        if (cursor != syncWithCursor) {
          cursor.triggerMove(point);
        }
      }
    }
    
    
    function syncDateAxes(syncWithAxis) {
      for (var i = 0; i < charts.length; i++) {
        var chart = charts[i];
        var dateAxis = chart.xAxes.getIndex(0);
        if (dateAxis != syncWithAxis) {
          dateAxis.events.disableType("selectionextremeschanged");
          dateAxis.start = syncWithAxis.start;
          dateAxis.end = syncWithAxis.end;
          dateAxis.events.enableType("selectionextremeschanged");
        }
      }
    }
    ;
    #chartdiv {
      width: 100%;
      height: 700px;
    }
    <script src="//www.amcharts.com/lib/4/core.js"></script>
    <script src="//www.amcharts.com/lib/4/charts.js"></script>
    <script src="//www.amcharts.com/lib/4/themes/animated.js"></script>
    <div id="chartdiv"></div>