User Flow with dropouts using D3 Sankey in Angular 10
Tags: angular, javascript, showdev, webdev
Published: 2020-12-17 13:00:20

---
title: User Flow with dropouts using D3 Sankey in Angular 10
published: true
description: This post summarizes the steps to create a user flow/user journey chart showcasing dropouts using the D3 Sankey chart in Angular 10
tags: angular, javascript, showdev, webdev
cover_image: https://res.cloudinary.com/idr/image/upload/q_auto,f_auto,fl_progressive/dev.to/user-flow-angular/user-flow-dropouts_okvqak.png
---

### :scroll: Background
A Sankey chart (diagram) is a visualization used to depict a flow from one set of values to another. The things being connected are called `nodes` and the connections are called `links`. 

One of the use cases of the Sankey chart is to represent the user flow or user journey or screen flow. When it comes to user journey analysis, these diagrams allow identifying at a glance what are the most frequently fired events, in what order, or what are the different paths from action A to action B. These are information that marketers, decision-makers, or your customers are likely to be interested in. Specifically, when representing user journeys, the dropouts are a game-changer.

A `dropout` is basically a node representing the number of users that didn't go to the next screen/action but exited the application (user flow). There are a lot of open-source libraries available to create Sankey charts such as [Google](https://developers.google.com/chart/interactive/docs/gallery/sankey), [Highcharts](https://www.highcharts.com/demo/sankey-diagram), [D3](https://github.com/d3/d3-sankey), etc. But none of them provide a capability to put in a *dropout node* in the flow. I was able to accomplish this using D3's Sankey chart. Let's check out how I achieved this feat :grin:


### :white_check_mark: Prerequisites
- We will assume that you have a basic knowledge of the Angular framework and [D3 Sankey chart](https://github.com/d3/d3-sankey)
- This post only aims to guide with the logic of implementation and hence showcasing only `code snippets`. For overall code implementation, check out my [GitHub repository](https://github.com/idris-rampurawala/ng-d3-sankey).


### :bricks: Integrating D3 with Angular
#### 1. Install D3
D3 is hosted on `npm` so we can easily install it via the `npm` command
`npm install d3`

#### 2. Install D3-sankey
To create Sankey charts in D3, we will have to add `d3-sankey` npm package on top of the D3
`npm install d3-sankey`

#### 3. Import dependencies in Angular
In our `app.component.ts`, we will just import packages as
```ts
import * as d3 from 'd3';
import * as d3Sankey from 'd3-sankey';
```
That's it! We are now ready to move to the next step :grin:


### :hammer_and_pick: Implementing Dropouts in Sankey chart
A Sankey chart consists of two entities to generate the graph:
<table>
<tr>
<td>Node</td>
<td>A rectangular box that represents the actual entity (i.e. in our example, it represents a screen user is visiting)</td>
</tr>
<tr>
<td>Link</td>
<td>It connects two nodes based on their weight</td>
</tr>
</table>

#### 1. Preparing data
- The minimum attributes that are required to create a node are `node (unique id)`, `name`. Here, we will also add one more attribute `drop` :heavy_minus_sign: a number representing the dropouts on this node.
- Similarly, for links, the attributes are `source`, `target`, `value`.
- To represent `dropouts`, we will make a node with `name` as `Dropout` and `drop` as `0`. This node will not have any link that will result in two nodes (node and dropout node) placed adjacent to each other.

Our data structure would look like this:
```js
'nodes': [
      {
        'node': 0, // unique node id
        'name': 'HomeActivity', // name of the node
        'drop': 2 // weight representing if any dropouts from this node
      },
      {
        'node': 1,
        'name': 'Dropout', // we always represent dropouts with this common name
        'drop': 0
      },
...
]
'links': [
      {
        'source': 0, // source node id
        'target': 1, // target node id
        'value': 2 // link weight
      },
      {
        'source': 0,
        'target': 2,
        'value': 2
      },
...
]
```

#### 2. Prepare HTML for rendering
Once we have data generated, it's time to add Sankey chart logic to generate the graph.

Let's consider we have a `div` for plotting sankey
```html
<!-- app.component.html -->
 <div id="sankey"></div>
```

#### 3. Adding Sankey chart rendering logic
Secondly, let's add some initial Sankey chart rendering logic in the `app.component.ts` `ngOnInit` function which gets called on-page init
```ts
// app.component.ts
...

ngOnInit(): void {
  // get some dummy data created in above step
  const chartData = {
    'nodes': [
      {
        'node': 0, // unique node id
        'name': 'HomeActivity', // name of the node
        'drop': 2 
      },
      ...
    ],
    'links': [
      {
        'source': 0,
        'target': 1,
        'value': 2
      }
      ...
    ]
  };
  
  this.drawChart(chartData);
}
...

drawChart(chartData): void {
 // plotting the sankey chart
    const sankey = d3Sankey.sankey()
      .nodeWidth(15)
      .nodePadding(10)
      .nodeAlign(d3Sankey.sankeyLeft)
      .extent([[1, 1], [width, height]]);
    sankey(chartData);

    const iter = d3.nest()
      .key((d: any) => d.x0)
      .sortKeys(d3.ascending)
      .entries(chartData.nodes)
      .map((d: any) => d.key)
      .sort((a: any, b: any) => a - b);
    
    // add svg for graph
    const svg = d3.select('#sankey').append('svg')
      .attr('width', width)
      .attr('height', height)
      .attr('viewbox', `0 0 ${width} ${height}`);
}

```
#### 4. Adding Sankey chart links
Now, let's add links to the Sankey chart (in the same drawChart()). We are gonna exclude the links which end with the Dropout node i.e. the links that have `target` as the Dropout node. This will help us to create a dropout node adjacent to its source node without any link in between.
```ts
// app.component.ts
drawChart(chartData): void {
...
// add in the links (excluding the dropouts, coz it will become node)
    const link = svg.append('g')
      .selectAll('.link')
      .data(chartData.links)
      .enter()
      .filter((l: any) => l.target.name.toLowerCase() !== DROPOUT_NODE_NAME)
      .append('path')
      .attr('d', d3Sankey.sankeyLinkHorizontal()
      )
      .attr('fill', 'none')
      .attr('stroke', '#9e9e9e')
      .style('opacity', '0.7')
      .attr('stroke-width', (d: any) => Math.max(1, d.width))
      .attr('class', 'link')
      .sort((a: any, b: any) => {
        if (a.target.name.toLowerCase() === DROPOUT_NODE_NAME) {
          return -1;
        } else if (b.target.name.toLowerCase() === DROPOUT_NODE_NAME) {
          return 1;
        } else {
          return 0;
        }
      })
      ;

}
```

#### 5. Adding dropout nodes
Let's now plot the dropout nodes. This is the most important step as we plot the dropout nodes here. So how do we achieve this? Well, remember, we left the link which targets the dropout node in the above step? That's where we put in the dropout node (i.e. a rectangle in terms of D3). 

The most important question is how to identify the height of this dropout node? :frowning: It's a little tricky question to solve. Remember, we are plotting dropout at the source node and hence we find the height of all the links on this node, excluding the dropout link (which we have not plotted). So, the dropout node height is 
:monocle_face: `height of source node - the height of all non-dropout links of this node`
```ts
// app.component.ts
drawChart(chartData): void {
...

    // plotting dropout nodes
    const dropLink = svg.append('g')
      .selectAll('.link')
      .data(chartData.links)
      .enter()
      .filter((l: any) => l.target.name.toLowerCase() === DROPOUT_NODE_NAME)
      .append('rect')
      .attr('x', (d: any) => d.source.x1)
      .attr('y', (d: any) => {
        if (d.source.drop > 0) {
          let totalWidth = 0;
          for (const elm of d.source.sourceLinks) {
            if (elm.target.name.toLowerCase() === DROPOUT_NODE_NAME) {
              break;
            } else if (elm.value >= d.source.drop && elm.target.name.toLowerCase() !== DROPOUT_NODE_NAME) {
              totalWidth += elm.width;
            }
          }
          return d.source.y0 + totalWidth;
        } else {
          return d.source.y0;
        }
      })
      .attr('height', (d: any) => Math.abs(d.target.y0 - d.target.y1))
      .attr('width', (d: any) => sankey.nodeWidth() + 3)
      .attr('fill', '#f44336')
      .attr('stroke', '#f44336')
      .attr('class', 'dropout-node')
      .on('click', (l: any) => {
        fnOnDropOutLinkClicked(l);
      });

    dropLink.append('title')
      .text((d: any) => d.source.name + '\n' +
        'Dropouts ' + format(d.value));

    // add the link titles
    link.append('title')
      .text((d: any) => d.source.name + ' → ' +
        d.target.name + '\n' + format(d.value));

}
```

#### 6. Finishing the chart by adding non-dropout nodes
Finally, let's add all the non-dropout nodes
```ts
// app.component.ts
drawChart(chartData): void {
...
    // plotting the nodes
    const node = svg.append('g').selectAll('.node')
      .data(chartData.nodes)
      .enter().append('g')
      .attr('class', 'node')
      .on('mouseover', fade(1))
      .on('mouseout', fade(0.7))
      .on('click', (d) => {
        fnOnNodeClicked(d);
      });

    node.append('rect')
      .filter((d: any) => d.name.toLowerCase() !== DROPOUT_NODE_NAME)
      .attr('x', (d: any) => d.x0)
      .attr('y', (d: any) => d.y0)
      .attr('height', (d: any) => d.y1 - d.y0)
      .attr('width', (d: any) => d.x1 - d.x0)
      .attr('fill', '#2196f3')
      .append('title')
      .text((d: any) => d.name + '\n' + format(d.value));

    node.append('text')
      .filter((d: any) => d.name.toLowerCase() !== DROPOUT_NODE_NAME)
      .attr('x', (d: any) => d.x1 + 20)
      .attr('y', (d: any) => (d.y1 + d.y0) / 2)
      .attr('dy', '0.35em')
      .attr('font-size', 10)
      .attr('font-family', 'Roboto')
      .attr('text-anchor', 'end')
      .text((d: any) => truncateText(d.name, 20))
      .attr('text-anchor', 'start')
      .append('title')
      .text((d: any) => d.name);

}
```

:trophy: Voila! That's all we need to create a dropout node feature in Sankey chart :relieved:

---

:heavy_check_mark: For more features such as, `showing interaction levels`, `node click handler`, `dynamic data update`, etc you can check my [GitHub repository](https://github.com/idris-rampurawala/ng-d3-sankey) or [visit this for a live demo](https://idris-rampurawala.github.io/ng-d3-sankey/).

---

<center>If you like my post do not forget to hit :heart: or :unicorn:</center>

<center>See ya! until my next post :yum:</center>