Tutorials

Tutorial - Multi-funnel network graph

D3 is a data-visualization tool used for rendering your data in the browser using SVG. If you find you want to display data in a way not covered by one of the Mixpanel Platform charts, the combination of Mixpanel Platform and D3 will give you almost limitless flexibility. Check out the D3 homepage for examples and documentation.

We're going to use D3 to create the graph above, which turns data from multiple funnels into a network of events showing the paths users take around your app. This is an example of D3's force-directed layout, an organic approach to arranging nodes in a graph. We'll start with the blank-slate report, so create a new report and choose that as the template. Now you can choose to either use the Mixpanel report editor, or build the app locally following the instructions here - this tutorial will work equally well using either method.

In the code examples below, the line numbers will match up with the line numbers in the current state of the code at the end of each section. So if you want to refer to the full code file to get some context and see where things should go, find the link at the bottom of the current section - this will take you to the full file up to your current point in the tutorial.

The first thing we'll do is set up the basic UI that will let you add events and make funnel queries. Replace the contents of the body tag in your new report with this code:

This funnels query looks a little strange. Instead of just calling MP.api.funnel('eventA', 'eventB') like we normally would, we use MP.api.funnel.apply to call the endpoint with an array: MP.api.funnel.apply(MP.api, ['eventA', 'eventB']). These two styles of calling the api are equivalent. We use the second style so that we can pass the events in our eventsArray as arguments to the endpoint.

You may be wondering what the strange-looking _.contains function on line 38 is. At Mixpanel we use Underscore.js to improve our code readability and consistency. While it's not necessary to use Mixpanel Platform, knowing a few basic Underscore functions will help in understanding this tutorial (and in our opinion, make your JavaScript cleaner). Every time we introduce an Underscore function, we'll add a note explaining what it does.

New Underscore function: _.contains accepts an array and item as input; returns true if the array contains that item.

You'll want to initialize eventsArray with an event you track that you want to start your funnels with (a signup event is a good choice). If had an "Opened app" event, you could use:

var eventsArray = ['Opened app'];

Try running your report to make sure everything is working. You should see output in the console every time you select an event:

D3 uses SVG to draw basic shapes on the page that represent your data. In order to render our visualization, we'll first need to set up the SVG element that will act as a canvas for D3 to draw on. Add this code at the bottom of your report, just before the closing </script> tag:

In addition to adding a DOM element for our SVG and preparing elements for our graph nodes and links, this piece of code also sets up scales that we will use to map our input data values to fixed ranges of color, node radius, and link width:

We'll talk more about the functionality of our scales when we actually use them later. For the impatient, look no further than the D3 documentation.

Here is the current state of the code. Use this as a reference in case you get lost.

Formatting funnel data

Our funnel query is retrieving data, but it's not in a form that D3 can interpret. We need to do some data manipulation - sum counts together, and turn our data into sets of nodes and links that can be rendered as a graph. Add these data structures underneath your eventsArray declaration (line 15):

Now we need to populate our nodesArray and linksArray data structures, by constructing a set of nodes and links from the results of our funnel query. Replace the code inside your MP.api.funnel .done callback with the following:

Finally, we create links between our nodes, and add them to our linksArray. Links have source and target attributes that refer to nodes in our nodesArray. They also have a count attribute that will determine their width.

We're almost there. Now we need D3 to step in and prepare our data for a visual representation. For that, we'll use D3's force layout. A layout in D3 is a formatting tool that turns input data into something that can be rendered to SVG. There are many different layouts for different data visualizations; in our case, the force layout will simply add x/y positions to our nodes. Add this code after your other D3 code, just before the closing </script> tag:

When we run the layout on our nodesArray and linksArray, x and y attributes will be added to the nodes that will be used by the .on('tick', function() { ... above to position our nodeElements and linkElements. Now we just need to add these elements to the SVG container.

Here is the current state of the code. Use this as a reference in case you get lost.

Rendering the graph

What we need now is a function that takes our data and renders an SVG element for each piece of it. The attributes of the SVG element - size, position, color, text - should be set according to the data. This task is exactly what D3 is for. Add this function at bottom of your report, before the closing </script> tag:

A scale in D3 is just a mapping of an input domain to an output range. So if our max count is 4000, and our scale has a range of 0 to 100, 4000 would be mapped to 100, 2000 would be mapped to 50, and 0 would be mapped to 0. This is useful because we usually don't want our data values to directly determine pixel values; we don't want a node circle with a radius of 4000px just because the node count is 4000. In our case, we have a countScale that maps counts to the range 0 to 40 pixels and will be used for node radius and link width. The strength scale maps to the range 0 to 1 and controls the "strength" of links in our layout - how easily they stretch or contract.

The next section is the core of our function. We take the link and node data from the force layout, and bind it to the linkElements and nodeElements. These are called selections, and are basically D3-wrapped sets of SVG elements:

Next, we have code that actually renders SVG elements for our nodes. The enter call on the selection is the key to D3 - it represents each new piece of data, that doesn't yet have an SVG element associated with it. By calling nodeElements.enter().append(), we are creating a new circle SVG element for each node in our data.

The rest of the code here is setting attributes and styles on the node elements, just like jQuery. We set the class as 'node', the cirle radius (the 'r' SVG attribute) as scaled relative to our node's count, and we pick a new color for each SVG 'fill' attribute using our colorScale. The last function call is important:

.call(forceLayout.drag);

This sets up draggable functionality on our nodes, something D3's force layout gives us for free. forceLayout.drag is a function that sets up the draggable behavior on an element, and .call executes that function on each element. With this simple addition, are nodes are draggable with the mouse.

Here's the same type of D3 invocation for adding SVG elements for our links:

The only strange thing here is that we use .insert('line', ':first-child') instead of .append('line') - this prepends the link elements instead of appending them to the SVG container. We do this so that our links are always drawn underneath our nodes. SVG is simple that way; whatever element is placed last in the SVG document is put on top.

Commit your code, run it, and make sure there aren't any errors. If you select a few events, you should see some orbs floating around, but it still isn't a graph. We need to add a little styling before we can see our links.

Add this <style> block at the end of the head tag at the top of your report:

Commit, run, and add some events. Now this is starting to look like a graph! Try dragging nodes around and see how the force layout causes them to behave. However, we're not quite there yet. Our "graph" right now only represents one funnel, since we're only making one funnel query. We want to show multiple funnels as a network of paths between events.

Here is the current state of the code. Use this as a reference in case you get lost.

Querying multiple funnels

What we want to do for a given list of events - A, B, C, D - is query every funnel between A and D: [A, D], [A, B, D], [A, C, D], and [A, B, C, D]. This will create a graph of the various routes users take between events A and D.

In other words, for a given list of events, we want every sublist of those events. We can do this with a recursive function, which you should add at the bottom of your report, before the closing </script> tag:

We also want to wrap each sublist with the first and last event in the funnel, since we're looking at paths between those two events. We'll use the function below to call getSubsets and wrap the output lists (add it below getSubsets, before the closing </script> tag):

Commit and re-run your report. When you add events, you should see their nodes fully connected by links:

Congrats, you've now created a full multi-funnel graph!

Here is the current state of the code. Use this as a reference in case you get lost.

Finishing touches

Our graph looks cool, but it needs one more element before it'll be useful: labels. We have no way of telling which event each node corresponds with, or what number of events each link width represents. Lucky for us, D3 makes the process of adding dynamic labels straightforward. We'll be modifying the renderGraph function:

To add node labels, we'll need to add an extra element to our nodes - an SVG text element. Because nodes will now consist of a <circle> and a <text> element, they'll need to be wrapped in a g, which is an SVG tag for containing a "group" of elements. With this wrapper we'll be able to position our circle and text together.

For each node, this creates a <g> instead of a <circle>, and appends <circle> and <text> elements to it. The only other change we'll need to make is within the .on('tick', function() { ... handler in our forceLayout declaration (line 145):

That's all you need! Commit your work, and take a look at what you've accomplished. You should see something like this:

You've now fully integrated D3 with Mixpanel Platform and have built a fairly complex visualization in the process. D3 has a high learning curve but is a powerful tool especially when used alongside Mixpanel Platform, since any data visualization is only as good as the data that fuels it. For more information on D3, as well as dozens of examples, explore the D3 homepage.

Here is the completed report. Use this as a reference in case you get lost.