javascripthtmlcssd3.jsr2d3

How to add custom conditional colors in D3 tree nodes?


I am learning D3js and I would like to use condition to assign colors to nodes of a tree diagram. For example, if the type of data is "str", the node color will become "read" or "green" if it is "elem".

I refer this thread and add the condition, but it turns out all nodes become black instead of "red" or "green".

Does anyone know the reason on this behavior?

Here is D3 code:

// Set the dimensions and margins of the diagram
//var treeData = data
var treeData = data;

var margin = {top: 20, right: 90, bottom: 30, left: 90},
    width = width - margin.left - margin.right,
    height = height - 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 = svg.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);

  // Add Circle for the nodes
  nodeEnter.append('circle')
      .attr('class', 'node')
      .attr('r', 1e-6)
      .style("fill", function(d) {
          if(d.type == "str") return "red";
          if(d.type == "elem") return "green";
      });

  // 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) {
          if(d.type == "str") return "red";
          if(d.type == "elem") return "green";
     })
    .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);
  }
}

data:

{
  "type": "str",
  "name": "Canada",
  "children": [
    {
      "type": "elem",
      "name": "Newfoundland",
      "children": [
        {
          "type": "str",
          "name": "St. John's"
        }
      ]
    },
    {
      "type": "elem",
      "name": "PEI",
      "children": [
        {
          "type": "str",
          "name": "Charlottetown"
        }
      ]
    },
    {
      "type": "elem",
      "name": "Nova Scotia",
      "children": [
        {
          "type": "str",
          "name": "Halifax"
        }
      ]
    }
  ]
}

Solution

  • D3-hiearchy produces a new data array, to avoid collisions between properties of the hierarchy and properties of the data, it places the data associated with a given node into a data property.

    D3-hierarchy does this to preserve the original data, for example, your data has a children property, d3-hierarchy produces a set of nodes that also have a children property, so to keep both and avoid collisions, the original data is moved to the data property.

    So instead of d.type you need to use d.data.type.

    For reference, each node returned by d3-hierarchy has only the following properties:

    node.data - the associated data, as specified to the constructor.
    node.depth - zero for the root node, and increasing by one for each descendant generation.
    node.height - zero for leaf nodes, and the greatest distance from any descendant leaf for internal nodes.
    node.parent - the parent node, or null for the root node.
    node.children - an array of child nodes, if any; undefined for leaf nodes.
    node.value - the summed value of the node and its descendants; optional, see node.sum and node.count.
    

    (from docs)