首页 > 解决方案 > 在 d3.js v6 中以编程方式打开嵌套、折叠(隐藏)节点

问题描述

后续问题/问题

在 d3.js v4 中以编程方式打开嵌套、折叠(隐藏)节点

为 d3.js v6 更新。问题是在 d3 可折叠菜单可视化中加载外部 JSON 数据,以及嵌套(折叠、隐藏)节点的编程访问。

似乎没有传递作为加载对象的“treeData”。

Uncaught ReferenceError: treeData is not defined

JSFiddle: https ://jsfiddle.net/vstuart/kant09hm/6/

JSFiddle(已更新答案): https ://jsfiddle.net/vstuart/kant09hm/9/


ontology_for_d3_test.json

{ "name": "Root",
  "children": [
    { "name": "Culture",
      "children": [
        { "name": "Entertainment" },
        { "name": "LGBT" }
      ]
    },

    { "name": "Nature",
      "id": "nature",
      "children": [
        { "name": "Earth",
          "id": "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",
      "children": [
          { "name": "Wikipedia",
            "url": "https://wikipedia.com" },
          { "name": "Example.com",
            "url": "https://example.com" }
      ]
    },
      
    { "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" }
      ]
    }
  ]
}

index.html [JSFiddle 的独立工作副本;编辑:更新答案]

<!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 {
      width: 75%;
      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.v4.min.js"></script> -->
  <!-- <script src="https://d3js.org/d3.v5.min.js"></script> -->
  <script src="https://d3js.org/d3.v6.min.js"></script>
</head>

<body>

  <div id="d3_object">
    <object>
      <p>apple</p>
      <div id="includedContent"></div>
      <p>banana</p>
    </object>
  </div>

  <script type="text/javascript">
    // Set the dimensions and margins of the diagram
    var margin = {top: 20, right: 90, bottom: 30, left: 90},
        width = 960 - margin.left - margin.right,
        height = 500 - margin.top - margin.bottom;

    // ----------------------------------------
    // PAN, ZOOM:

    // https://www.d3-graph-gallery.com/graph/interactivity_zoom.html
    // var svg = d3.select("body").append("svg")

    var svg = d3.select("#includedContent").append("svg")
        .attr("width", width + margin.right + margin.left)
        .attr("height", height + margin.top + margin.bottom)
        // d3.js v4, v5:
        // .call(d3.zoom().on("zoom", function () {
        //   svg.attr("transform", d3.event.transform)
        // d3.js v6:
        .call(d3.zoom().on("zoom", function (event) {
          svg.attr("transform", event.transform)
        }))
      .append("g")
        .attr("transform", "translate("
              + margin.left + "," + margin.top + ")");
    // ----------------------------------------

    var i = 0,
        duration = 250,
        root;

    // declares a tree layout and assigns the size
    var treemap = d3.tree().size([height, width]);

    // ----------------------------------------
    // LOAD THE EXTERNAL DATA:
    //d3.json("https://gist.githubusercontent.com/mbostock/4339083/raw/9585d220bef18a0925922f4d384265ef767566f5/flare.json", function(error, treeData) {

    // ----------------------------------------
    // d3.js v4 [ https://d3js.org/d3.v4.min.js ]
    /*
      d3.json("https://gist.githubusercontent.com/victoriastuart/abbcf355bf1590be02f6dec297be2706/raw/2418e5f6b7626b3c5842665a51b7d0d27f74e909/ontology_for_d3_test.json", function(error, treeData) {
        if (error) throw error;
        // 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);
      });
    */
    // ----------------------------------------

    // ----------------------------------------
    // d3.js v5   https://d3js.org/d3.v5.min.js
    //            https://gist.github.com/d3noob/1a96af738c89b88723eb63456beb6510 
    // d3.js v6   https://d3js.org/d3.v5.min.js
    //            https://gist.github.com/d3noob/9de0768412ac2ce5dbec430bb1370efe

    // https://stackoverflow.com/questions/49768165/code-within-d3-json-callback-is-not-executed
    // https://www.tutorialsteacher.com/d3js/loading-data-from-file-in-d3js
    // https://stackoverflow.com/questions/47664292/d3-json-method-doesnt-return-my-data-array

    // ----------------------------------------
    // LOAD EXTERNAL JSON DATA FILE (via PROMISE):

    //   https://stackoverflow.com/questions/49768165/code-within-d3-json-callback-is-not-executed
    //   https://stackoverflow.com/questions/49534470/d3-js-v5-promise-all-replaced-d3-queue
    //   https://www.roelpeters.be/explaining-promises-in-d3-js-the-what-and-the-why/
    //
    //   https://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call

    // This callback function should return the "treeData" Object:
    d3.json("https://gist.githubusercontent.com/victoriastuart/abbcf355bf1590be02f6dec297be2706/raw/2418e5f6b7626b3c5842665a51b7d0d27f74e909/ontology_for_d3_test.json")
      .then(function(treeData) { 
        console.log('[d3.js] 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);

        // ----------------------------------------
        // Per answers at
        //   https://stackoverflow.com/questions/67527258/programmatically-open-nested-collapsed-hidden-node-in-d3-js-v4/67530786?noredirect=1#comment119390942_67530786
        //   https://jsfiddle.net/mrovinsky/ujwsd7qz/
        //   https://stackoverflow.com/questions/67549992/accessing-promised-data-in-d3-js-v6-programmatically-opening-nested-collapsed-n

        const findNodeAncestors = (root, name) => {
          if (root.name === 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, 'Earth');

        if (chain) {
          console.log('[d3.js] chain:', chain)

          for (let i = chain.length - 1; i >= 0; i--) {
            const node = d3.select(`.node[node-name="${chain[i]}"]`);
            const nodeData = node.datum();
            if (!nodeData.children && nodeData.data.children) {
              node.node().dispatchEvent(new Event('click'));
            }
          }
        }
        else {
          console.log('[d3.js] "chain" is either "null" or "undefined"')
        }
        // ----------------------------------------

      })
      // IF (ERROR) THROW ERROR:
      .catch(function(error) {
        console.log('[d3.js] JSON callback function 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')
          // --------------------------------------------
          // Per answer at
                  //   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(d) {
      // ***** New in 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.
          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);
      }
      // ----------------------------------------
    }

    // ----------------------------------------
    // Per answer at
    //   https://stackoverflow.com/questions/67480339/programmatically-opening-d3-js-v4-collapsible-tree-nodes
    ///*
      setTimeout(() => {
        //const node = d3.select('.node[node-name="Earth"]').node();
        //const node = d3.select('.node[node-name="Nature"]').node();
        const node = d3.select('.node[node-name="Society"]').node();
        console.log('[setTimeout()] NODE: ', node);
        node.dispatchEvent(new Event('click'));
      }, 2500);
    //*/
    // ----------------------------------------

    // ----------------------------------------
    // Per answer at
    //   https://stackoverflow.com/questions/67527258/programmatically-open-nested-collapsed-hidden-node-in-d3-js-v4/67530786?noredirect=1#comment119390942_67530786
    //   https://jsfiddle.net/mrovinsky/ujwsd7qz/

    const findNodeAncestors = (root, name) => {
      if (root.name === 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, 'Earth');
    // Console: "Uncaught ReferenceError: treeData is not defined"
    
    for (let i = chain.length - 1; i >= 0; i--) {
      const node = d3.select(`.node[node-name="${chain[i]}"]`);
      const nodeData = node.datum();
      if (!nodeData.children && nodeData.data.children) {
        node.node().dispatchEvent(new Event('click'));
      }
    }
    // ----------------------------------------

  </script>
</body>
</html>

标签: javascripthtmld3.jspromisecallback

解决方案


treeData变量只能在定义为参数的函数范围内使用:

d3.json("https://...json")
  .then(function(treeData) {
    // Scope of the function, treeData can be used here
  });  

// Out of scope, treeData is not defined

const findNodeAncestors...解决方案是在函数体内移动以(〜25行)开头的块(在最后一次出现右update括号之后):

        update(d);
      }
      // ----------------------------------------
      // HERE
    }

推荐阅读