Juan
Juan

Reputation: 194

How to create an interactive collapsible tree diagram from csv with D3 and javascript?

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

Answers (1)

Blue Eyed Behemoth
Blue Eyed Behemoth

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

Related Questions