4

I have the initial state of tree diagram that I'm building, with collapse animation and custom node design but whatever I try to make the tree vertical instead of horizontal I mess something else (animation not working well, the lines are wrong and other problems).

I'm trying to visualize a merkle tree where a vertical display is more intuitive. unfortunately I dont have strong background in front-end or deep understanding of d3 lib.

Here is the code:

<!DOCTYPE html>
<meta charset="UTF-8">
<style>
    .node rect {
        stroke-width: 3px;
    }
    
    .node text {
        font: 12px sans-serif;
        fill: #fff;
    }
    
    .link {
        fill: none;
        stroke: #ccc;
        stroke-width: 2px;
    }
</style>

<body>

    <!-- load the d3.js library -->
    <script src="https://d3js.org/d3.v5.min.js"></script>
    <script>

var treeData =
  {
    "name": "NODE NAME 1",  
    "subname": "CODE N1",    
    "fill":"orange",
    "children": [
      { 
        "name": "NODE NAME 2.1",
        "subname": "CODE N1",    
        "fill":"blue"
      },
      { "name": "NODE NAME 2.2","subname": "CODE N1"  ,"fill":"blue" },
      { "name": "NODE NAME 2.3","subname": "CODE N1","fill":"blue",
      "children": [
          { "name": "NODE NAME 3.3","fill":"blue","subname": "CODE N1",
           "children": [{
               "name":"NODE NAME 4.1","subname": "CODE N1",
               "fill":"#d281d2"
           }
               ] },
          { "name": "NODE NAME 3.4","fill":"blue", "subname": "CODE N1" ,
           "children": [{
               "name":"NODE NAME 4.2", "subname": "CODE N1" ,
               "fill":"#d281d2"
           }
               ]
           }
        ]
        }
    ]
  };

// 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,
    root;

// declares a tree layout and assigns the size
var treemap = d3.tree().size([height, width]);

// Assigns parent, children, height, depth
root = d3.hierarchy(treeData, function(d) { return d.children; });
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);

    var rectHeight = 60, rectWidth = 120;
    
    nodeEnter.append('rect')
      .attr('class', 'node')
      .attr("width", rectWidth)
      .attr("height", rectHeight)
      .attr("x", 0)
      .attr("y", (rectHeight/2)*-1)
      .attr("rx","5")
      .style("fill", function(d) {
          return d.data.fill;
      });

  // Add labels for the nodes
  nodeEnter.append('text')
      .attr("dy", "-.35em")
      .attr("x", function(d) {
        return 13;
      })
      .attr("text-anchor", function(d) {
        return "start";
      })
      .text(function(d) { return d.data.name; })
      .append("tspan")
      .attr("dy", "1.75em")
      .attr("x", function(d) {
        return 13;
      })
      .text(function(d) { return d.data.subname; });

  // 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>
Robin Mackenzie
  • 18,801
  • 7
  • 38
  • 56
Ron
  • 1,744
  • 6
  • 27
  • 53

1 Answers1

0

The three changes below from your code show that the flip from horizontal to vertical orientation is based on the transform attribute of the nodes during enter and update, and the drawing of the links.

You can see that because the changes put x and y in the normal order, that the natural orientation is vertical - but many examples of D3 trees out there have horizontal orientations.

There's a few other questions that address this e.g. here, here and here. As they are a few years old this post shows a D3 v5 example of the general principle.

Change 1 - note the change flips x and y:

  // Enter any new modes at the parent's previous position.
  var nodeEnter = node.enter().append('g')
      .attr('class', 'node')
      .attr("transform", function(d) {
        // BEFORE ....
        //return "translate(" + source.y0 + "," + source.x0 + ")";
        // AFTER ....
        return "translate(" + source.x0 + "," + source.y0 + ")";
    })
    .on('click', click);

Change 2 - note the change flips x and y:

  // Transition to the proper position for the node
  nodeUpdate.transition()
    .duration(duration)
    .attr("transform", function(d) { 
        // BEFORE ....
        //return "translate(" + d.y + "," + d.x + ")";
        // AFTER ....
        return "translate(" + d.x + "," + d.y + ")";
     });

And, Change 3 - same x and y flip plus (rectWidth / 2) draws the links centered in the nodes.

  // Creates a curved (diagonal) path from parent to the child nodes
  function diagonal(s, d) {

    // BEFORE ....
    //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}`

    // AFTER ....
    path = `M ${s.x + (rectWidth / 2)} ${s.y}
            C ${(s.x + d.x) / 2 + (rectWidth / 2)} ${s.y},
              ${(s.x + d.x) / 2 + (rectWidth / 2)} ${d.y},
              ${d.x + (rectWidth / 2)} ${d.y}`
              
    return path
  }

Working example:

<!DOCTYPE html>
<meta charset="UTF-8">
<style>
    .node rect {
        stroke-width: 3px;
    }
    
    .node text {
        font: 12px sans-serif;
        fill: #fff;
    }
    
    .link {
        fill: none;
        stroke: #ccc;
        stroke-width: 2px;
    }
</style>

<body>

    <!-- load the d3.js library -->
    <script src="https://d3js.org/d3.v5.min.js"></script>
    <script>

var treeData =
  {
    "name": "NODE NAME 1",  
    "subname": "CODE N1",    
    "fill":"orange",
    "children": [
      { 
        "name": "NODE NAME 2.1",
        "subname": "CODE N1",    
        "fill":"blue"
      },
      { "name": "NODE NAME 2.2","subname": "CODE N1"  ,"fill":"blue" },
      { "name": "NODE NAME 2.3","subname": "CODE N1","fill":"blue",
      "children": [
          { "name": "NODE NAME 3.3","fill":"blue","subname": "CODE N1",
           "children": [{
               "name":"NODE NAME 4.1","subname": "CODE N1",
               "fill":"#d281d2"
           }
               ] },
          { "name": "NODE NAME 3.4","fill":"blue", "subname": "CODE N1" ,
           "children": [{
               "name":"NODE NAME 4.2", "subname": "CODE N1" ,
               "fill":"#d281d2"
           }
               ]
           }
        ]
        }
    ]
  };

// 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,
    root;

// declares a tree layout and assigns the size
var treemap = d3.tree().size([height, width]);

// Assigns parent, children, height, depth
root = d3.hierarchy(treeData, function(d) { return d.children; });
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) {
        // BEFORE ....
        //return "translate(" + source.y0 + "," + source.x0 + ")";
        // AFTER ....
        return "translate(" + source.x0 + "," + source.y0 + ")";
    })
    .on('click', click);

    var rectHeight = 60, rectWidth = 120;
    
    nodeEnter.append('rect')
      .attr('class', 'node')
      .attr("width", rectWidth)
      .attr("height", rectHeight)
      .attr("x", 0)
      .attr("y", (rectHeight/2)*-1)
      .attr("rx","5")
      .style("fill", function(d) {
          return d.data.fill;
      });

  // Add labels for the nodes
  nodeEnter.append('text')
      .attr("dy", "-.35em")
      .attr("x", function(d) {
        return 13;
      })
      .attr("text-anchor", function(d) {
        return "start";
      })
      .text(function(d) { return d.data.name; })
      .append("tspan")
      .attr("dy", "1.75em")
      .attr("x", function(d) {
        return 13;
      })
      .text(function(d) { return d.data.subname; });

  // UPDATE
  var nodeUpdate = nodeEnter.merge(node);

  // Transition to the proper position for the node
  nodeUpdate.transition()
    .duration(duration)
    .attr("transform", function(d) { 
        // BEFORE ....
        //return "translate(" + d.y + "," + d.x + ")";
        // AFTER ....
        return "translate(" + d.x + "," + d.y + ")";
     });

  // 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) {
          // BEFORE ....
          //return "translate(" + source.y + "," + source.x + ")";
          // AFTER ....
          return "translate(" + source.x + "," + source.y + ")";
      })
      .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) {

    // BEFORE ....
    //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}`

    // AFTER ....
    path = `M ${s.x + (rectWidth / 2)} ${s.y}
            C ${(s.x + d.x) / 2 + (rectWidth / 2)} ${s.y},
              ${(s.x + d.x) / 2 + (rectWidth / 2)} ${d.y},
              ${d.x + (rectWidth / 2)} ${d.y}`
              
    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>
Robin Mackenzie
  • 18,801
  • 7
  • 38
  • 56
  • Thats progress :) but the collapse animation is still off, here I captured my screen so you can see: https://drive.google.com/file/d/1h21OxzPRR5N3UOK_gTFScgUQfnoG0rwB/view?usp=sharing – Ron Nov 22 '21 at 09:16
  • The structure of your tree in the video is not the same as the tree in the question. I think the example I posted addresses the underlying issue in your code i.e. the switch from horizontal to vertical is based on the switching x/y in the transforms and diagonal function. Read the other answers I linked to if you're not clear about this. – Robin Mackenzie Nov 22 '21 at 10:51