Drill-down Sankey Diagram

This tutorial will walk you through building a multi-level drill-down Sankey Diagram.

Prerequisites

Before we start off, make sure you brush off your knowledge in following topics:

Task

We are going to build a Sankey Diagram. When you click on a link, the chart will update to show a break down of the specific link by showing another Sankey Diagram - drill-down.

We want this support multiple levels of data break down, not just the two levels down.

Finally, we want to show breadcrumb navigation, so that users know how deep and where exactly he/she is now.

Data

Below is a sample data. For convenience, we have the data in a nested structure, where one data item, has a property sub which holds the array of data items for its break down:

[
  {
    from: "A",
    to: "D",
    value: 10,
    sub: [
      { from: "A1", to: "D1", value: 2 },
      {
        from: "A2",
        to: "D2",
        value: 5,
        sub: [
          { from: "A1-1", to: "D1-1", value: 1 },
          { from: "A1-1", to: "D1-2", value: 1 },
          { from: "A1-2", to: "D1-2", value: 2 },
          { from: "A1-2", to: "D1-3", value: 1 }
        ]
      },
      { from: "A3", to: "D2", value: 3 }
    ]
  },
  {
    from: "B",
    to: "D",
    value: 8,
    sub: [
      { from: "B1", to: "D1", value: 2 },
      { from: "B2", to: "D2", value: 2 },
      { from: "B3", to: "D2", value: 3 },
      { from: "B4", to: "D2", value: 1 }
    ]
  },
  {
    from: "B",
    to: "E",
    value: 4,
    sub: [
      { from: "B1", to: "E1", value: 2 },
      { from: "B2", to: "E2", value: 1 },
      { from: "B2", to: "E3", value: 1 }
    ]
  },
  {
    from: "C",
    to: "E",
    value: 3,
    sub: [
      { from: "C1", to: "E1", value: 2 },
      { from: "C1", to: "E2", value: 1 }
    ]
  }
]

Base chart

Now let's build simple Sankey Diagram using the data above. Even though the chart itself does not directly support multi-level data, we can still use it to feed the chart. The sub-items will be ignored.

let chart = am4core.create("chartdiv", am4charts.SankeyDiagram);
chart.data = data;
chart.dataFields.fromName = "from";
chart.dataFields.toName = "to";
chart.dataFields.value = "value";
var chart = am4core.create("chartdiv", am4charts.SankeyDiagram);
chart.data = data;
chart.dataFields.fromName = "from";
chart.dataFields.toName = "to";
chart.dataFields.value = "value";
{
  "data": data,
  "dataFields": {
    "fromName": "from",
    "toName": "to",
    "value": "value",
  }
}

See the Pen amCharts 4: Sankey drill-down by amCharts (@amcharts) on CodePen.24419

Implementing drill-down

Now that we have our base chart and data we can start implementing drill-down.

Since we're going to be drilling down by clicking on a Link, we are going to be using Link template to add "hit" event on.

chart.links.template.cursorOverStyle = am4core.MouseCursorStyle.pointer;
chart.links.template.events.on("hit", function(ev) {
  chart.colors.reset();
  let linkData = ev.target.dataItem.dataContext;
  if (linkData.sub) {
    chart.data = linkData.sub;
  }
});
chart.links.template.cursorOverStyle = am4core.MouseCursorStyle.pointer;
chart.links.template.events.on("hit", function(ev) {
  chart.colors.reset();
  var linkData = ev.target.dataItem.dataContext;
  if (linkData.sub) {
    chart.data = linkData.sub;
  }
});
{
  // ...
  "links": {
    "cursorOverStyle": "pointer",
    "events": {
      "hit": function(ev) {
        chart.colors.reset();
        var linkData = ev.target.dataItem.dataContext;
        if (linkData.sub) {
          chart.data = linkData.sub;
        }
      }
    }
  }
}

Once user hits a link, and our custom function kicks in, it checks ev.target which holds reference to the SankeyLink object just clicked. It's dataItem.dataContext will hold a reference to original data from our source data object that was holding data for this specific link, including sub property.

If the sub property is in Link's data, we know we want to drill-down deepter into it. For that we simply replace our chart's data with the sub data.

And voilà - our flat chart just graduated to a multi-level drill-down-capable one.

See the Pen amCharts 4: Sankey drill-down (2) by amCharts (@amcharts) on CodePen.24419

NOTE Notice how we are also setting Link template's cursorOverStyle to a pointer. This will inform users that something is clickable when they hover their mouse cursor over the links.

ANOTHER NOTE The following code chart.colors.reset(); helps us reset automatic color generator so that every time we update chart's data, we do not get new colors.

Implementing breadcrumb nav

Adding the nav

Now that we have implemented drilling down, we need some kind of way to navigate back.

For that, amCharts 4 has a built in simple breadcrumb control, called NavigationBar.

We're just going to instantiate a new Navigation Bar object and push it into chart's chartContainer.

MORE INFO This might be a good time to take a break and look at how Container concept works in amCharts 4. To do so, head over to our "Working with Containers" article.

let nav = chart.createChild(am4charts.NavigationBar);
nav.data = [{ name: "Home" }];
nav.toBack();
var nav = chart.createChild(am4charts.NavigationBar);
nav.data = [{ name: "Home" }];
nav.toBack();
{
  // ...
  "children": [{
    "type": "NavigationBar",
    "data": [{ name: "Home" }]
  }]
}

The data for Navigation Bar is pretty straightforward: it's an array of objects with at least one property "name". The name will be used as a link in a breadcrumb text.

As you can see we preset our chart with the first step - "Home".

NOTE Notice the toBack() call? It moves the Nav to the beginning of the chart's Container's list of children, so it is is drawn first - at the very top. Unfortunately, this is not yet available in JSON-based configs.

Updating nav on drill-down

We will not have to update our drill-down functions to update the Nav with each step when we go down the data hierarchy. For that, we just need to update Nav's data:

chart.links.template.cursorOverStyle = am4core.MouseCursorStyle.pointer;
chart.links.template.events.on("hit", function(ev) {
  chart.colors.reset();
  let linkData = ev.target.dataItem.dataContext;
  let nav = chart.children.getIndex(0);
  if (linkData.sub) {
    chart.data = linkData.sub;
    nav.data.push({
      name: ev.target.populateString("{fromName}→{toName}"),
      step: ev.target.dataItem.dataContext
    });
    nav.invalidateData();
  }
});
chart.links.template.cursorOverStyle = am4core.MouseCursorStyle.pointer;
chart.links.template.events.on("hit", function(ev) {
  chart.colors.reset();
  var nav = chart.children.getIndex(0);
  var linkData = ev.target.dataItem.dataContext;
  if (linkData.sub) {
    chart.data = linkData.sub;
    nav.data.push({
      name: ev.target.populateString("{fromName}→{toName}"),
      step: ev.target.dataItem.dataContext
    });
    nav.invalidateData();
  }
});
{
  // ...
  "links": {
    "cursorOverStyle": "pointer",
    "events": {
      "hit": function(ev) {
        chart.colors.reset();
        var linkData = ev.target.dataItem.dataContext;
        if (linkData.sub) {
          chart.data = linkData.sub;
          nav.data.push({
            name: ev.target.populateString("{fromName}→{toName}"),
            step: ev.target.dataItem.dataContext
          });
          nav.invalidateData();
        }
      }
    }
  }
}

NOTE If we were replacing the whole data for the Nav, we would not have to do anything else, since it would automatically rebuild itself. However, Nav can't catch minor changes in existing array, hence our need to call invalidateData() manually.

Adding nav functionality

Having the Nav helps users identify their current position in data. However, without ability to go back levels, it's not much useful.

We'll need to add another "hit" event: this time to Nav's links.template that would handle navigation:

nav.links.template.events.on("hit", function(ev) {
  let target = ev.target.dataItem.dataContext;
  let nav = ev.target.parent;
  chart.colors.reset();
  if (target.step) {
    chart.data = target.step.sub;
    nav.data.splice(nav.data.indexOf(target) + 1);
    nav.invalidateData();
  } else {
    chart.data = data;
    nav.data = [{ name: "Home" }];
  }
});
nav.links.template.events.on("hit", function(ev) {
  var target = ev.target.dataItem.dataContext;
  var nav = ev.target.parent;
  chart.colors.reset();
  if (target.step) {
    chart.data = target.step.sub;
    nav.data.splice(nav.data.indexOf(target) + 1);
    nav.invalidateData();
  } else {
    chart.data = data;
    nav.data = [{ name: "Home" }];
  }
});
{
  // ...
  "children": [{
    "type": "NavigationBar",
    "data": [{ name: "Home" }],
    "links": {
      "events": {
        "hit": function(ev) {
          var target = ev.target.dataItem.dataContext;
          var nav = ev.target.parent;
          chart.colors.reset();
          if (target.step) {
            chart.data = target.step.sub;
            nav.data.splice(nav.data.indexOf(target) + 1);
            nav.invalidateData();
          } else {
            chart.data = data;
            nav.data = [{ name: "Home" }];
          }
        }
      }
    }
  }]
}

Mission accomplished!

See the Pen amCharts 4: Sankey drill-down (3) by amCharts (@amcharts) on CodePen.24419