<!DOCTYPE html>
<html ng-app="Sunburst">
<head>
<meta charset="utf-8">
<title>D3 Sunburst Sequence</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.1/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.2.0/css/font-awesome.min.css" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:400,600">
<link rel="stylesheet" href="style.css" />
</head>
<body class="container">
<!-- header -->
<header class="page-header">
<h1>D3 Sunburst Sequence</h1>
<p class="text-small">Interactive sunburst visualization of sequential data changes.</p>
</header>
<!-- main content -->
<div class="main" ng-controller="MainCtrl as sunburst">
<!-- visualization -->
<h2>Visualization</h2>
<p>Hover for data summary, click on visualization to reset summary.</p>
<p>Select example: <select ng-options="example for example in sunburst.examples" ng-model="sunburst.exampleSelected" ng-change="sunburst.selectExample(sunburst.exampleSelected)"></select></p>
<div class="visualization">
<sunburst data="sunburst.data"></sunburst>
</div>
<p>A sunburst visualization helps track population changes from initial states over lifecycles e.g. product churn rates, product conversions.</p>
<p>For custom testing, load up a file conforming to the data schema (see details below) or you can test out the following sample files (fake data):</p>
<ul>
<li><a href="data_android_os_conversion.csv" target="_blank">Android OS Conversions</a>
</li>
<li><a href="data_netflix_churn.csv" target="_blank">Netflix Churn</a>
</li>
<li><a href="data_page_clicks.csv" target="_blank">Page Clicks</a>
</li>
</ul>
<input id="fileUpload" type="file" on-read-file="sunburst.getData($fileContent)" />
<hr />
<!-- details -->
<div class="Details">
<h2>Details</h2>
<p>
This is a variation of the original <a href="http://bl.ocks.org/kerryrodden/7090426" target="_blank">sunburst sequence</a>. A major improvement to the original vis is to organize the code base and draw the D3 components (breadcrumbs, sunburst,
legend) from a single HTML <code>div</code> tag, and to dynamically assign color and legend scales.
</p>
<p>
The other improvement is generalizing and conventionalizing data inputs. The input requires a simple tabular schema of <code>sequence, stage, node, value</code> (see below) and the program will parse the data into a JSON graph.</p>
<p>The design of the data input therefore makes the visualization more useable on relational database queries. The CSV data can be unsorted but it must <em>NOT</em> contain a header, and has to conform to the following data column requirements.
</p>
<ul>
<li><code>sequence (int/string):</code> an ordered sequence that clearly defines the grouping of nodes.</li>
<li><code>stage (int): </code>the index/order of nodes in a given sequence.</li>
<li><code>node (int/string): </code>the data name of the node.</li>
<li><code>value (int): </code>the value at each stage of a given sequence. Only the final stage value in a given sequence is used in this visualization.</li>
</ul>
<hr />
</div>
<!-- data/file preview -->
<div class="preview">
<h2>Data</h2>
<pre>{{ sunburst.data }}</pre>
</div>
</div>
<!-- footer -->
<footer>
<p><a href="https://gist.github.com/chrisrzhou/d5bdd8546f64ca0e4366" target="_blank">D3 Sunburst Sequence</a> by chrisrzhou, 2014-12-29
<br />
<a href="http://github.com/chrisrzhou" target="_blank"><i class="fa fa-github"></i></a> |
<a href="http://bl.ocks.org/chrisrzhou" target="_blank"><i class="fa fa-th"></i></a> |
<a href="http://www.linkedin.com/in/chrisrzhou" target="_blank"><i class="fa fa-linkedin"></i></a>
</p>
</footer>
<!-- scripts -->
<script src="http://code.angularjs.org/1.3.5/angular.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="app.js"></script>
<script src="sunburst.js"></script>
<script>
// Hack to make this example display correctly in an iframe on bl.ocks.org
d3.select(self.frameElement).style("height", "1000px");
</script>
</body>
</html>
function sunburstDraw(scope, element) {
/**
* Angular variables
*
*/
// watch for changes on scope.data
scope.$watch("data", function() {
var data = scope.data;
render(data);
});
/**
* Dimensions of svg, sunburst, legend, breadcrumbs
*
*/
// svg dimensions
var width = 500;
var height = 300;
var radius = Math.min(width, height) / 2;
// Breadcrumb dimensions: width, height, spacing, width of tip/tail.
var b = {
w: 60,
h: 30,
s: 3,
t: 10
};
// Legend dimensions: width, height, spacing, radius of rounded rect.
var li = {
w: 75,
h: 30,
s: 3,
r: 3
};
// margins
var margin = {
top: radius,
bottom: 50,
left: radius,
right: 0
};
// sunburst margins
var sunburstMargin = {
top: 2 * radius + b.h,
bottom: 0,
left: 0,
right: radius / 2
};
/**
* Drawing variables:
*
* e.g. colors, totalSize, partitions, arcs
*/
// Mapping of nodes to colorscale.
var colors = d3.scale.category10();
// Total size of all nodes, to be used later when data is loaded
var totalSize = 0;
// create d3.layout.partition
var partition = d3.layout.partition()
.size([2 * Math.PI, radius * radius])
.value(function(d) {
return d.size;
});
// create arcs for drawing D3 paths
var arc = d3.svg.arc()
.startAngle(function(d) {
return d.x;
})
.endAngle(function(d) {
return d.x + d.dx;
})
.innerRadius(function(d) {
return Math.sqrt(d.y);
})
.outerRadius(function(d) {
return Math.sqrt(d.y + d.dy);
});
/**
* Define and initialize D3 select references and div-containers
*
* e.g. vis, breadcrumbs, lastCrumb, summary, sunburst, legend
*/
// create main vis selection
var vis = d3.select(element[0])
.append("div").classed("vis-continer", true)
.style("position", "relative")
.style("margin-top", "20px")
.style("margin-bottom", "20px")
.style("left", "50px")
.style("height", height + 2 * b.h + "px");
// create and position SVG
var sunburst = vis
.append("div").classed("sunburst-container", true)
.style("position", "absolute")
.style("left", sunburstMargin.left + "px")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// create and position legend
var legend = vis
.append("div").classed("legend-container", true)
.style("position", "absolute")
.style("top", b.h + "px")
.style("left", 2 * radius + sunburstMargin.right + "px")
.style("width", 50 + "px")
.style("height", 50 + "px")
.append("svg")
.attr("width", li.w)
.attr("height", height);
// create and position breadcrumbs container and svg
var breadcrumbs = vis
.append("div").classed("breadcrumbs-container", true)
.style("position", "absolute")
.style("top", sunburstMargin.top + "px")
.append("svg")
.attr("width", width)
.attr("height", b.h)
.attr("fill", "white")
.attr("font-weight", 600);
// create last breadcrumb element
var lastCrumb = breadcrumbs
.append("text").classed("lastCrumb", true);
// create and position summary container
var summary = vis
.append("div").classed("summary-container", true)
.style("position", "absolute")
.style("top", radius * 0.80 + "px")
.style("left", sunburstMargin.left + radius / 2 + "px")
.style("width", radius + "px")
.style("height", radius + "px")
.style("text-align", "center")
.style("font-size", "11px")
.style("color", "#666")
.style("z-index", "-1");
/**
* Render process:
*
* 1) Load data
* 2) Build Tree
* 3) Draw visualization
*/
// render visualization
function render(data) {
var parsedData = d3.csv.parseRows(data); // load data
var json = buildHierarchy(parsedData); // build json tree
removeVisualization(); // remove existing visualization if any
createVisualization(json); // visualize json tree
}
/**
* Helper functions:
*
* @function removeVisualization(): removes existing SVG components
* @function createVisualization(json): create visualization from json tree structure
* @function colorMap(d): color nodes with colors mapping
* @function mouseover(d): mouseover function
* @function mouseleave(d): mouseleave function
* @function getAncestors(node): get ancestors of a specified node
* @function buildHierarchy(data): generate json nested structure from csv data input
*/
// removes existing SVG components
function removeVisualization() {
sunburst.selectAll(".nodePath").remove();
legend.selectAll("g").remove();
}
// visualize json tree structure
function createVisualization(json) {
drawSunburst(json); // draw sunburst
drawLegend(); // draw legend
};
// helper function colorMap - color gray if "end" is detected
function colorMap(d) {
return colors(d.name);
}
// helper function to draw the sunburst and breadcrumbs
function drawSunburst(json) {
// Build only nodes of a threshold "visible" sizes to improve efficiency
var nodes = partition.nodes(json)
.filter(function(d) {
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
});
// this section is required to update the colors.domain() every time the data updates
var uniqueNames = (function(a) {
var output = [];
a.forEach(function(d) {
if (output.indexOf(d.name) === -1) output.push(d.name);
});
return output;
})(nodes);
colors.domain(uniqueNames); // update domain colors
// create path based on nodes
var path = sunburst.data([json]).selectAll("path")
.data(nodes).enter()
.append("path").classed("nodePath", true)
.attr("display", function(d) {
return d.depth ? null : "none";
})
.attr("d", arc)
.attr("fill", colorMap)
.attr("opacity", 1)
.attr("stroke", "white")
.on("mouseover", mouseover);
// // trigger mouse click over sunburst to reset visualization summary
vis.on("click", click);
// Update totalSize of the tree = value of root node from partition.
totalSize = path.node().__data__.value;
}
// helper function to draw legend
function drawLegend() {
// remove "root" label from legend
var labels = colors.domain().splice(1, colors.domain().length);
// create legend "pills"
var g = legend.selectAll("g")
.data(labels).enter()
.append("g")
.attr("transform", function(d, i) {
return "translate(0," + i * (li.h + li.s) + ")";
});
g.append("rect").classed("legend-pills", true)
.attr("rx", li.r)
.attr("ry", li.r)
.attr("width", li.w)
.attr("height", li.h)
.style("fill", function(d) {
return colors(d);
});
g.append("text").classed("legend-text", true)
.attr("x", li.w / 2)
.attr("y", li.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.attr("fill", "white")
.attr("font-size", "10px")
.attr("font-weight", 600)
.text(function(d) {
return d;
});
}
// helper function mouseover to handle mouseover events/animations and calculation of ancestor nodes etc
function mouseover(d) {
// build percentage string
var percentage = (100 * d.value / totalSize).toPrecision(3);
var percentageString = percentage + "%";
if (percentage < 1) {
percentageString = "< 1.0%";
}
// update breadcrumbs (get all ancestors)
var ancestors = getAncestors(d);
updateBreadcrumbs(ancestors, percentageString);
// update sunburst (Fade all the segments and highlight only ancestors of current segment)
sunburst.selectAll("path")
.attr("opacity", 0.3);
sunburst.selectAll("path")
.filter(function(node) {
return (ancestors.indexOf(node) >= 0);
})
.attr("opacity", 1);
// update summary
summary.html(
"Stage: " + d.depth + "<br />" +
"<span class='percentage'>" + percentageString + "</span><br />" +
d.value + " of " + totalSize + "<br />"
);
// display summary and breadcrumbs if hidden
summary.style("visibility", "");
breadcrumbs.style("visibility", "");
}
// helper function click to handle mouseleave events/animations
function click(d) {
// Deactivate all segments then retransition each segment to full opacity.
sunburst.selectAll("path").on("mouseover", null);
sunburst.selectAll("path")
.transition()
.duration(1000)
.attr("opacity", 1)
.each("end", function() {
d3.select(this).on("mouseover", mouseover);
});
// hide summary and breadcrumbs if visible
breadcrumbs.style("visibility", "hidden");
summary.style("visibility", "hidden");
}
// Return array of ancestors of nodes, highest first, but excluding the root.
function getAncestors(node) {
var path = [];
var current = node;
while (current.parent) {
path.unshift(current);
current = current.parent;
}
return path;
}
// Generate a string representation for drawing a breadcrumb polygon.
function breadcrumbPoints(d, i) {
var points = [];
points.push("0,0");
points.push(b.w + ",0");
points.push(b.w + b.t + "," + (b.h / 2));
points.push(b.w + "," + b.h);
points.push("0," + b.h);
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
points.push(b.t + "," + (b.h / 2));
}
return points.join(" ");
}
// Update the breadcrumb breadcrumbs to show the current sequence and percentage.
function updateBreadcrumbs(ancestors, percentageString) {
// Data join, where primary key = name + depth.
var g = breadcrumbs.selectAll("g")
.data(ancestors, function(d) {
return d.name + d.depth;
});
// Add breadcrumb and label for entering nodes.
var breadcrumb = g.enter().append("g");
breadcrumb
.append("polygon").classed("breadcrumbs-shape", true)
.attr("points", breadcrumbPoints)
.attr("fill", colorMap);
breadcrumb
.append("text").classed("breadcrumbs-text", true)
.attr("x", (b.w + b.t) / 2)
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("font-size", "10px")
.attr("text-anchor", "middle")
.text(function(d) {
return d.name;
});
// Set position for entering and updating nodes.
g.attr("transform", function(d, i) {
return "translate(" + i * (b.w + b.s) + ", 0)";
});
// Remove exiting nodes.
g.exit().remove();
// Update percentage at the lastCrumb.
lastCrumb
.attr("x", (ancestors.length + 0.5) * (b.w + b.s))
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.attr("fill", "black")
.attr("font-weight", 600)
.text(percentageString);
}
// Take a 4-column CSV of ["sequence", "stage", "node", "value"] and
// transform it into a hierarchical structure suitable for a partition layout.
function buildHierarchy(csv) {
var data = csv2json(csv); // build JSON dataframe from csv using helper function
// build tree
var root = {
name: "root",
children: []
};
data.forEach(function(d) {
var nodes = d.nodes;
var size = parseInt(d.size);
// build graph, nodes, and child nodes
var currentNode = root;
for (var j = 0; j < nodes.length; j++) {
var children = currentNode.children;
var nodeName = nodes[j];
var childNode;
if (j + 1 < nodes.length) {
// Not yet at the end of the sequence; move down the tree.
var foundChild = false;
for (var k = 0; k < children.length; k++) {
if (children[k].name == nodeName) {
childNode = children[k];
foundChild = true;
break;
}
}
if (!foundChild) { // If we don't already have a child node for this branch, create it.
childNode = {
name: nodeName,
children: []
};
children.push(childNode);
}
currentNode = childNode;
} else { // Reached the end of the sequence; create a leaf node.
childNode = {
name: nodeName,
size: size
};
children.push(childNode);
}
}
});
return root;
}
// helper function to buildHierarchy to transform 4-column CSV into a JSON dataframe.
function csv2json(csv) {
var data = [];
var sequences = [];
// sort the dataframe ascending by sequence (d[0]) then by stage (d[1])
csv.sort(function(a, b) {
if (a[2] === b[2]) {
return d3.ascending(a[0], b[0]);
}
return d3.ascending(a[1], b[1]);
});
csv.forEach(function(record) {
var sequence = record[0];
if (sequences.indexOf(sequence) < 0) sequences.push(sequence);
});
sequences.forEach(function(sequence) {
var d = {
nodes: [],
size: 0
};
csv.forEach(function(record) {
var node = record[2];
var size = record[3];
if (sequence === record[0]) {
d.nodes.push(node);
d.size = size;
}
});
data.push(d);
});
return data;
}
}
##Links
- [bl.ocks](http://bl.ocks.org/chrisrzhou/d5bdd8546f64ca0e4366)
- [plunker](http://embed.plnkr.co/TgGw0V/preview)
##Description
- D3 Sunburst Sequence visualizes a graph of nodes by highlight sequential progression of nodes leading up to a final value. It is useful to visualize relative weights/percentages of a starting state to an end state (e.g. webpage redirects, product retention, subscription-based products, cashflows).
- This is a variation of the original [sunburst sequence](http://bl.ocks.org/kerryrodden/7090426).
- A major improvement to the original vis is to organize the code base and draw the D3 components (breadcrumbs, sunburst, legend) from a single HTML div tag, and to dynamically assign color and legend scales.
- The other improvement is generalizing and conventionalizing data inputs. The input requires a simple tabular schema of `sequence, stage, node, value` (see below) and the program will parse the data into a JSON graph.
- The CSV data can be unsorted but it must NOT contain a header.
- The data input has to be a 4-column CSV conforming to the data schema of:
- `sequence (int/string)`: an ordered sequence that clearly defines the grouping of nodes.
- `stage (int)`: the index/order of nodes in a given sequence.
- `node (int/string)`: the data name of the node.
- `value (int)`: the value at each stage of a given sequence. Only the final stage value in a given sequence is used in this visualization.
##Files
- **`index.html`**: Main angular app connecting the D3 vis through an angular directive `<sunburst>`.
- **`app.js`**: Main angular app file connecting the DOM view with Javascript variables. Contains directive `onReadFile` to handle file uploads and `sunburst` to re-render the D3 visualization on data updates.
- **`sunburst.js`**: Contains the logic for drawing the D3 visualization by selecting the `angular.element` from which the vis is to be drawn. Updates and prompts D3 to re-render the visualization when the angular data changes on file uploads.
- **`style.css`**: stylesheet containing optional D3 classes that can be adjusted (commented out)
- **`data.csv`**: Four CSV-data files for sample downloads and uploads to the app.
##Notes
- Visualization hover can be a little glitchy if the base data does not contain very meaningful sequences i.e. smaller parent nodes that lead up to larger child nodes.
- A big help from this [fiddle](http://jsfiddle.net/alexsuch/6aG4x/) to help implement an AngularJS `FileReader`.
body {
font-family: "Open Sans", sans-serif;
font-size: 12px;
font-weight: 400;
padding-top: 10px;
padding-bottom: 100px;
}
html {
overflow-y: scroll;
}
h1 {
color: steelblue;
font-weight: 800;
font-size: 1.7em;
}
h2 {
color: steelblue;
font-size: 1.3em;
padding-bottom: 10px;
}
footer a,
footer a:hover, footer a:visited {
color: #D2A000;
}
.text-small {
font-size: 12px;
font-style: italic;
}
footer {
color: white;
padding-top: 5px;
border-top: 1px solid gray;
font-size: 12px;
position: fixed;
left: 0;
bottom: 0;
height: 50px;
width: 100%;
background: black;
text-align: center;
}
.percentage {
font-size: 2em;
}
pre {
height: 300px;
font-size: 9px;
overflow-y: scroll;
}
/* Customizable classes used in D3 vis, uncomment to customize
.vis-container {
position: relative;
top: 50px;
left: 50px;
}
.sunburst-container {
position: absolute;
z-index: 1;
}
.summary-container {
position: absolute;
}
.breadcrumbs-container {
position: absolute;
}
.legend-container {
position: absolute;
}
.lastCrumb {
fill: black;
font-weight: 600;
}
.breadcrumbs-text {
fill: white;
font-weight: 600;
}
.legend-text{
fill: red;
font-weight: 600;
}
.nodePath {
stroke: white;
} */
1, 1, froyo, 0
1, 2, froyo, 0
1, 3, froyo, 0
1, 4, froyo, 25
2, 1, froyo, 0
2, 2, froyo, 0
2, 3, froyo, 0
2, 4, gingerbread, 52
3, 1, froyo, 0
3, 2, froyo, 0
3, 3, froyo, 0
3, 4, icecream, 128
4, 1, froyo, 0
4, 2, froyo, 0
4, 3, froyo, 0
4, 4, jellybean, 328
5, 1, froyo, 0
5, 2, froyo, 0
5, 3, froyo, 0
5, 4, kitkat, 231
6, 1, gingerbread, 0
6, 2, gingerbread, 0
6, 3, gingerbread, 0
6, 4, gingerbread, 116
7, 1, gingerbread, 0
7, 2, gingerbread, 0
7, 3, gingerbread, 0
7, 4, icecream, 229
8, 1, gingerbread, 0
8, 2, gingerbread, 0
8, 3, gingerbread, 0
8, 4, jellybean, 73
9, 1, gingerbread, 0
9, 2, gingerbread, 0
9, 3, gingerbread, 0
9, 4, kitkat, 23
10, 1, gingerbread, 0
10, 2, icecream, 0
10, 3, jellybean, 0
10, 4, kitkat, 1265
11, 1, gingerbread, 0
11, 2, gingerbread, 0
11, 3, icecream, 0
11, 4, jellybean, 869
12, 1, gingerbread, 0
12, 2, icecream, 0
12, 3, icecream, 0
12, 4, jellybean, 321
13, 1, gingerbread, 0
13, 2, gingerbread, 0
13, 3, icecream, 0
13, 4, icecream, 264
14, 1, gingerbread, 0
14, 2, icecream, 0
14, 3, icecream, 0
14, 4, icecream, 168
15, 1, gingerbread, 0
15, 2, icecream, 0
15, 3, jellybean, 0
15, 4, jellybean, 476
16, 1, gingerbread, 0
16, 2, icecream, 0
16, 3, icecream, 0
16, 4, jellybean, 576
17, 1, gingerbread, 0
17, 2, icecream, 0
17, 3, jellybean, 0
17, 4, kitkat, 1342
18, 1, gingerbread, 0
18, 2, icecream, 0
18, 3, kitkat, 0
18, 4, kitkat, 469
(function() {
angular.module("Sunburst", [])
.directive("sunburst", sunburst)
.directive("onReadFile", onReadFile)
.controller("MainCtrl", MainCtrl);
// controller function MainCtrl
function MainCtrl($http) {
var ctrl = this;
init();
// function init
function init() {
// initialize controller variables
ctrl.examples = [
"data_android_os_conversion",
"data_netflix_churn",
"data_page_clicks"
];
ctrl.exampleSelected = ctrl.examples[0];
ctrl.getData = getData;
ctrl.selectExample = selectExample;
// initialize controller functions
ctrl.selectExample(ctrl.exampleSelected);
}
// function getData
function getData($fileContent) {
ctrl.data = $fileContent;
}
// function selectExample
function selectExample(item) {
var file = item + ".csv";
$http.get(file).success(function(data) {
ctrl.data = data;
});
}
}
// directive function sunburst
function sunburst() {
return {
restrict: "E",
scope: {
data: "=",
},
link: sunburstDraw
};
}
// directive function onReadFile
function onReadFile($parse) {
return {
restrict: "A",
scope: false,
link: function(scope, element, attrs) {
var fn = $parse(attrs.onReadFile);
element.on("change", function(onChangeEvent) {
var reader = new FileReader();
reader.onload = function(onLoadEvent) {
scope.$apply(function() {
fn(scope, {
$fileContent: onLoadEvent.target.result
});
});
};
reader.readAsText((onChangeEvent.srcElement || onChangeEvent.target).files[0]);
});
}
};
}
})();
1, 1, Netflix, 0
1, 2, Netflix, 0
1, 3, Netflix, 0
1, 4, Netflix, 0
1, 5, Netflix, 1359
2, 1, Netflix, 0
2, 2, Netflix, 0
2, 3, Netflix, 0
2, 4, Netflix, 0
2, 5, Amazon, 359
3, 1, Netflix, 0
3, 2, Netflix, 0
3, 3, Netflix, 0
3, 4, Netflix, 0
3, 5, Hulu, 265
4, 1, Netflix, 0
4, 2, Netflix, 0
4, 3, Netflix, 0
4, 4, Netflix, 0
4, 5, HBOGo, 54
5, 1, Netflix, 0
5, 2, Netflix, 0
5, 3, Netflix, 0
5, 4, Amazon, 629
6, 1, Netflix, 0
6, 2, Netflix, 0
6, 3, Netflix, 0
6, 4, Hulu, 329
7, 1, Netflix, 0
7, 2, Netflix, 0
7, 3, Netflix, 0
7, 4, HBOGo, 23
8, 1, Netflix, 0
8, 2, Netflix, 0
8, 3, Amazon, 125
9, 1, Netflix, 0
9, 2, Netflix, 0
9, 3, Hulu, 825
10, 1, Netflix, 0
10, 2, Netflix, 0
10, 3, HBOGo, 23
11, 1, Netflix, 0
11, 3, Hulu, 425
12, 1, Netflix, 0
12, 3, Amazon, 65
1, 1, home, 0
1, 2, product, 0
1, 3, product, 0
1, 4, product, 0
1, 5, product, 335
2, 1, home, 0
2, 2, product, 0
2, 3, product, 0
2, 4, product, 0
2, 5, search, 35
3, 1, home, 0
3, 2, product, 0
3, 3, product, 0
3, 4, product, 0
3, 5, account, 135
4, 1, home, 0
4, 2, product, 0
4, 3, product, 0
4, 4, product, 0
4, 5, other, 65
5, 1, product, 0
5, 2, product, 0
5, 3, product, 0
5, 4, product, 0
5, 5, product, 267
6, 1, product, 0
6, 2, other, 0
6, 3, other, 0
6, 4, other, 0
6, 5, other, 34
7, 1, home, 0
7, 2, other, 0
7, 3, other, 0
7, 4, other, 0
7, 5, other, 134
8, 1, account, 0
8, 2, account, 0
8, 3, other, 0
8, 4, other, 0
8, 5, product, 76
9, 1, account, 0
9, 2, product, 0
9, 3, product, 0
9, 4, other, 0
9, 5, other, 52
10, 1, home, 0
10, 2, product, 0
10, 3, product, 0
10, 4, other, 367
11, 1, home, 0
11, 2, product, 0
11, 3, product, 0
11, 4, account, 87
12, 1, home, 0
12, 2, home, 0
12, 3, home, 0
12, 4, product, 56
13, 1, account, 0
13, 2, account, 0
13, 3, home, 0
13, 4, home, 96
14, 1, account, 0
14, 2, other, 0
14, 3, product, 397
15, 1, account, 0
15, 2, home, 0
15, 3, product, 124
16, 1, account, 0
16, 2, other, 0
16, 3, home, 67
17, 1, home, 0
17, 2, other, 0
17, 3, product, 762
18, 1, home, 0
18, 2, home, 0
18, 3, product, 242
19, 1, home, 0
19, 2, home, 0
19, 3, other, 623