用d3绘制可折叠缩进树

时间:2014-12-30 15:22:07

标签: d3.js

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

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

enter image description here

正如可能在图片上看到的那样,代码中存在一些问题,我们将不胜感激: 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;
}

3 个答案:

答案 0 :(得分:1)

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

  
      
  1. 在扩展树分支时重绘root / start节点,导致左箭头和下箭头重叠,请参阅SCL行。
  2.   

这是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

答案 1 :(得分:0)

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

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

enter image description here

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

答案 2 :(得分:0)

从上面的代码开始,可以进行以下更改以设计部分可折叠的树。它以折叠树开始,单击箭头消耗分支,单击复选框选择项目。箭头上的新单击会部分折叠树,选定的项目仍然可见,新的单击将全部折叠。折叠/消耗时保留选择。 enter image description here

修改后的代码块:

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