【问题标题】:Drawing a collapsible indented tree with d3使用 d3 绘制可折叠的缩进树
【发布时间】:2014-12-30 15:22:06
【问题描述】:

正在用 d3 绘制一个缩进树。我从Mike Rostock's code 开始并进行了一些修改,以便:1)显示一个右/下箭头,除了叶子; 2)在每一行添加一个复选框; 3) 隐藏根节点。

代码如下,它接受任何数据,我用两个参数调用 drawIntentedTree 函数:根节点和绘制树的 div id。

如图所示,代码中存在一些问题,希望得到帮助: 1. root/start 节点在扩展一个树枝的同时被重绘,导致左右箭头重叠,见 SCL 线。 2. 复选框也出现了类似的问题,它基本上是一个隐藏在白色矩形上的透明 x。我的第一个意图是用笔触颜色填充框,但必须弄清楚每行的 css 颜色是什么,因为它会发生变化。

除了解决这两个问题之外,我还打算在节点之间绘制直线,但原始代码改为绘制卷线,并允许在折叠和展开之间使用 45° 旋转箭头的中间状态(部分折叠),仅显示分支中的复选框。此外,我希望在扩展另一个分支时折叠或部分折叠分支以避免向下滚动。

Mike Bostock 正在使用一种技巧来显示/隐藏树的一部分,他在 _children 中备份子项,然后将子项分配给 null 以隐藏折叠的分支,但重绘总是从根节点开始,我没有设法: 1)避免根节点重绘; 2) 将已存在的左三角形旋转 90 或 90°。

一篇文章中有很多问题,我将不胜感激任何方面的帮助。 jsfiddle link.

d3js 代码:

function drawIndentedTree(root, wherein) {

var width = 300, minHeight = 800;
var barHeight = 20, barWidth = 50;

var margin = {
        top: -10,
        bottom: 10,
        left: 0,
        right: 10
    }

var i = 0, duration = 200;

var tree = d3.layout.tree()
    .nodeSize([0, 20]);

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

var svg = d3.select("#"+wherein).append("svg")
    .attr("width", width + margin.left + margin.right)
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

// set initial coordinates
root.x0 = 0;
root.y0 = 0;

// collapse all nodes recusively, hence initiate the tree
function collapse(d) {
    d.Selected = false;
    if (d.children) {
        d.numOfChildren = d.children.length;
        d._children = d.children;
        d._children.forEach(collapse);
        d.children = null;
    }
    else {
        d.numOfChildren = 0;
    }
}
root.children.forEach(collapse);

update(root);

function update(source) {

    // Compute the flattened node list. TODO use d3.layout.hierarchy.
    var nodes = tree.nodes(root);

    height = Math.max(minHeight, nodes.length * barHeight + margin.top + margin.bottom);

    d3.select("svg").transition()
        .duration(duration)
        .attr("height", height);

    d3.select(self.frameElement).transition()
        .duration(duration)
        .style("height", height + "px");

    // Compute the "layout".
    nodes.forEach(function(n, i) {
          n.x = i * barHeight;
        });

    // Update the nodes…
    var node = svg.selectAll("g.node")
        .data(nodes, function(d) {
              return d.index || (d.index = ++i); });

    var nodeEnter = node.enter().append("g").filter(function(d) { return d.id != root.id })
        .attr("class", "node")
        .style("opacity", 0.001)
        .attr("transform", function(d) {
              return "translate(" + source.y0 + "," + source.x0 + ")";
        });

    // Enter any new nodes at the parent's previous position.
    nodeEnter.append("path").filter(function(d) { return d.numOfChildren > 0 && d.id != root.id })
        .attr("width", 9)
        .attr("height", 9)
        .attr("d", "M -3,-4, L -3,4, L 4,0 Z")
        .attr("class", function(d) { return "node "+d.type; } )
        .attr("transform", function(d) {
              if (d.children) {
                return "translate(-14, 0)rotate(90)";
              }
              else {
                return "translate(-14, 0)rotate(0)";
              }
            })
        .on("click", click);

    // Enter any new nodes at the parent's previous position.
    nodeEnter.append("rect").filter(function(d) { return d.id != root.id })
        .attr("width", 11)
        .attr("height", 11)
        .attr("y", -5)
        .attr("class", function(d) { return "node "+d.type; } );

// check box filled with 'x' or '+'
    nodeEnter.append("text")
        .attr("dy", 4)
        .attr("dx", 2)
        .attr("class", function(d) { return "node "+d.type+" text"; } )
        .text("x");

    nodeEnter.append("rect").filter(function(d) { return d.parent })
        .attr("width", 9)
        .attr("height", 9)
        .attr("x", 1)
        .attr("y", -4)
        .attr("class", "node select")
        .attr("style", function(d) { return "fill: "+boxStyle(d) })
        .on("click", check);

    nodeEnter.append("text")
        .attr("dy", 5)
        .attr("dx", 14)
        .attr("class", function(d) { return "node "+d.type+" text"; } )
        .text(function(d) { return d.Name; });

    // Transition nodes to their new position.
    nodeEnter.transition()
        .duration(duration)
        .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
        .style("opacity", 1);

    node.transition()
        .duration(duration)
        .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
        .style("opacity", 1)
        .select("rect");

    // Transition exiting nodes to the parent's new position.
    node.exit().transition()
        .duration(duration)
        .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
        .style("opacity", 1e-6)
        .remove();

    // Stash the old positions for transition.
    nodes.forEach(function(d) {
          d.x0 = d.x;
          d.y0 = d.y;
      });
}

// Toggle children on click.
function click(d) {
    if (d.children) {
        d3.select(this).attr("translate(-14, 0)rotate(90)");
        d._children = d.children;
        d.children = null;
    } else if (d._children) {
        d.children = d._children;
        d._children = null;
    }
    update(d);
}

// Toggle check box on click.
function check(d) {
    d.Selected = !d.Selected;
    d3.select(this).style("fill", boxStyle(d));
}

function boxStyle(d) {
    return d.Selected ? "transparent" : "white";
}
}

var wherein = "chart";
var root = {
"name": "AUT-1",
"children": [
    {
        "name": "PUB-1","children": [
            {"name": "AUT-11","children": [
                {"name": "AFF-111"},
                {"name": "AFF-112"}
            ]},
            {"name": "AUT-12","children": [
                {"name": "AFF-121"}
            ]},
            {"name": "AUT-13","children": [
                {"name": "AFF-131"},
                {"name": "AFF-132"}
            ]},
            {"name": "AUT-14","children": [
                {"name": "AFF-141"}
            ]}
        ]
    },
    {
        "name": "PUB-2","children": [
            {"name": "AUT-21"},
            {"name": "AUT-22"},
            {"name": "AUT-23"},
            {"name": "AUT-24"},
            {"name": "AUT-25"},
            {"name": "AUT-26"},
            {"name": "AUT-27"},
            {"name": "AUT-28","children":[
                {"name": "AFF-281"},
                {"name": "AFF-282"},
                {"name": "AFF-283"},
                {"name": "AFF-284"},
                {"name": "AFF-285"},
                {"name": "AFF-286"}
            ]}
        ]
    },
    {"name": "PUB-3"},
    {
        "name": "PUB-4","children": [
            {"name": "AUT-41"},
            {"name": "AUT-42"},
            {"name": "AUT-43","children": [
                {"name": "AFF-431"},
                {"name": "AFF-432"},
                {"name": "AFF-433"},
                {"name": "AFF-434","children":[
                    {"name": "ADD-4341"},
                    {"name": "ADD-4342"},
                ]}
            ]},
            {"name": "AUT-44"}
        ]
    }
]
};

CSS:

.node {
font: 12px sans-serif;
fill: #ccebc5;
stroke: #7c9b75;
stroke-width: 1px;
}

.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
cursor: pointer;
}

.node rect {
width: 11px;
height: 11px;
cursor: pointer;
}

.node.select {
width: 9px;
height: 9px;
cursor: pointer;
fill: red;
stroke-width: 0px;
}

.node path {
width: 11px;
height: 11px;
cursor: pointer;
}

.node text Panel {
stroke: #08519c;
stroke-width: 0.5px;
}

.node text Cell {
stroke: #a50f15;
stroke-width: 0.5px;
}

.node.Root {
fill: #f7f7f7;
stroke: #505050;
stroke-width: 1.0px;
}

.node.Root.text {
fill: #505050;
stroke-width: 0px;
font-size: 10px;
font-family: sans-serif;
}

.node.Panel {
fill: #eff3ff;
stroke: #08519c;
stroke-width: 1.0px;
}

.node.Panel.text {
fill: #08519c;
stroke-width: 0px;
font-size: 12px;
font-family: sans-serif;
}

.node.Cell {
fill: #fee5d9;
stroke: #a50f15;
stroke-width: 1.0px;
}

.node.Cell.text {
fill: #a50f15;
stroke-width: 0px;
font-size: 12px;
font-family: sans-serif;
}

【问题讨论】:

  • 您能否用您的styles 和您的数据示例更新您的问题?
  • 我觉得这个说法不太正确“但是重绘总是从根节点开始”。进入节点最初是在 root 的位置开始的,但这仅适用于 root 的子节点。稍后每个节点都在其父位置启动(因为 update(d)click 函数中)。还值得注意的是,更新节点的 html 不会重新生成。因此,他们从他们所在的位置开始,然后转换(翻译)到他们的新位置(参见注释为 // Transition nodes to their new position. 的块)。

标签: d3.js


【解决方案1】:

我会在处理您的问题时更新我的​​答案。

  1. 根/起始节点在扩展树枝时重绘,导致左右箭头重叠,请参见 SCL 线。

这是 d3 进入/更新/退出的经典示例。您有nodeEnter 变量-输入数据时要绘制的内容-这是最初绘制的元素。然后你有 node 变量 - 这是所有已经绘制的东西。当您切换箭头时,您正在对nodeEnter 进行操作,因此您正在重新附加一个新的path,从而导致重叠。相反,只需更新已经存在的 path 并更改转换:

node.select("path").attr("transform", function(d) {
    if (d.children) {
      return "translate(-14, 0) rotate(90)";
    } else {
      return "translate(-14, 0) rotate(0)";
    }
});

例如here

【讨论】:

    【解决方案2】:

    在 Mark 的大力帮助下,问题现已解决,更正的代码如下。我用路径替换了复选框中的 x 文本。

    对我来说,进一步的改进是将箭头旋转与节点运动相结合,然后允许如上所述的部分折叠和自动折叠,并且可能会在节点之间添加直线,这可能很难看。

    function drawIndentedTree(root, wherein) {
    
    var width = 300, minHeight = 800;
    var barHeight = 20, barWidth = 50;
    
    var margin = {
            top: -10,
            bottom: 10,
            left: 0,
            right: 10
        }
    
    var i = 0, duration = 200;
    
    var tree = d3.layout.tree()
        .nodeSize([0, 20]);
    
    var diagonal = d3.svg.diagonal()
        .projection(function(d) { return [d.y, d.x]; });
    
    var svg = d3.select("#"+wherein).append("svg")
        .attr("width", width + margin.left + margin.right)
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    
    // set initial coordinates
    root.x0 = 0;
    root.y0 = 0;
    
    // collapse all nodes recusively, hence initiate the tree
    function collapse(d) {
        d.Selected = false;
        if (d.children) {
            d.numOfChildren = d.children.length;
            d._children = d.children;
            d._children.forEach(collapse);
            d.children = null;
        }
        else {
            d.numOfChildren = 0;
        }
    }
    root.children.forEach(collapse);
    
    update(root);
    
    function update(source) {
    
        // Compute the flattened node list. TODO use d3.layout.hierarchy.
        var nodes = tree.nodes(root);
    
        height = Math.max(minHeight, nodes.length * barHeight + margin.top + margin.bottom);
    
        d3.select("svg").transition()
            .duration(duration)
            .attr("height", height);
    
        // Compute the "layout".
        nodes.forEach(function(n, i) {
              n.x = i * barHeight;
            });
    
        // Update the nodes…
        var node = svg.selectAll("g.node")
            .data(nodes, function(d) {
                  return d.index || (d.index = ++i); });
    
        var nodeEnter = node.enter().append("g")
            .attr("class", "node")
            .style("opacity", 0.001)
            .attr("transform", function(d) {
                  return "translate(" + source.y0 + "," + source.x0 + ")";
            });
    
        // Enter any new nodes at the parent's previous position.
        nodeEnter.append("path").filter(function(d) { return d.numOfChildren > 0 && d.id != root.id })
            .attr("width", 9)
            .attr("height", 9)
            .attr("d", "M -3,-4, L -3,4, L 4,0 Z")
            .attr("class", function(d) { return "node "+d.type; } )
            .attr("transform", "translate(-14, 0)")
            .on("click", click);
    
        node.select("path").attr("transform", function(d) {
                if (d.children) {
                    return "translate(-14, 0)rotate(90)";
                }
                else {
                    return "translate(-14, 0)rotate(0)";
                }
            });
    
        // Enter any new nodes at the parent's previous position.
        nodeEnter.append("rect").filter(function(d) { return d.id != root.id })
            .attr("width", 11)
            .attr("height", 11)
            .attr("y", -5)
            .attr("class", function(d) { return "node "+d.type; } );
    
        nodeEnter.append("path").filter(function(d) { return d.parent })
            .attr("width", 9)
            .attr("height", 9)
            .attr("d", "M -5,-5, L -5,6, L 6,6, L 6,-5 Z M -5,-5, L 6,6, M -5,6 L 6,-5")
            .attr("class", function(d) { return "node "+d.type; } )
            .attr("style", function(d) { return "opacity: "+boxStyle(d) })
            .attr("transform", "translate(5, 0)")
            .on("click", check);
    
        nodeEnter.append("text")
            .attr("dy", 5)
            .attr("dx", 14)
            .attr("class", function(d) { return "node "+d.type+" text"; } )
            .text(function(d) { return d.Name; });
    
        // Transition nodes to their new position.
        nodeEnter.transition()
            .duration(duration)
            .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
            .style("opacity", 1);
    
        node.transition()
            .duration(duration)
            .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
            .style("opacity", 1)
            .select("rect");
    
        // Transition exiting nodes to the parent's new position.
        node.exit().transition()
            .duration(duration)
            .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
            .style("opacity", 1e-6)
            .remove();
    
        // Stash the old positions for transition.
        nodes.forEach(function(d) {
              d.x0 = d.x;
              d.y0 = d.y;
          });
    }
    
    // Toggle children on click.
    function click(d) {
        if (d.children) {
            d3.select(this).attr("translate(-14, 0)rotate(90)");
            d._children = d.children;
            d.children = null;
        } else if (d._children) {
            d.children = d._children;
            d._children = null;
        }
        update(d);
    }
    
    // Toggle check box on click.
    function check(d) {
        d.Selected = !d.Selected;
        d3.select(this).style("opacity", boxStyle(d));
    }
    
    function boxStyle(d) {
        return d.Selected ? 1 : 0;
    }
    }
    

    【讨论】:

      【解决方案3】:

      从上面的代码开始,可以进行以下更改来设计一棵部分可折叠的树。它从一棵折叠的树开始,单击箭头展开一个分支,单击复选框选择项目。再次单击箭头会部分折叠树,选定的项目仍然可见,再次单击将全部折叠。在折叠/展开时保留选择。

      修改代码块:

      // rotate the arrow up, down and third way down on expensing/collapsing
          node.select("path").attr("transform", function(d) {
                  if (d.children) {
                      if (!d._children) {
                          return "translate(-14, 0)rotate(90)";
                      }
                      else {
                          return "translate(-14, 0)rotate(30)";
                      }
                  }
                  else {
                      return "translate(-14, 0)rotate(0)";
                  }
              });
      
      // toggle between the three states
      function click(d) {
          if (d.children) {
              if (!d._children) {
                  // backup children
                  d._children = d.children;
                  // restrick to selected items
                  d.children = marked = d.children.filter(function(d) { return d.Selected });
              }
              else {
                  // partly collapsed -> collapse all
                  d.children = null;
              }
          } else if (d._children) {
              d.children = d._children;
              d._children = null;
          }
          update(d);
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-06-28
        • 2019-03-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多