Skip to content

Instantly share code, notes, and snippets.

@chrisrzhou
Last active October 19, 2017 09:32
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save chrisrzhou/d5bdd8546f64ca0e4366 to your computer and use it in GitHub Desktop.
Save chrisrzhou/d5bdd8546f64ca0e4366 to your computer and use it in GitHub Desktop.
D3 Sunburst Sequence

D3 Sunburst Sequence

bl.ocks.org link

D3 Sunburst Sequence visualizes a graph of nodes by highlighting sequential progression of nodes leading up to a final value. A sunburst sequence 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).


Description

  • This is a variation of the original sunburst sequence.

  • A major improvement to the original visualization is to organize the code base and draw the D3 components (breadcrumbs, sunburst, legend) into 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, and 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 HTML file.

  • app.js: Main angular app file. 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.


Other 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 to help implement an AngularJS FileReader.

Analytics

(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 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
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
<!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">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-cubes"></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>
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;
} */
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;
}
}
@rtexal
Copy link

rtexal commented Sep 5, 2015

Hi Chris, is there anyway to use JSON directly instead of CSV? I read that d3 parses the CSV into a JSON graph anyway. If so, do you have a sample of how the JSON structure looks like? Thanks! :)

@tranlm
Copy link

tranlm commented Feb 10, 2016

Hi there,

Great write up Chris. I'm thankful for it!

I noticed that there's a console error every time I load the page. Looking closer at the code, it appears to have something to do with Angular and d3.csv.parse. Specifically, line 161 in sunburst.js is the root of the error. Do you happen to have any clues as to what's causing this?

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment