0

I am working on an angular application with d3. My code is as follows.

var treeData = [{
  "name": "MD",
  "children": [{
    "name": "Professional",
    "children": [{
      "name": "Third A",
      "children": [{
        "name": "Fourth A",
        "children": [{
          "name": "Fifth A"
        }, {
          "name": "Fifth B"
        }, {
          "name": "Fifth C"
        }, {
          "name": "Fifth D"
        }]
      }, {
        "name": "Fourth B"
      }, {
        "name": "Fourth C"
      }, {
        "name": "Fourth D"
      }]
    }, {
      "name": "Third B"
    }]
  }, {
    "name": "Leader",
    "children": [{
      "name": "Third C"
    }, {
      "name": "Third D"
    }]
  }, {
    "name": "Advocate",
    "children": [{
      "name": "Third E"
    }, {
      "name": "Third F"
    }]
  }, {
    "name": "Clinician",
    "children": [{
      "name": "Third G"
    }, {
      "name": "Third H"
    }, ]
  }, ]
}];



var colourScale = d3.scale.ordinal()
  .domain(["MD", "Professional", "Leader", "Advocate", "Clinician"])
  .range(["#6695c8", "#cd3838", "#d48440", "#a8ba5f", "#63b7c0"]);


// ************** Generate the tree diagram  *****************
var margin = {
    top: 20,
    right: 120,
    bottom: 20,
    left: 120
  },
  width = 1200 - margin.right - margin.left,
  height = 650 - margin.top - margin.bottom;

var i = 0,
  duration = 750,
  root;

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

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


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 + ")");

root = treeData[0];
root.x0 = height / 2;
root.y0 = 0;

update(root);

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


// 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) {
  console.log('UPDATE')

  // 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 * 200;
  });

  // 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("r", 1e-6)
    .style("fill", function(d) {
      return d._children ? "#C0C0C0" : "#fff";
    });

  nodeEnter.append("text")
    .attr("x", function(d) {
      return d.children || d._children ? -13 : 13;
    })
    .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);
    
             nodeEnter.append('foreignObject').attr('width', '20')
        .attr("x", 10)
        .attr("y", 1)
        .attr('height', '20').append('xhtml:input')
        .attr('type', 'checkbox')
        .attr("id", d => `checkbox-${d.id}`)
        //.attr("fill","none")
        //.style("opacity","1")
        // An on click function for the checkboxes
        .on("click", d => {
           if (d.children) {
             d.children.forEach(child => {
               const cb = d3.select(`#checkbox-${child.id}`);
               //console.log('CB: ', cb.node());
               cb.node().checked = d3.event.target.checked;
               cb.attr('disabled', d3.event.target.checked ? true : null);
             })
           }
           else {
             if (d3.event.target.checked) {
               d.parent.children.forEach(child => {
             console.log('CID: ', child.id, d.id);
               if (child.id !== d.id) {
const cb = d3.select(`#checkbox-${child.id}`);
console.log('CB: ', cb.node())
cb.node().checked = false;               
               
               } 
               
                 
               
               });
           }
           }
           d3.event.stopPropagation();
         //console.log(d);
         //console.log(d3.event.target.checked);
        })

  // 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", 10)
    .attr("fill-opacity", "0.7")
    .attr("stroke-opacity", "1")
    .style("fill", function(d) {
        return (typeof d._children !== 'undefined') ? (colourScale(findParent(d))) : '#FFF';
    })
    .style("stroke", function(d) {
      return colourScale(findParent(d));
    });
    
  

  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.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("opacity", "0.3")
    .style("stroke", function(d) {
      return colourScale(findParentLinks(d));
    });

  // Transition links to their new position.
  link.transition()
    .duration(duration)
    .attr("d", diagonal);

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

function findParent(datum) {
  if (datum.depth < 2) {
    return datum.name
  } else {
    return findParent(datum.parent)
  }
}

function findParentLinks(datum) {
  if (datum.target.depth < 2) {
    return datum.target.name
  } else {
    return findParent(datum.target.parent)
  }
}


// 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);
}
.node {
  cursor: pointer;
}

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

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

.link {
  fill: none;
  stroke: #C0C0C0;
  stroke-width: 1.5px;
}

I am facing following problem:

If a collapsed node has children, and its parent is getting checked, the node itself is checked as well, but its children cannot be checked because they are not visible. The checking should work recursively: if a node is checked, all its descendants should be checked and disabled as well regardless their state (expanded or collapsed).

How can I do this?

R. Richards
  • 24,603
  • 10
  • 64
  • 64
Julie
  • 87
  • 5

1 Answers1

1

Here is a proposed solution:

const treeData = {
  "id": 1,
  "name": "Root",
  "checked": false,
  "color": "white",
  "children": [
    {
      "id": 2,
        "name": "Leaf A",
      "checked": false,
      "color": "red",
        "children": [
        {
          "id": 3,
          "name": "A - 1",
                "checked": false,
          "color": "brown",
          }, 
        {
          "id": 4,
          "name": "A - 2",
          "checked": false,
          "color": "orange",
          },
        {
          "id": 5,
          "name": "A - 3",
                "checked": false,
          "color": "yellow",
          },
      ]
    }, 
    {
      "id": 6,
        "name": "Leaf B",
      "checked": false,
      "color": "green",
        "children": [
        {
          "id": 7,
          "name": "B - 1",
          "checked": false,
          "color": "#00ff40",
          }, 
        {
          "id": 8,
          "name": "B - 2",
          "checked": false,
          "color": "#00ff80",
          }
      ]
    }
  ]  
};

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

var i = 0,duration = 750;
  
const tree = d3.layout.tree()
  .size([height, width]);

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


const svg = d3.select("body").append("svg")
  .attr("width", width + margin.right + margin.left)
  .attr("height", height + margin.top + margin.bottom);
  
const container = svg.append("g")
  .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

const root = treeData;
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) {
  // Compute the new tree layout.
  const nodes = tree.nodes(root).reverse();
  const links = tree.links(nodes);

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

  // Update the nodes…
  const node = container.selectAll("g.node")
    .data(nodes, d => d.id);

  // Enter any new nodes at the parent's previous position.
  var nodeEnter = node.enter()
    .append("g")
    .attr("class", "node")
    .attr("transform", d => `translate(${source.y0},${source.x0})`)
    .on("click", onClickNode);

  nodeEnter.append("circle")
    .attr("r", 10)
    .style("fill", d => d.color);

  nodeEnter.append("text")
    .attr("x", 20)
    .attr("dy", 4)
    .attr("text-anchor", "start")
    .text(d => d.name);
    
  nodeEnter.append('foreignObject')
    .attr('width', '20')
    .attr('height', '20')
    .attr("x", -30)
    .attr("y", -8)
    .append('xhtml:input')
    .attr('type', 'checkbox')
    .attr("id", d => `checkbox-${d.id}`)
    .on("click", onClickCheckbox)

  // 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")
    .style("stroke", 'black');
  nodeUpdate.each(function(d) {
    const cb = d3.select(this).select('[type="checkbox"]').node();
    cb.checked = d.checked;
    cb.disabled = isParentChecked(d);
  });
    
  nodeUpdate.select("text")
    .style("fill-opacity", 1);

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

  nodeExit.select("circle")
    .attr("r", 0);

  nodeExit.select("text")
    .style("fill-opacity", 0);

  // Update the links…
  var link = container.selectAll("path.link")
    .data(links, d => d.target.id);

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

  // Transition links to their new position.
  link.transition()
    .duration(duration)
    .attr("d", diagonal);

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

function findParent(datum) {
  if (datum.depth < 2) {
    return datum.name
  } else {
    return findParent(datum.parent)
  }
}

function findParentLinks(datum) {
  if (datum.target.depth < 2) {
    return datum.target.name
  } else {
    return findParent(datum.target.parent)
  }
}

const checkNode = (d, checked, byParent) => {
  if (d.id === 2)
    console.log('CHECK TO: ', checked);
  d.checked = checked;
  const children = d.children || d._children;
  if (children)
    children.forEach(child => checkNode(child, checked, true));
    
  if (!byParent && checked && d.parent) {
    console.log('UNCHECK SIBLINGS');
    
    const siblings = d.parent.children || d.parent._children;
    
    siblings.forEach(sibling => {
      if (sibling.id !== d.id) {
        console.log('UNCHECK: ', sibling)
        checkNode(sibling, false, true);
      }
    });
    
  }  
  
}

function isParentChecked (d) {
  if (!d.parent) {
    return false;
  }
  if (d.parent.checked) {
    return true;
  }
  return isParentChecked(d.parent);
}

function onClickCheckbox(d) {
  d3.event.stopPropagation();
    checkNode(d, d3.event.target.checked, false);
  console.log('ROOT: ', root);
  update(root);   
}


// Toggle children on click.
function onClickNode(d) {
  if (d.children) {
    d._children = d.children;
    d.children = null;
  } 
  else {
    d.children = d._children;
    d._children = null;
  }
  update(d);
}
.node {
  cursor: pointer;
}

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

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

.link {
  fill: none;
  stroke: #C0C0C0;
  stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
Michael Rovinsky
  • 6,807
  • 7
  • 15
  • 30
  • 1
    Hi Michael your code worked like charm. I will be posting 3 more questions related to d3.js. Will you able to help me out on those 3 questions too? – Julie May 18 '21 at 04:58
  • Hi Julie, sure :) I will appreciate your upvote on this answer as well – Michael Rovinsky May 18 '21 at 05:10
  • 1
    ok I will post the question and will share link here in comment section. Thanks – Julie May 18 '21 at 05:12
  • 1
    Hi Michael, I have posted first question. https://stackoverflow.com/questions/67580844/changing-diagonal-link-color-in-d3-js-on-the-basis-of-whether-node-is-checked-or Request you to please have a look at it and help – Julie May 18 '21 at 06:16
  • Hi Michael, I have posted second question https://stackoverflow.com/questions/67640038/checking-node-and-its-all-childrens-on-the-basis-of-id . Request you to please have a look and help – Julie May 21 '21 at 15:44
  • Hi Michael could you please help me? – Julie May 21 '21 at 17:24