javascripthtmld3.jspromisecallback

Programmatic access of data in d3.js v6 collapsible tree via selectors


Update [2021-05-27: 8d later] I solved this myself -- see the accepted answer. I edited my original question below for brevity, removing portions that are no longer relevant to the issue/solution.

A copy of the original, problematic code (HTML file, which at that time contained the application-specific d3.js code embedded within that file) can be found here.

https://gist.githubusercontent.com/victoriastuart/83f1de548ff2de4dda60ccbd0da937aa/raw/38a343e11bead986f0acb38fcb33f64c730bc103/ontology-d3jsv6.html

In that code, I used functions to load the JSON data and search and retrieve nodes.

The solution simplifies that process, no longer relying on those functions.


Original question (paraphrased)

I am encountering an issue regarding the programmatic access of data loaded into a d3.js v6 collapsible tree via a promise.

The main issue is that once I load the JSON data into the d3 visualization, after the first access I can not re-acess those data.

That is, I appear to be "stuck" in the initial data load (which I interpret as being due to the promise/ callbacks not exiting - and/or other unidentified coding issues).

not working:

d3.js ontology demo - 2021.05.19


Solution

  • I managed to sort this out.

    Complete code follows.


    working!

    d3 ontology demo - 2021.05.27


    ontology-d3jsv6e.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">
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    
      <title>My Ontology</title>
    
      <link rel="stylesheet" href="d3jsv6.css">
    
      <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>
    </head>
    
    <body>
      <div id="d3_object">
        <object>
          <div id="includedContent"></div>
        </object>
      </div>
        <div id="controls">
          <ul>
            <span class="dropdown_item">
              Node:
              <select class="dropdown_item" name="dropdown_item"></select>
            </span>
    
            <button class="reset">Reset</button>&nbsp;
            <button onclick="window.location.reload(false)">Reload</button>
          </ul>
        </div>
    
        <script src="ontology-d3jsv6e.js"></script>
    
    </body>
    </html>
    
    

    d3jsv6.css

    .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;
    }
    

    ontology.json

    { "name": "Root",
      "children": [
        { "name": "Culture",
          "children": [
            { "name": "LGBT" }
          ]
        },
    
        { "name": "Nature",
          "children": [
            { "name": "Earth",
              "children": [
                { "name": "Environment" },
                { "name": "Geography" },
                { "name": "Geology" },
                { "name": "Geopolitical" },
                { "name": "Geopolitical - Countries" },
                { "name": "Geopolitical - Countries - Canada" },
                { "name": "Geopolitical - Countries - United States" },
                { "name": "Nature" },
                { "name": "Regions" }
              ]
            },
            { "name": "Cosmos" },
            { "name": "Outer space" }
          ]
        },
    
        { "name": "Humanities",
          "children": [
              { "name": "History" },
              { "name": "Philosophy" },
              { "name": "Philosophy - Theology" }
          ]
        },
    
        { "name": "Miscellaneous" },
    
        { "name": "Science",
          "children": [
              { "name": "Biology" },
              { "name": "Health" },
              { "name": "Health - Medicine" },
              { "name": "Sociology" }
          ]
        },
    
        { "name": "Technology",
          "children": [
                { "name": "Computers" },
                { "name": "Computers - Hardware" },
                { "name": "Computers - Software" },
                { "name": "Computing" },
                { "name": "Computing - Programming" },
                { "name": "Internet" },
                { "name": "Space" },
              { "name": "Transportation" }
          ]
        },
    
      { "name": "Society",
          "children": [
                { "name": "Business" },
                { "name": "Economics" },
                { "name": "Economics - Business" },
                { "name": "Economics - Capitalism" },
                { "name": "Economics - Commerce" },
                { "name": "Economics - Finance" },
                { "name": "Politics" },
              { "name": "Public services" }
          ]
        }
      ]
    }
    

    ontology-d3jsv6e.js

    // ============================================================================
    // INITIALIZATION:
        var i = 0,
            duration = 250,
            root;
    
        var margin = {top: 20, right: 90, bottom: 30, left: 90},
            width = 1500 - margin.left - margin.right,
            height = 600 - margin.top - margin.bottom;
    
        var svg = d3.select("#includedContent").append("svg")
            .attr("width", width + margin.right + margin.left)
            .attr("height", height + margin.top + margin.bottom)
    
            // ----------------------------------------
            // PAN, ZOOM:
            .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 DATA:
    
    d3.json('../../../../js/ontology.json', hello())
        .then(d => {
            setup(d);
            update(d);
        })
        .catch(function(error) {
            console.log('Error: failed promise')
            if (error) throw error;
        });
    
    function hello() {
      console.log('*********************\n** Hello Victoria! **\n*********************')
    };
    
    // ============================================================================
    // SETUP VISUALIZATION:
    
    function setup(myTree) {
        // console.log('[setup()] myTree:', this.myTree)
    
        // ASSIGN PARENT, CHILDREN, HEIGHT, DEPTH:
        root = d3.hierarchy(myTree, function(d) { return d.children; });
    
        root.x0 = height / 2;
        root.y0 = 0;
    
        // COLLAPSE AFTER THE SECOND LEVEL:
        root.children.forEach(collapse);
    
        update(root);
    
        let variables = myTree.children;
    
        // POPULATE DROPDOWN SELECTOR:
        d3.select('select.dropdown_item')
            .on("change", function() {
                // console.log('value:', this.value)
                // const node = d3.select(`.node[node-name="Nature"]`);
                const node = d3.select(`.node[node-name=${this.value}]`);
                const nodeData = node.datum();
                if (!nodeData.children && nodeData.data.children) {
                    node.node().dispatchEvent(new Event('click'));
                }
            })
            .selectAll('option')
            .data(variables)
            .enter()
            .append('option')
            // .attr('value', d => d.children[0].name)
            .attr('value', d => d.children.name)
            .text(d => d.name);
    
        // RESET ONTOLOGY:
        d3.select('button.reset')
            .on("click", function() {
                setup(myTree)
            });
    };
    
    // ============================================================================
    // 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
        }
    }
    
    // ============================================================================
    // UPDATE THE VISUALIZATION:
    
    function update(source) {
    
        // ASSIGN 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('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:
    
        // d3.js v5:
        //   function click(d) {
        // d3.js v6:
        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:
            window.location = d.data.url;
            // window.open("https://www.example.com", "_self");
        }
        update(d);
        }
        // ----------------------------------------
        // return;
    }
    // ============================================================================