Reputation: 194
I want to create an interactive collapsible tree diagram from a csv table with D3.
For example, the csv can look like this:
name,parent
Level 2: A,Top Level
Top Level,null
Son of A,Level 2: A
Daughter of A,Level 2: A
Level 2: B,Top Level
My idea was to use these codes to produce I want. I used the first code and add some lines from the second to use the csv file. However, it doesn't work. Do you have any ideas how to do it correctly?
Thanks.
This is the code I did, which doesn't work (error message : "d3.v4.min.js:2 Uncaught TypeError: t.eachBefore is not a function") :
<!DOCTYPE html>
<meta charset="UTF-8">
<style>
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text {
font: 12px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
</style>
<body>
<!-- load the d3.js library -->
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
d3.csv("treedata.csv", function(error, data) {
// Set the dimensions and margins of the diagram
var margin = {top: 20, right: 90, bottom: 30, left: 90},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// append the svg object to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("body").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate("
+ margin.left + "," + margin.top + ")");
var i = 0,
duration = 750;
// declares a tree layout and assigns the size
var treemap = d3.tree().size([height, width]);
// Assigns parent, children, height, depth
// *********** Convert flat data into a nice tree ***************
// create a name: node map
var dataMap = data.reduce(function(map, node) {
map[node.name] = node;
return map;
}, {});
// create the tree array
var treeData = [];
data.forEach(function(node) {
// add to parent
var parent = dataMap[node.parent];
if (parent) {
// create child array if it doesn't exist
(parent.children || (parent.children = []))
// add node to child array
.push(node);
} else {
// parent is null or missing
treeData.push(node);
}
});
var root = treeData[0];
root.x0 = height / 2;
root.y0 = 0;
// Collapse after the second level
root.children.forEach(collapse);
update(root);
// Collapse the node and all it's children
function collapse(d) {
if(d.children) {
d._children = d.children
d._children.forEach(collapse)
d.children = null
}
}
function update(source) {
// Assigns the x and y position for the nodes
var treeData = treemap(root);
// Compute the new tree layout.
var nodes = treeData.descendants(),
links = treeData.descendants().slice(1);
// Normalize for fixed-depth.
nodes.forEach(function(d){ d.y = d.depth * 180});
// ****************** Nodes section ***************************
// Update the nodes...
var node = svg.selectAll('g.node')
.data(nodes, function(d) {return d.id || (d.id = ++i); });
// Enter any new modes at the parent's previous position.
var nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
// Add Circle for the nodes
nodeEnter.append('circle')
.attr('class', 'node')
.attr('r', 1e-6)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
// Add labels for the nodes
nodeEnter.append('text')
.attr("dy", ".35em")
.attr("x", function(d) {
return d.children || d._children ? -13 : 13;
})
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) { return d.data.name; });
// UPDATE
var nodeUpdate = nodeEnter.merge(node);
// Transition to the proper position for the node
nodeUpdate.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Update the node attributes and style
nodeUpdate.select('circle.node')
.attr('r', 10)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
})
.attr('cursor', 'pointer');
// Remove any exiting nodes
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
// On exit reduce the node circles size to 0
nodeExit.select('circle')
.attr('r', 1e-6);
// On exit reduce the opacity of text labels
nodeExit.select('text')
.style('fill-opacity', 1e-6);
// ****************** links section ***************************
// Update the links...
var link = svg.selectAll('path.link')
.data(links, function(d) { return d.id; });
// Enter any new links at the parent's previous position.
var linkEnter = link.enter().insert('path', "g")
.attr("class", "link")
.attr('d', function(d){
var o = {x: source.x0, y: source.y0}
return diagonal(o, o)
});
// UPDATE
var linkUpdate = linkEnter.merge(link);
// Transition back to the parent element position
linkUpdate.transition()
.duration(duration)
.attr('d', function(d){ return diagonal(d, d.parent) });
// Remove any exiting links
var linkExit = link.exit().transition()
.duration(duration)
.attr('d', function(d) {
var o = {x: source.x, y: source.y}
return diagonal(o, o)
})
.remove();
// Store the old positions for transition.
nodes.forEach(function(d){
d.x0 = d.x;
d.y0 = d.y;
});
// Creates a curved (diagonal) path from parent to the child nodes
function diagonal(s, d) {
path = `M ${s.y} ${s.x}
C ${(s.y + d.y) / 2} ${s.x},
${(s.y + d.y) / 2} ${d.x},
${d.y} ${d.x}`
return path
}
// Toggle children on click.
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
}
});
</script>
</body>
Upvotes: 0
Views: 1831
Reputation: 3872
I have written the following that should do what you need it to. If not you can adjust it for your needs:
function convertToJsonTree(lines) {
// create the base
let parent = {};
// iterate through the lines
for (let i = 0; i < lines.length; i++) {
let _s = lines[i].split(',');
let _name = _s[0];
let _parent = _s[1];
// if the parents name is null, we must be at the base
if (_parent === 'null') {
// set the name
parent.name = _name;
// get rid of the current line
delete lines[i];
let subLines = lines.filter(function(el) {
return el != null;
});
// check for the first set of children
for (let j = 0; j < subLines.length; j++) {
let __s = subLines[j].split(',');
let __name = __s[0];
let __parent = __s[1];
// if there's a child that has the current level as the parent
if (parent && parent.name === __parent) {
// if the children array is undefined, create an array
if (!parent.children)
parent.children = [];
// create the child
let child = {
name: __name
};
// add it to the parents children
parent.children.push(child);
// remove the line that was already checked
delete subLines[j];
let _subLines = subLines.filter(function(el) {
return el != null;
});
// check if the child has any children
getChildren(child, _subLines);
}
}
}
}
return parent;
}
function getChildren(parent, lines) {
for (let i = 0; i < lines.length; i++) {
let _s = lines[i].split(',');
let _name = _s[0];
let _parent = _s[1];
// if there's a child that has the current level as the parent
if (parent && parent.name === _parent) {
// if the children array is undefined, create an array
if (!parent.children)
parent.children = [];
// create the child
let child = {
name: _name
};
// add it to the parents children
parent.children.push(child);
// remove the line that was already checked
delete lines[i];
let subLines = lines.filter(function(el) {
return el != null;
});
// check if the child has any children
getChildren(child, subLines);
}
}
}
Its use:
let csv = `name,parent
Level 2: A,Top Level
Top Level,null
Son of A,Level 2: A
Daughter of A,Level 2: A
Level 2: B,Top Level`;
let lines = csv.split(/\r\n|\n/);
let treeData = convertToJsonTree(lines);
console.log(treeData);
JSFiddle: https://jsfiddle.net/dbheale/Lrhsxw9e/
EDIT:
To read in your CSV from a file:
function loadDiagramFromCsvFile(file)
{
var rawFile = new XMLHttpRequest();
rawFile.open("GET", file, false);
rawFile.onreadystatechange = function ()
{
if(rawFile.readyState === 4)
{
if(rawFile.status === 200 || rawFile.status == 0)
{
var csv = rawFile.responseText;
let lines = csv.split(/\r\n|\n/);
let treeData = convertToJsonTree(lines);
// Do what you need with your treeData here
// Maybe create a method that generates the diagram?
createDiagram(treeData);
}
}
}
rawFile.send(null);
}
Upvotes: 1