0

I am working on an angular application with d3 js. I have jsFiddle below

https://jsfiddle.net/ksc3pxh9/1/

In my code for each node I have a checkbox as shown in fiddle. Code for checkbox is as follows

         nodeEnter.append('foreignObject').attr('width', '20')
    .attr("x", 200 / 2)
    .attr("y", 1)
    .attr('height', '20').append('xhtml:input')
    .attr('type', 'checkbox')
    .attr("id", "checkbox")
    .attr("fill","none")
    .style("opacity","1")
    // An on click function for the checkboxes
    .on("click",function(d) {
       d3.event.stopPropagation();
     console.log(d);
    })

For check boxes I want to check and uncheck checkboxes on the basis of some conditions/relations, which are as follows.

  1. If user checks parent checkbox, then checkboxes for childrens also should be checked and disabled. Means if parents is checked then childrens are also automatically should be checked and user should not be able to uncheck childrens in this case. if we uncheck the parent then automatically all children becomes uncheck and enable

  2. If user checks any children then it's siblings and parent will be unchecked and enabled. Means user can make siblings/parent checked at any point. In this is if parent is checked then I should behave like point 1

How can I implement this?

Julie
  • 87
  • 5
  • You have a logical problem here. If you check the parent, you get all the children checked. Now, if you uncheck the parent, you get a few enabled and checked children, while your requirement is to keep only one checked child if the parent is not checked. Try to build a scenario and see how you can handle all the possible cases – Michael Rovinsky May 16 '21 at 09:19
  • 1
    if we uncheck the parent then automatically all children becomes uncheck and enabled – Julie May 16 '21 at 09:24
  • I don;t know how to do this as I am new to d3 that;s why I asked here – Julie May 16 '21 at 09:24
  • @MichaelRovinsky could you please help in this? I am not able to find any example for this nor I am able to do it myselfd as I am very new to d3 – Julie May 16 '21 at 13:02
  • I've posted an answer. I still think there is an ambiguity for the nodes that are both parents and children, but that's a solution I can provide based on your fiddle. My fiddle is here: https://jsfiddle.net/mrovinsky/eb0mLuhv/ – Michael Rovinsky May 16 '21 at 13:26

1 Answers1

0

Assign a unique id attribute to each checkbox:

.attr("id", d => `checkbox-${d.id}`)

then check/uncheck the boxes using id:

.on("click", d => {
  if (d.children) { // Node has children
    d.children.forEach(child => { // check and disable / uncheck and enable all
      const cb = d3.select(`#checkbox-${child.id}`);
      cb.node().checked = d3.event.target.checked;
      cb.attr('disabled', d3.event.target.checked ? true : null);
    })
  }
  else { // Node does not have children
    if (d3.event.target.checked) { // Uncheck other siblings 
      d.parent.children.forEach(child => {
        if (child.id !== d.id) {
          d3.select(`#checkbox-${child.id}`).node().checked = false;               
        } 
      });
    }
  }
  d3.event.stopPropagation();
})

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;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
Michael Rovinsky
  • 6,807
  • 7
  • 15
  • 30
  • most of the scenarios are working fine except one. if children has subchildrens. so I check parent then immediate children becomes checked and disabled but subchildrens still can be checked and enabled. Logically if parent is checked then all children and sub childrens should be checked and disabled. I have updated my fiddle with your code https://jsfiddle.net/zhk0Lxes/3/ – Julie May 16 '21 at 15:46
  • could you please help to solve above issue? – Julie May 16 '21 at 15:57
  • michael could you please update your code for above issue? – Julie May 16 '21 at 16:00
  • Well, I see you've changed my original code and the exclusive sibling check does not work anymore... Why? – Michael Rovinsky May 16 '21 at 16:07
  • I was doing some testing. You can put it there. But my change didn't created the issue which I mentioned above – Julie May 16 '21 at 16:09
  • There is another logical problem: If I check an item and its sibling was previously checked, I should uncheck not only the sibling itself, but all its descendants recursively... – Michael Rovinsky May 16 '21 at 16:11
  • yes. Could you please update your code for these two scenarios. That will be very helpful – Julie May 16 '21 at 16:13
  • You need to change your data model: First, the check flag should be a part of your data. Second, each time you click a checkbox, you don't change the state of other checkbox, but change the __data__ itself and then render the whole tree with the updated data. I hardly see it can work another way – Michael Rovinsky May 16 '21 at 16:23
  • sorry didn't got it – Julie May 16 '21 at 16:25
  • When you collapse or expand a node, the tree is updated. The same should be done when you check a node - you go through your data, check/uncheck the nodes, then call update – Michael Rovinsky May 16 '21 at 16:30
  • how calling the update will solve the problem which I mentioned in my first comment? – Julie May 16 '21 at 16:59
  • When the parent node is collapsed, the child is not rendered and you cannot check or uncheck it. What you can do, is to change the check attribute of the child node in the tree data, and when the parent is expanded and tree is re-rendered, the child will be created with chacked and disabled checkbox – Michael Rovinsky May 16 '21 at 19:24
  • I understand it's difficult at once, so maybe it's a good idea to split the issue and post a new question with the recursive problem. We obviously have some progress here with the visible checkboxes and will try to solve the recursion issue as well. I will appreciate very much if you mark my answer as correct and upvote it and will try to help with the next question :) – Michael Rovinsky May 16 '21 at 19:31
  • posted new question as you said. Please help https://stackoverflow.com/questions/67561002/making-childrens-checked-and-disabled-if-parent-node-is-checked-in-d3-tree – Julie May 16 '21 at 19:54
  • Hi Michael, Could you please help me in that recursion issue? – Julie May 17 '21 at 06:27
  • Good morning, I'm trying to fix it right now – Michael Rovinsky May 17 '21 at 06:51
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/232498/discussion-between-michael-rovinsky-and-julie). – Michael Rovinsky May 17 '21 at 07:38