Accessing promised data in d3.js v6 collapsible tree

  callback, d3.js, html, javascript, promise

An extension of my previous question,

Programmatically open nested, collapsed (hidden) nodes in d3.js v6

The issue I am struggling with is programmatically accessing JSON data visualized in the D3 collapsible trees, with those data loaded via a promise in d3.js.

While that issue is illustrated in the following JSFiddle,

https://jsfiddle.net/vstuart/fzgqxr17/20/

the code is better accessed (and the issues better described) in the standalone HTML file provided below. Text appended to that page further clarifies those issues.

enter image description here


ontology-d3jsv6.html

<!DOCTYPE html>
<html lang="en-US" xmlns:xlink="http://www.w3.org/1999/xlink">

<head>
  <meta content="text/html; charset=utf-8" http-equiv="Content-Type">

  <style>
    .node {
      cursor: pointer;
    }

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

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

    .link {
      fill: none;
      stroke: #ccc;
      stroke-width: 2px;
    }

    #includedContent {
      position: static !important;
      display: inline-block;
    }

    #d3_object {
      border: 1px solid darkgray;
      overflow: auto;
      width: 80%;
      margin: 0.5rem 0.5rem 1rem 0.25rem;
    }
  </style>

  <script type="text/javascript" src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>

  <script src="https://d3js.org/d3.v6.min.js"></script>

  <script type="text/javascript">
    function findNode(node) {
      console.log('[findNode()] node:', node)
      myOntology(node);
      // ----------------------------------------
    }
  </script>
</head>

<body>

  <div id="d3_object">
    <object>
      <div id="includedContent"></div>
    </object>
  </div>

  <script type="text/javascript">
    var i = 0,
        duration = 250,
        root;

    // ----------------------------------------
    // SET THE DIMENSIONS AND MARGINS OF THE DIAGRAM:
    var margin = {top: 20, right: 90, bottom: 30, left: 90},
        width = 1000 - margin.left - margin.right,
        height = 400 - margin.top - margin.bottom;

    // ----------------------------------------
    // PAN, ZOOM:
    var svg = d3.select("#includedContent").append("svg")
      .attr("width", width + margin.right + margin.left)
      .attr("height", height + margin.top + margin.bottom)
      .call(d3.zoom()
        .scaleExtent([0.25, 3])
        .on("zoom", function (event) {
          svg.attr("transform", event.transform)
        }))
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    // ----------------------------------------

    // ----------------------------------------
    // DECLARE A TREE LAYOUT, SIZE:
    var treemap = d3.tree().size([height, width]);

    // ----------------------------------------
    // LOAD THE EXTERNAL DATA (via PROMISE):

    // globalThis.treeData = {};
    function myOntology(node) {
      d3.json("https://gist.githubusercontent.com/victoriastuart/abbcf355bf1590be02f6dec297be2706/raw/2418e5f6b7626b3c5842665a51b7d0d27f74e909/ontology_for_d3_test.json")
      .then(function(treeData) { 
        console.log('----------------------------------------')
        console.log('[myOntology()] node:', node)
        console.log('[myOntology()] treeData:', treeData, '| type:', typeof(treeData), '| length:', treeData['children'].length)
        /*
        for (let i = 0; i < treeData['children'].length; i++) {
          console.log('node:', treeData['children'][i].name);
          if ( treeData['children'][i].id !== undefined ) {
            console.log('  id:', treeData['children'][i].id);
          }
        }
        */

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

        // ----------------------------------------
        // FUNCTION getNode() :

        // https://stackoverflow.com/questions/67527258/programmatically-open-nested-collapsed-hidden-node-in-d3-js-v4
        function getNode(node) {
          const findNodeAncestors = (root, name) => {
            if (root.name === name) {
              console.log('[getNode()] node:', name)
              return [name];
            }
            if (root.children)
              for (let i = 0; i < root.children.length; i++) {
                const chain = findNodeAncestors(root.children[i], name);
                if (chain) {
                  chain.push(root.name);
                  return chain;
                }
              }
            return null;
          }; 

          const chain = findNodeAncestors(treeData, node);
          console.log('[getNode()] chain:', chain)

          for (let i = chain.length - 1; i >= 0; i--) {
            console.log('[getNode()] chain[' + i + ']:', chain[i]);
            const node = d3.select(`.node[node-name="${chain[i]}"]`);
            const nodeData = node.datum();
            console.log('[getNode()] chain[' + i + '] node: ', node, '| type:', typeof(node));
            if (!nodeData.children && nodeData.data.children) {
              node.node().dispatchEvent(new Event('click'));
            }
          }
        }
        if (node == null) {
          console.log("'node == null'; setting to 'Root'")
          node = 'Root';
          getNode(node);
        }
        else {
          getNode(node);
        }
        // end, FUNCTION getNode() :
        // ----------------------------------------
      })
      // IF (ERROR) THROW ERROR:
      .catch(function(error) {
        console.log('myOntology(node) error')
        if (error) throw error;
      });
    }
    // ----------------------------------------

    // 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')
          // --------------------------------------------
                  // https://stackoverflow.com/questions/67480339/programmatically-opening-d3-js-v4-collapsible-tree-nodes
          .attr('node-name', d => d.data.name)
                  // --------------------------------------------
          .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) {
              return d._children ? "lightsteelblue" : "#fff";
          });

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

      // CREATE 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(event, d) {
        if (d.children) {
          d._children = d.children;
          d.children = null;
        } else if (d._children) {
          d.children = d._children;
          d._children = null;
        } else {
          // THIS WAS A LEAF NODE, SO REDIRECT:
          console.log('d:', d)
          console.log('d.data:', d.data)
          console.log('d.name:', d.name)
          console.log('d.data.name:', d.data.name)
          console.log('urlMap[d.data.name]:', urlMap[d.data.name])
          window.location = d.data.url;
          // window.open("https://www.example.com", "_self");
        }
        update(d);
      }
      // ----------------------------------------
      return;
    }
  </script>


  <script type="text/javascript">
    // ----------------------------------------
    // FOR TESTING - ANY OF THESE SHOULD WORK SINGLY;
    // *** ISSUE: *** FAILS WHEN MORE THAN ONE IS UNCOMMENTED.

    // myOntology();
    // myOntology('Root');
    // myOntology('Culture');
    // myOntology('Earth');
    // ----------------------------------------
    // findNode();
    // findNode('Root');
    // findNode('Earth');
    // findNode('Culture');
    // findNode('Earth');

    // I AM LEAVING THOSE COMMENTED OUT, AS I AM MUCH
    // MORE INTERESTED IN GETTING THE FOLLOWING HTML
    // ELEMENTS TO EXECUTE.
    // ----------------------------------------
  </script>

  <div>
    <input type="text" id="apple" style="font-size:16px" autocomplete="off" size="45" placeholder="Enter ontology term (e.g. Earth | click for 1st level items)" list="ontology_items" autofocus="true" onselect="findNode(value)">&nbsp;

    <button onclick="findNode('Root')">Reset ontology</button>&nbsp;
    <button onclick="window.location.reload(false)">Reload page</button>

    <datalist id="ontology_items">
      <option>Culture</option>
      <option>Nature</option>
      <option>Earth</option>
      <option>Humanities</option>
      <option>Miscellaneous</option>
      <option>Science</option>
      <option>Technology</option>
      <option>Society</option>
    </datalist>

    <p>Hyperlink test: <a onclick="findNode('Earth')" href="#">findNode('Earth')</a></p>
  </div>

  <div>
    <p><b>Notes</b></p>
    
      <ul>
        <li><p>JSON (ontology) data is loaded (<code><b>d3.json().then(...{...})</b></code>) via a function that delivers a promise, <code><b>myOntology(node){...}</b></code>.</p></li>

        <li><p>I call <code><b>myOntology(node){...}</b></code> with a function, <code><b>findNode(node){...}</b></code> that is located in the &lt;head&gt;</code>-element.</p></li>

        <li><p><code><b>myOntology(node){...}</b></code> contains an embedded function, <code><b>getNode(node){...}</b></code>.</p></li>
      </ul>
      
     <p><b><font color="magenta">ISSUES</font></b></p>
    <ul>
      <li><p>Inexperienced with JavaScript promises and d3.js, I am having trouble in programmatically accessing the JSON data.</p></li>

      <li><p>While I can singly access nodes via</p></li>
        <ul>
          findNode('Earth');<br/>
          findNode('Culture');<br/>
          etc.
        </ul>
      (replicated in the dropdown input box, above), I get stuck after <code><b>myOntology(node){...}</b></code> is called the first time (delivering the promised data). That is, when I attempt a second query of those data, the ontology "resets" and fails to find the newly-queried nodes.

      <li><p>That issue appears to be the reloading of the data with each query, via the <code><b>findNode(node){...}</b></code> - that each time run reloads the data via <code><b>myOntology(node){...}</b></code>, but gets stuck there.</p></li>
      
      <li><p>When this issue is resolved, the input box and the buttons (above) should all execute properly.</p></li>
    </ul>
  </div>

</body>
</html>

Source: Ask Javascript Questions

LEAVE A COMMENT