Skip to content

Drill-down Sankey Diagram

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

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

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.


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 }
]
}
]


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",
}
}

Open in CodePen


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.

Open in CodePen

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.



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.

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”.


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.


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!

Open in CodePen