We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies.

We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies. Less

We use cookies and other tracking technologies... More

Login or register
to apply for this job!

Login or register
to publish this job!

Login or register
to save this job!

Login or register
to save interesting jobs!

Login or register
to get access to all your job applications!

Login or register to start contributing with an article!

Login or register
to see more jobs from this company!

Login or register
to boost this post!

Show some love to the author of this blog by giving their post some rocket fuel 🚀.

Login or register to search for your ideal job!

Login or register to start working on this issue!

Login or register
to save articles!

Engineers who find a new job through WorksHub average a 15% increase in salary 🚀

Blog hero image

User Flow with dropouts using D3 Sankey in Angular 10

Idris Rampurawala 20 January, 2021 | 5 min read

📜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, Highcharts, D3, 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 😄

✅ Prerequisites

  • We will assume that you have a basic knowledge of the Angular framework and D3 Sankey chart
  • 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.

🧱 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

import * as d3 from 'd3';
import * as d3Sankey from 'd3-sankey';

That's it! We are now ready to move to the next step.

⚒️ Implementing Dropouts in Sankey chart

A Sankey chart consists of two entities to generate the graph:

Node A rectangular box that represents the actual entity (i.e. in our example, it represents a screen user is visiting)
Link It connects two nodes based on their weight

Join our newsletter
Join over 111,000 others and get access to exclusive content, job opportunities and more!

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 ➖ 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:

'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

<!-- 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

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

// 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? 😦 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 🧐 height of source node - the height of all non-dropout links of this node

// 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

// 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);

}

🏆 Voila! That's all we need to create a dropout node feature in Sankey chart 😌

✔️ For more features such as, showing interaction levels, node click handler, dynamic data update, etc you can check my GitHub repository or visit this for a live demo.

Originally published on dev.to

    Python
    JavaScript

Related Issues

cosmos / gaia
  • Started
  • 0
  • 4
  • Intermediate
  • Go
cosmos / gaia
  • Started
  • 0
  • 3
  • Intermediate
  • Go
cosmos / ibc
  • Open
  • 0
  • 0
  • Intermediate
  • TeX
cosmos / ibc
cosmos / ibc
  • Started
  • 0
  • 1
  • Intermediate
  • TeX
viebel / klipse-clj
viebel / klipse-clj
  • Started
  • 0
  • 4
  • Intermediate
  • Clojure
viebel / klipse
  • Started
  • 0
  • 1
  • Intermediate
  • Clojure
viebel / klipse
  • 1
  • 2
  • Intermediate
  • Clojure
viebel / klipse
  • Started
  • 0
  • 4
  • Intermediate
  • Clojure
  • $80

Get hired!

Sign up now and apply for roles at companies that interest you.

Engineers who find a new job through WorksHub average a 15% increase in salary.

Start with GitHubStart with Stack OverflowStart with Email