0

I've been trying to migrate from D3.js v3 to version 4. I've reviewed the changelog and updated all functions, but I'm unable to render the path from source to target nodes now that the diagonal function is removed.

I'm using a Parse Tree generated by a Python Script via HTML and d3.js. The Python Script generates an HMTL document, here it is running with D3.js version 3

function drawTree(){
var margin = {top: 20, right: 120, bottom: 20, left: 120},
    width = 1060 - margin.right - margin.left,
    height = 600 - margin.top - margin.bottom;
    
var i = 0,
    duration = 750,// animation duration
    root;// stores the tree structure in json format

var tree = d3.layout.tree()
    .size([height, width]);

var edge_weight = d3.scale.linear()
     .domain([0, 100])
                    .range([0, 100]);

var diagonal = d3.svg.diagonal()
    .projection(function(d) { return [d.y, d.x]; });

// adding the svg to the html structure
var svg = d3.select("div#viz").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 treeData =
  {
    "name": "Grandparent",
    "size" : 100,
    "children": [
      { 
        "name": "Parent A",
        "size": 70,
        "children": [
          { "name": "Son of A",
            "size" : 30,
            "children": [
              { "name": "grandson of A",
                "size" : 3},
              { "name": "grandson 2 of A",
                "size" : 2},
              { "name": "grandson 3 of A",
                "size" : 5},
              { "name": "grandaughter of A",
                "size" : 20,
                "children": [
                  { "name": "great-grandson of A",
                    "size" : 15},
                  { "name": "great-grandaughter of A",
                    "size" : 5}
                ]
              }
            ],
          },
          { "name": "Daughter of A" ,
          "size" : 40
          }
        ]
      },
      { "name": "Parent B",
        "size" : 30 }],
     };
  
  
  edge_weight.domain([0,treeData.size]);
  
// Assigns parent, children, height, depth
root = treeData;
root.x0 = height / 2;
root.y0 = 0;
  
  root.children.forEach(collapse);
  update(root);

d3.select(self.frameElement).style("height", "800px");

/**
 * Updates the node.
 * cloppases and expands the node bases on the structure of the source
 * all 'children' nodes are expanded and '_children' nodes collapsed
 * @param {json structure} source
 */
function update(source) {

  // Compute the new tree layout.
  var nodes = tree.nodes(root).reverse(),
      links = tree.links(nodes);

  // Normalize for fixed-depth.
  nodes.forEach(function(d) { d.y = d.depth * 180; });

  // Update the nodes…
  var node = svg.selectAll("g.node")
      .data(nodes, function(d) { return d.id || (d.id = ++i); });

  // Enter any new nodes 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);

  nodeEnter.append("circle")
      .attr('class', 'node')
      .attr('r', 1e-6)
      .style("fill", function(d) {
          return d._children ? "lightsteelblue" : "#fff";
      }); 

  nodeEnter.append("text")
      .attr("x", function(d) { return d.children || d._children ? -10 : 10; })
      .attr("dy", ".35em")
      .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
      .text(function(d) { return d.name; })
      .style("fill-opacity", 1e-6);

  // Transition nodes to their new position.
  var nodeUpdate = node.transition()
      .duration(duration)
      .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });

  nodeUpdate.select("circle")
      .attr("r", function(d){ console.log(">>>>>>>>", d);return edge_weight(d.size/2);})
      .style("fill", function(d) { 
      return d._children ? "lightsteelblue" : "#fff"; });

  nodeUpdate.select("text")
      .style("fill-opacity", 1);

  // Transition exiting nodes to the parent's new position.
 var nodeExit = node.exit().transition()
      .duration(duration)
      .attr("transform", function(d) {
          return "translate(" + source.y + "," + source.x + ")";
      })
      .remove();

  nodeExit.select("circle")
      .attr("r", 1e-6);

  nodeExit.select("text")
      .style("fill-opacity", 1e-6);

  // Update the links…
  // Update the links…
  var link = svg.selectAll("path.link")
      .data(links, function(d) { return d.target.id; });

  // Enter any new links at the parent's previous position.
  link.enter().insert("path", "g")
      .attr("class", "link")
      .attr("stroke-width", function(d){
       return 1;
      })
      .attr("d", function(d) {
        var o = {x: source.x0, y: source.y0};
        return diagonal({source: o, target: o});
      })
      .attr("stroke", function(d){ 
       return "lavender";});

  // Transition links to their new position.
  link.transition()
      .duration(duration)
      .attr("d", function(d){
      /* calculating the top shift */
      var source = {x: d.source.x - edge_weight(calculateLinkSourcePosition(d)), y: d.source.y};
      var target = {x: d.target.x, y: d.target.y};
      return diagonal({source: source, target: target});
      })
      .attr("stroke-width", function(d){
       return edge_weight(d.target.size);
      });

 // Transition exiting nodes to the parent's new position.
  link.exit().transition()
      .duration(duration)
      .attr("d", function(d) {
        var o = {x: source.x, y: source.y};
        return diagonal({source: o, target: o});
      })
      .remove();

  // Stash the old positions for transition.
  nodes.forEach(function(d) {
    d.x0 = d.x;
    d.y0 = d.y;
  });
}

/**
 * Calculates the source y-axis position of the link.
 * @param {json structure} link
 */
function calculateLinkSourcePosition(link) {
 targetID = link.target.id;
 var childrenNumber = link.source.children.length;
 var widthAbove = 0;
 for (var i = 0; i < childrenNumber; i++)
 {
  if (link.source.children[i].id == targetID)
  {
   // we are done
   widthAbove = widthAbove + link.source.children[i].size/2;
   break;
  }else {
   // keep adding
   widthAbove = widthAbove + link.source.children[i].size
  }
 }
 return link.source.size/2 - widthAbove;
}

/*
 * Toggle children on click.
 * @param {node} d
 */ 
function click(d) {
  if (d.children) {
    d._children = d.children;
    d.children = null;
  } else {
    d.children = d._children;
    d._children = null;
  }
  update(d);
}

/*
 * Collapses the node d and all the children nodes of d
 * @param {node} d
*/
function collapse(d) {
  if (d.children) {
    d._children = d.children;
    d._children.forEach(collapse);
    d.children = null;
  }
}

/*
 * Collapses the node in the tree
*/
function collapseAll() {
 root.children.forEach(collapse);
 update(root);
}

/*
 * Expands the node d and all the children nodes of d
 * @param {node} d
*/
function expand(d) {
 if (d._children) {
  d._children = null;
 }
 if (d.children) {
  d.children.forEach(expand);
 }
 
}
/*
 * Expands all the nodes in the tree
*/
function expandAll() {
 root.children.forEach(expand);
 update(root);
}
}
.node {
  cursor: pointer;
}

.node circle {
  fill: #fff;
  stroke: steelblue;
  stroke-width: 1.5px;
}

.node text {
  font: 10px sans-serif;
}

.link {
  fill: none;
  /*stroke: steelblue;*/
  opacity: 0.3;
  /*stroke-width: 1.5px;*/
}

#levels{
  margin-left: 120px;
}
<!DOCTYPE html>
<meta charset="utf-8">
<body onLoad="drawTree()">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.0.0/d3.min.js"></script>

<button type="button" onclick="collapseAll()">Collapse All</button>
<button type="button" onclick="expandAll()">Expand All</button>
<div id="viz"></div>
</body>

And here's as far as I got with the v4 migration.

var margin = {
    top: 20,
    right: 120,
    bottom: 20,
    left: 120
  },
  width = 900 - margin.right - margin.left,
  height = 400 - margin.top - margin.bottom;

var i = 0,
  duration = 750, // animation duration
  root; // stores the tree structure in json format


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

var edge_weight = d3.scaleLinear()
  .domain([0, 100])
  .range([0, 100]);


// 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
}

// adding the svg to the html structure
var svg = d3.select("div#viz").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 treeData = {
  "name": "Grandparent",
  "size": 100,
  "children": [{
      "name": "Parent A",
      "size": 70,
      "children": [{
          "name": "Son of A",
          "size": 30,
          "children": [{
              "name": "grandson of A",
              "size": 3
            },
            {
              "name": "grandson 2 of A",
              "size": 2
            },
            {
              "name": "grandson 3 of A",
              "size": 5
            },
            {
              "name": "grandaughter of A",
              "size": 20,
              "children": [{
                  "name": "great-grandson of A",
                  "size": 15
                },
                {
                  "name": "great-grandaughter of A",
                  "size": 5
                }
              ]
            }
          ],
        },
        {
          "name": "Daughter of A",
          "size": 40
        }
      ]
    },
    {
      "name": "Parent B",
      "size": 30
    }
  ],
};


edge_weight.domain([0, treeData.size]);

// Assigns parent, children, height, depth
root = d3.hierarchy(treeData, function(d) {
  return d.children;
});
root.x0 = height / 2;
root.y0 = 0;

root.children.forEach(collapse);
update(root);

d3.select(self.frameElement).style("height", "800px");

/**
 * Updates the node.
 * cloppases and expands the node bases on the structure of the source
 * all 'children' nodes are expanded and '_children' nodes collapsed
 * @param {json structure} source
 */
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;
  });

  // Update the nodes…
  var node = svg.selectAll("g.node")
    .data(nodes, function(d) {
      return d.id || (d.id = ++i);
    });

  // Enter any new nodes 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);

  nodeEnter.append("circle")
    .attr('class', 'node')
    .attr('r', 1e-6)
    .style("fill", function(d) {
      return d._children ? "lightsteelblue" : "#fff";
    });

  nodeEnter.append("text")
    .attr("x", function(d) {
      return d.children || d._children ? -10 : 10;
    })
    .attr("dy", ".35em")
    .attr("text-anchor", function(d) {
      return d.children || d._children ? "end" : "start";
    })
    .text(function(d) {
      return d.name;
    })
    .style("fill-opacity", 1e-6);

  // Transition nodes to their new position.
  var nodeUpdate = nodeEnter.merge(node);

  nodeUpdate.transition()
    .duration(duration)
    .attr("transform", function(d) {
      return "translate(" + d.y + "," + d.x + ")";
    });

  nodeUpdate.select("circle")
    .attr("r", function(d) {
      return edge_weight(d.data.size / 2);
    })
    .style("fill", function(d) {
      return d._children ? "lightsteelblue" : "#fff";
    });

  nodeUpdate.select("text")
    .style("fill-opacity", 1);

  // Transition exiting nodes to the parent's new position.
  var nodeExit = node.exit().transition()
    .duration(duration)
    .attr("transform", function(d) {
      return "translate(" + source.y + "," + source.x + ")";
    })
    .remove();

  nodeExit.select("circle")
    .attr("r", 1e-6);

  nodeExit.select("text")
    .style("fill-opacity", 1e-6);

  // Update the links
  var link = svg.selectAll("path.link")
    .data(links, function(d) {
      return d.id;
    });
  //.data(links, function(d) { return d.target.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) {
      console.log("linkEnter", d);
      var o = {
        x: source.x,
        y: source.y
      }
      console.log("o", o);
      return diagonal(o, o)
    })
    .attr("stroke", function(d) {
      return "cyan";
    });

  // Transition links to their new position.
  link.transition()
    .duration(duration)
    .attr("d", function(d) {
      console.log("lala", d);
      /* calculating the top shift */
      var source = {
        x: d.x - edge_weight(calculateLinkSourcePosition(d)),
        y: d.y
      };
      var target = {
        x: d.parent.x,
        y: d.parent.y
      };
      return diagonal({
        source: source,
        target: target
      });
    })
    .attr("stroke-width", function(d) {
      return edge_weight(d.target.size);
    });

  // Transition exiting nodes to the parent's new position.
  link.exit().transition()
    .duration(duration)
    .attr("d", function(d) {
      var o = {
        x: source.x,
        y: source.y
      };
      return diagonal({
        source: o,
        target: o
      });
    })
    .remove();

  // Stash the old positions for transition.
  nodes.forEach(function(d) {
    console.log("stash", d);
    d.x0 = d.x;
    d.y0 = d.y;
  });
}

/**
 * Calculates the source y-axis position of the link.
 * @param {json structure} link
 */
function calculateLinkSourcePosition(link) {
  targetID = link.target.id;
  var childrenNumber = link.source.children.length;
  var widthAbove = 0;
  for (var i = 0; i < childrenNumber; i++) {
    if (link.source.children[i].id == targetID) {
      // we are done
      widthAbove = widthAbove + link.source.children[i].size / 2;
      break;
    } else {
      // keep adding
      widthAbove = widthAbove + link.source.children[i].size
    }
  }
  return link.source.size / 2 - widthAbove;
}

/*
 * Toggle children on click.
 * @param {node} d
 */
function click(d) {
  if (d.children) {
    d._children = d.children;
    d.children = null;
  } else {
    d.children = d._children;
    d._children = null;
  }
  update(d);
}

/*
 * Collapses the node d and all the children nodes of d
 * @param {node} d
 */
function collapse(d) {
  if (d.children) {
    d._children = d.children;
    d._children.forEach(collapse);
    d.children = null;
  }
}

/*
 * Collapses the node in the tree
 */
function collapseAll() {
  root.children.forEach(collapse);
  update(root);
}

/*
 * Expands the node d and all the children nodes of d
 * @param {node} d
 */
function expand(d) {
  if (d._children) {
    d.children = d._children;
    d._children = null;
  }
  if (d.children) {
    d.children.forEach(expand);
  }

}
/*
 * Expands all the nodes in the tree
 */
function expandAll() {
  root.children.forEach(expand);
  update(root);
}
.node {
  cursor: pointer;
}

.node circle {
  fill: #fff;
  stroke: steelblue;
  stroke-width: 1.5px;
}

.node text {
  font: 10px sans-serif;
}

.link {
  fill: none;
  /*stroke: steelblue;*/
  opacity: 0.3;
  /*stroke-width: 1.5px;*/
}

#levels {
  margin-left: 120px;
}
<body>
  <script src="http://d3js.org/d3.v4.min.js"></script>
  <button type="button" onclick="collapseAll()">Collapse All</button>
  <button type="button" onclick="expandAll()">Expand All</button>
  <div id="viz"></div>
</body>

Most likely I'm just blind and you'll instantly see what I did wrong, but I've been staring at this for a while now...

Thanks in advance for any help!

kinet
  • 1,790
  • 3
  • 20
  • 32

1 Answers1

1

I'm currently working on a similar project myself and figured this could be pretty helpful.. It seems you were really close, just needed to adjust a few minor paths to reach parent/child nodes..

Basically "source" is now "parent" and there is no "target". You can see most of the updates where commented out lines are v3 with v4 updates just below. For example:

link.target.id --> link.id
link.source.children.length --> link.parent.children.length
link.source.size --> link.parent.data.size

There's a few other miscellaneous updates throughout the code as well. The one thing I couldn't get working fully were the "Expand All/Collapse All" buttons. "Expand All" seems to work ok, but "Collapse All" seems to leave the link paths alone..

Here's a working fiddle: https://jsfiddle.net/jufra0b2/

I suspect there's something that can be done to the exit link but not sure. Anyways it's a step in the right direction. Hope you figure out the rest ok..

Rick
  • 712
  • 5
  • 23