<!doctype html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<link rel="stylesheet" type="text/css" href="https://cdn.mxpnl.com/libs/mixpanel-platform/css/reset.css"> | |
<link rel="stylesheet" type="text/css" href="https://cdn.mxpnl.com/libs/mixpanel-platform/build/mixpanel-platform.v0.latest.min.css"> | |
<script src="https://cdn.mxpnl.com/libs/mixpanel-platform/build/mixpanel-platform.v0.latest.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script> <!-- Add this line --> | |
<style> | |
svg { | |
background: #fff; | |
} | |
.node { | |
stroke-width: 0; | |
fill-opacity: 0.4; | |
} | |
.node text { | |
text-anchor: middle; | |
alignment-baseline: middle; | |
fill-opacity: 1; | |
} | |
.link { | |
stroke: #999; | |
stroke-opacity: .6; | |
stroke-linecap: round; | |
} | |
</style> | |
</head> | |
<body class="mixpanel-platform-body"> | |
<div class="mixpanel-platform-section"> | |
<div id="eventSelect"></div> | |
</div> | |
<script> | |
var eventsArray = ['Signup']; | |
var nodesMap = {}, nodesArray = []; // stores our node data | |
var linksMap = {}, linksArray = []; // stores our link data | |
var eventSelect = $('#eventSelect').MPEventSelect(); | |
var runQuery = function(events) { | |
console.log('Making a funnel query with events:', events); | |
// Call the funnel endpoint passing events | |
// If events is ['A', 'B', 'C'], this is equivalent to calling | |
// MP.api.funnel('A', 'B', 'C') ... | |
MP.api.funnel.apply(MP.api, events).done(function(funnelData) { | |
// Sum step counts | |
var steps = _.map(funnelData, function(step) { | |
return { | |
event: _.first(_.values(step)).event, | |
count: _.sum(_.pluck(step, 'count')) | |
}; | |
}); | |
// Add nodes | |
_.each(steps, function(step, i) { | |
step = _.clone(step); // copy step so that it isn't modified | |
var event = step.event; | |
var count = step.count; | |
if (_.has(nodesMap, event)) { // if the node already exists check its count | |
// Each node's count should be the max value it has in any funnel | |
nodesMap[event].count = _.max([nodesMap[event].count, count]); | |
} else if (count) { // otherwise create node if it has a non-zero count | |
// add the new node to our data structures | |
nodesMap[event] = step; | |
nodesArray.push(step); | |
} | |
}); | |
// Add links | |
_.each(steps, function(step, i) { | |
var nextStep = steps[i + 1]; | |
// Only create a link if nextStep exists and has a non-zero count | |
if (nextStep && nextStep.count && _.has(nodesMap, nextStep.event)) { | |
// Only create a link if one doesn't exist for these two events yet | |
if (!_.has(linksMap, step.event + '-' + nextStep.event)) { | |
var link = { | |
// link target and source values are set as corresponding nodes | |
source: nodesMap[step.event], | |
target: nodesMap[nextStep.event], | |
count: nextStep.count // link count is the target node count | |
}; | |
// add the new link to our data structures | |
linksMap[step.event + '-' + nextStep.event] = link; | |
linksArray.push(link); | |
} | |
} | |
}); | |
renderGraph(); | |
}); | |
}; | |
// Do an initial query to render the first event node | |
runQuery(eventsArray); | |
// When the event select changes, re-query | |
eventSelect.on('change', function() { | |
var event = eventSelect.MPEventSelect('value'); | |
// Only add events that we aren't already querying on | |
if (!_.contains(eventsArray, event)) { | |
eventsArray.push(event); // add the new event to our events array | |
runQuery(eventsArray); | |
} | |
}); | |
var width = 960; // SVG element width | |
var height = 640; // SVG element height | |
var svg = d3.select('body').append('svg') // the SVG element | |
.attr('width', width) | |
.attr('height', height); | |
var linkElements = svg.selectAll('.link'); // the link elements | |
var nodeElements = svg.selectAll('.node'); // the node elements | |
var colorScale = d3.scale.category20(); // used with colorIndex to select a different color for each node | |
var countScale = d3.scale.linear().range([0, 40]); // maps our link widths to values from 0px to 40px | |
var strengthScale = d3.scale.linear().range([0, 1]); // maps our link strengths to values from 0 to 1 | |
var colorIndex = 0; // incremented to select a different color for each node | |
var nodeIndex = 0; // incremented to be display a funnel step number in node labels | |
// The D3 Force Layout that calculates our node and link positions | |
// See https://github.com/mbostock/d3/wiki/Force-Layout for detailed documentation | |
var forceLayout = d3.layout.force() | |
.nodes(nodesArray) // link our node data with the layout | |
.links(linksArray) // link our link data with the layout | |
.size([width, height]) | |
.charge(-120) // node repulsion/attraction - should always be negative | |
.linkDistance(300) // the distance nodes are placed from each other | |
.linkStrength(function(d) { return strengthScale(d.count); }) // set link strength based on count | |
.on('tick', function() { // a function that runs continuously to animate node and link positions | |
linkElements // update link positions | |
.attr('x1', function(d) { return d.source.x; }) | |
.attr('y1', function(d) { return d.source.y; }) | |
.attr('x2', function(d) { return d.target.x; }) | |
.attr('y2', function(d) { return d.target.y; }); | |
nodeElements // update node positions | |
.attr('cx', function(d) { return d.x; }) | |
.attr('cy', function(d) { return d.y; }); | |
}); | |
function renderGraph() { | |
var maxCount = _.max(_.pluck(nodesArray, 'count')); | |
// update scale domains for the new data | |
countScale.domain([0, maxCount]); | |
strengthScale.domain([0, maxCount]); | |
// our data has changed - rebind layout data to SVG elements | |
nodeElements = nodeElements.data(forceLayout.nodes()); | |
linkElements = linkElements.data(forceLayout.links()); | |
nodeElements.enter() // for any new node data, add corresponding node elements | |
.append('circle') | |
.attr('class', 'node') | |
.attr('r', function(d) { return countScale(d.count); }) // set node circle radius | |
.style('fill', function(d) { return colorScale(++colorIndex); }) // set node color | |
.call(forceLayout.drag); | |
linkElements.enter() // for any new link data, add corresponding link elements | |
.insert('line', ':first-child') // prepend links so that they are always behind nodes | |
.attr('class', 'link') | |
.style('stroke-width', function(d) { return countScale(d.count); }); // set link width based on count | |
// (Re)start the layout - must happen any time node or link data changes | |
// https://github.com/mbostock/d3/wiki/Force-Layout#start | |
forceLayout.start(); | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment