zoukankan      html  css  js  c++  java
  • d3-tree 双向树

    <!DOCTYPE html>
    <meta charset="utf-8">
    <style>
    body {
    text-align: center;
    }

    svg {
    margin-top: 32px;
    border: 1px solid #aaa;
    }

    .person rect {
    fill: #fff;
    stroke: rgba(0, 0, 0, 0.15);
    stroke- 1px;
    }

    .person {
    font: 14px sans-serif;
    }

    .btn {
    cursor: pointer;
    }

    .link {
    fill: none;
    stroke: rgba(0, 0, 0, 0.15);
    stroke- 1.5px;
    }

    .btn circle {
    stroke: rgba(0, 0, 0, 0.15);
    fill: #fff;
    }
    </style>

    <body>
    <script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
    <script>
    var json = {
    "name": "Isaac Warren",
    "id": "acd21329-2b99-5bdb-a080-ff15ad2fcaa6",
    "_parents": [{
    "name": "父Virgie Hampton",
    "id": "56d25f0e-7c01-50f9-8dfe-9ed953ea5b33",
    "_parents": [{
    "name": "父Jared Evans",
    "id": "efa251bb-13f7-583d-b5ec-c16f8fcd109f",
    },
    {
    "name": "父Mina Taylor",
    "id": "6a2c6fee-7e8c-5d14-97a0-2fb79c20c89f",
    }
    ]
    },
    {
    "name": "父Matthew Haynes",
    "id": "051d725d-cfe4-5694-a330-28b3f3e8b9b1",
    "_parents": [{
    "name": "父Mayme Moss",
    "id": "5b79bad1-aa95-5d49-9c4b-ccd681efdeed",
    },
    {
    "name": "父Barry Perry",
    "id": "214f52e2-51d5-51f8-9b3a-93b41b49ec58",
    }
    ]
    }
    ],
    "_children": [{
    "name": "子Celia Frazier",
    "id": "60d7d16b-2ec3-51ac-b016-a1b16cf56d39",
    "_children": [{
    "name": "子Bill Greene",
    "id": "bda34c70-061b-5678-af24-f9648ecfb985",
    },
    {
    "name": "子Travis Day",
    "id": "dc3a79aa-4a4e-52b0-8e10-a88f00d77b93",
    },
    ]
    },
    {
    "name": "子Jeremiah Webb",
    "id": "f774bf05-430d-5cfe-9181-3a732233f0d3",
    "_children": [{
    "name": "子Bill Greene",
    "id": "bda34c70-061b-5678-af24",
    "_children": [{
    "name": "子Bill Greene",
    "id": "bda34c70-061b-af24",
    },
    {
    "name": "子Travis Day",
    "id": "dc3a79aa-4a4e-8e10",
    },
    ]
    },
    {
    "name": "子Travis Day",
    "id": "dc3a79aa-4a4e-52b0-8e10",
    },
    ]
    },
    {
    "name": "子Amelia Curtis",
    "id": "a66042ef-d968-50a4-87e0-bea4e6793825",
    "_children": [{
    "name": "子Bill Greene",
    "id": "bda34c70-061b-5678-af24-",
    },
    {
    "name": "子Travis Day",
    "id": "dc3a79aa-4a4e-52b0-8e10-",
    },
    ]
    }
    ]
    }
    </script>
    <script>
    var boxWidth = 150,
    boxHeight = 40,
    nodeWidth = 150,
    nodeHeight = 200,

    // duration of transitions in ms
    duration = 750,

    // d3 multiplies the node size by this value
    // to calculate the distance between nodes
    separation = .5;

    /**
    * For the sake of the examples, I want the setup code to be at the top.
    * However, since it uses a class (Tree) which is defined later, I wrap
    * the setup code in a function at call it at the end of the example.
    * Normally you would extract the entire Tree class defintion into a
    * separate file and include it before this script tag.
    */
    function setup() {

    var zoom = d3.behavior.zoom()
    .scaleExtent([.1, 1]) //用于设置最小和最大的缩放比例
    .on('zoom', function () {
    svg.attr("transform", "translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")");
    })
    // 当 zoom 事件发生时,调用 zoomed 函数
    // :zoomed 函数,用于更改需要缩放的元素的属性,d3.event.translate 是平移的坐标值,d3.event.scale 是缩放的值。
    .translate([400, 200]);

    var svg = d3.select("body").append("svg")
    .attr('width', 1000)
    .attr('height', 500)
    .call(zoom)
    .append('g')

    // Left padding of tree so that the whole root node is on the screen.
    // TODO: find a better way
    .attr("transform", "translate(400,200)");


    //父节点
    var ancestorTree = new Tree(svg, 'ancestor', -1);
    ancestorTree.children(function (person) {
    if (person.collapsed) {
    return;
    } else {
    return person._parents;
    }
    });
    //子节点
    var descendantsTree = new Tree(svg, 'descendant', 1);
    descendantsTree.children(function (person) {
    if (person.collapsed) {
    return;
    } else {
    return person._children;
    }
    });

    // d3.json('data/8gens.json', function(error, json){

    // if(error) {
    // return console.error(error);
    // }

    // D3 modifies the objects by setting properties such as
    // coordinates, parent, and children. Thus the same node
    // node can't exist in two trees. But we need the root to
    // be in both so we create proxy nodes for the root only.
    var ancestorRoot = rootProxy(json);
    var descendantRoot = rootProxy(json);

    // Start with only the first few generations of ancestors showing
    // ancestorRoot._parents.forEach(function (parents) {
    // parents._parents.forEach();
    // });
    ancestorRoot._parents.forEach(collapse);
    // Start with only one generation of descendants showing
    descendantRoot._children.forEach(collapse);

    // Set the root nodes
    ancestorTree.data(ancestorRoot);
    descendantsTree.data(descendantRoot);

    // Draw the tree
    ancestorTree.draw(ancestorRoot);
    descendantsTree.draw(descendantRoot);

    // });

    }

    function rootProxy(root) {
    return {
    name: root.name,
    id: root.id,
    x0: 0,
    y0: 0,
    _children: root._children,
    _parents: root._parents,
    collapsed: false,
    root_parents: false,
    root_children: false
    };
    }

    /**
    * Shared code for drawing ancestors or descendants.
    * `selector` is a class that will be applied to links
    * and nodes so that they can be queried later when
    * the tree is redrawn.
    * `direction` is either 1 (forward) or -1 (backward).
    */
    var Tree = function (svg, selector, direction) {
    this.svg = svg;
    this.selector = selector;
    this.direction = direction;

    this.tree = d3.layout.tree()

    // Using nodeSize we are able to control
    // the separation between nodes. If we used
    // the size parameter instead then d3 would
    // calculate the separation dynamically to fill
    // the available space.
    // .nodeSize([nodeWidth, nodeHeight])
    .nodeSize([nodeWidth, nodeHeight])

    // By default, cousins are drawn further apart than siblings.
    // By returning the same value in all cases, we draw cousins
    // the same distance apart as siblings.
    .separation(function () {
    return 1.5;
    });
    };

    /**
    * Set the `children` function for the tree
    */
    Tree.prototype.children = function (fn) {
    this.tree.children(fn);
    return this;
    };

    /**
    * Set the root of the tree
    */
    Tree.prototype.data = function (data) {
    this.root = data;
    return this;
    };

    /**
    * Draw/redraw the tree
    */
    Tree.prototype.draw = function (source) {
    if (this.root) {
    var nodes = this.tree.nodes(this.root),
    links = this.tree.links(nodes);
    this.drawLinks(links, source);
    this.drawNodes(nodes, source);
    this.drawBtn(nodes, source);
    } else {
    throw new Error('Missing root');
    }
    return this;
    };

    /**
    * Draw/redraw the connecting lines
    */
    Tree.prototype.drawLinks = function (links, source) {

    var self = this;

    // Update links
    var link = self.svg.selectAll("path.link." + self.selector)

    // The function we are passing provides d3 with an id
    // so that it can track when data is being added and removed.
    // This is not necessary if the tree will only be drawn once
    // as in the basic example.
    .data(links, function (d) {
    return d.target.id;
    });

    var diagonal = d3.svg.diagonal()
    .projection(function (d) {
    return [d.x, (d.y + 20) * self.direction];
    });
    // Add new links
    // Transition new links from the source's
    // old position to the links final position
    // var diagonal = d3.linkHorizontal().x(d => d.x).y(d => d.y)

    link.enter().append("path")
    .attr("class", "link " + self.selector)
    .attr("d", d => {
    const o = {
    x: source.x0,
    y: source.y0
    };
    return diagonal({
    source: o,
    target: o
    });
    }).transition()
    .duration(duration)
    // Update the old links positions
    // link.transition()
    // .duration(duration)
    // .attr("d", function (d) {
    // return elbow(d, self.direction);
    // });
    link.transition()
    .duration(duration)
    .attr("d", function (d) {
    return diagonal(d);
    });

    // Remove any links we don't need anymore
    // if part of the tree was collapsed
    // Transition exit links from their current position
    // to the source's new position
    link.exit()
    .transition()
    .duration(duration)
    .attr("d", d => {
    const o = {
    x: source.x,
    y: source.y
    };
    return diagonal({
    source: o,
    target: o
    });
    })
    .remove();

    };


    /**
    * Draw/redraw the person boxes.
    */
    Tree.prototype.drawNodes = function (nodes, source) {
    var self = this;

    // Update nodes
    var node = self.svg.selectAll("g.person." + self.selector)

    // The function we are passing provides d3 with an id
    // so that it can track when data is being added and removed.
    // This is not necessary if the tree will only be drawn once
    // as in the basic example.
    .data(nodes, function (person) {
    return person.id;
    });

    // Add any new nodes
    var nodeEnter = node.enter().append("g")
    .attr("class", "person " + self.selector)
    // Add new nodes at the right side of their child's box.
    // They will be transitioned into their proper position.
    .attr('transform', function (person) {
    // 节点从哪里出来的位移
    return 'translate(' + source.x0 + ',' + (self.direction * (source.y0 + boxHeight / 2)) + ')';
    })

    // Draw the rectangle person boxes.
    // Start new boxes with 0 size so that
    // we can transition them to their proper size.
    nodeEnter.append("rect")
    .attr({
    x: 0,
    y: 0,
    0,
    height: 0
    });

    // Draw the person's name and position it inside the box
    nodeEnter.append("text")
    .attr("dx", 0)
    .attr("dy", 5)
    .attr("text-anchor", "start")
    .attr('class', 'name')
    .text(function (d) {
    return d.name;
    })
    .style('fill-opacity', 0);


    // Update the position of both old and new nodes
    var nodeUpdate = node.transition()
    .duration(duration)
    .attr("transform", function (d) {
    return "translate(" + d.x + "," + (self.direction * d.y) + ")";
    });



    // 盒子边框
    nodeUpdate.select('rect')
    .attr({
    x: -(boxWidth / 2),
    y: -(boxHeight / 2),
    boxWidth,
    height: boxHeight,
    rx: "2",
    })
    // Move text to it's proper position
    // 盒子内部文本
    nodeUpdate.select('text')
    .attr("dx", -(boxWidth / 2) + 10)
    .style('fill-opacity', 1)
    .style('vertical-align', 'middle');

    // Remove nodes we aren't showing anymore
    var nodeExit = node.exit()
    .transition()
    .duration(duration)
    .attr("transform", function (d) {
    return 'translate(' + source.x + ',' + (self.direction * (source.y + boxHeight / 2)) + ')';
    })
    .remove();

    // Shrink boxes as we remove them
    nodeExit.select('rect')
    .attr({
    x: 0,
    y: 0,
    0,
    height: 0
    });

    // Fade out the text as we remove it
    nodeExit.select('text')
    .style('fill-opacity', 0)
    .attr('dx', 0);

    // a(nodeEnter, self, source)
    // Stash the old positions for transition.
    nodes.forEach(function (person) {
    person.x0 = person.x;
    person.y0 = person.y;
    });

    };
    Tree.prototype.drawBtn = function (nodes, source) {
    var self = this;

    // Update nodes
    var node = self.svg.selectAll("g.btn." + self.selector)

    // The function we are passing provides d3 with an id
    // so that it can track when data is being added and removed.
    // This is not necessary if the tree will only be drawn once
    // as in the basic example.
    .data(nodes, function (person) {
    return person.id;
    });
    // // Add any new nodes
    var nodeEnter = node.enter().append("g")
    .attr("class", "btn " + self.selector)
    // Add new nodes at the right side of their child's box.
    // They will be transitioned into their proper position.
    .attr('transform', function (person) {
    // 节点从哪里出来的位移
    return 'translate(' + source.x0 + ',' + (self.direction * (source.y0 + boxHeight / 2)) + ')';
    })
    .on('click', function (person, ...event) {
    if (this.childNodes[1].innerHTML === '+') {
    this.childNodes[1].innerHTML = '-'
    } else {
    this.childNodes[1].innerHTML = '+'
    }
    self.togglePerson(person);
    });

    // // Draw the rectangle person boxes.
    // // Start new boxes with 0 size so that
    // // we can transition them to their proper size.
    nodeEnter.append("circle")
    .attr({
    x: 0,
    y: 0,
    r: 0,
    })

    nodeEnter.append("text")
    .attr("x", "0")
    .attr("dy", function (d) {
    if (self.direction === -1) {
    return -12
    } else {
    return 26
    }
    })
    .attr('r', 80)
    .attr("text-anchor", "middle")
    .style("fill", "rgba(0, 0, 0, 0.15)")
    .style('font-size', '22px')
    .text(function (d) {
    if (!d.depth) {
    return ''
    }
    if (self.direction === -1) {
    if (d._parents && d._parents.length) {
    return d.collapsed ? '+' : '-'
    } else {
    return "";
    }
    } else {
    if (d._children && d._children.length) {
    return d.collapsed ? '+' : '-'
    } else {
    return "";
    }
    }
    })
    .style('fill-opacity', 0);



    // // Update the position of both old and new nodes
    var nodeUpdate = node.transition()
    .duration(duration)
    .attr("transform", function (d) {
    return "translate(" + d.x + "," + (self.direction * d.y) + ")";
    });


    nodeUpdate.select('circle')
    .attr("cx", 0)
    .attr("cy", function (d) {
    if (self.direction === -1) {
    return -20
    } else {
    return 20
    }
    })
    .attr("r", function (d) {
    if (!d.depth) {
    return 0
    }
    if (self.direction === -1) {
    return d._parents && d._parents.length ? 10 : 0;
    } else {
    return d._children && d._children.length ? 10 : 0;
    }
    })
    nodeUpdate.select('text')
    .style('fill-opacity', 1)


    // Remove nodes we aren't showing anymore

    var nodeExit = node.exit()
    .transition()
    .duration(duration)
    .attr("transform", function (d) {
    return 'translate(' + source.x + ',' + (self.direction * (source.y + boxHeight / 2)) + ')';
    })
    .remove();
    // // Shrink boxes as we remove them
    nodeExit.select('circle')
    .attr({
    x: 0,
    y: 0,
    r: 0,
    });

    // Fade out the text as we remove it
    nodeExit.select('text')
    .style('fill-opacity', 0)


    nodes.forEach(function (person) {
    person.x0 = person.x;
    person.y0 = person.y;
    });

    };
    /**
    * Update a person's state when they are clicked.
    */

    Tree.prototype.togglePerson = function (person, direction) {

    // Don't allow the root to be collapsed because that's
    // silly (it also makes our life easier)
    if (person === this.root) {
    return '';
    } else {
    if (person.collapsed) {
    person.collapsed = false;
    } else {
    collapse(person);
    }
    this.draw(person);
    }
    };

    /**
    * Collapse person (hide their ancestors). We recursively
    * collapse the ancestors so that when the person is
    * expanded it will only reveal one generation. If we don't
    * recursively collapse the ancestors then when
    * the person is clicked on again to expand, all ancestors
    * that were previously showing will be shown again.
    * If you want that behavior then just remove the recursion
    * by removing the if block.
    */
    function collapse(person) {
    person.collapsed = true;
    if (person._parents) {
    person._parents.forEach(collapse);
    }
    if (person._children) {
    person._children.forEach(collapse);
    }
    }

    // function rootCollapse(person) {
    // person.collapsed = true;
    // if (person._parents) {
    // person._parents.forEach(collapse);
    // }
    // if (person._children) {
    // person._children.forEach(collapse);
    // }
    // }

    /**
    * Custom path function that creates straight connecting
    * lines. Calculate start and end position of links.
    * Instead of drawing to the center of the node,
    * draw to the border of the person profile box.
    * That way drawing order doesn't matter. In other
    * words, if we draw to the center of the node
    * then we have to draw the links first and the
    * draw the boxes on top of them.
    */

    function elbow(d, direction) {
    let sourceX = d.source.x,
    sourceY = d.source.y + (boxHeight / 2),
    targetX = d.target.x,
    targetY = d.target.y - (boxHeight / 2);
    return "M" + sourceX + "," + (direction * sourceY) +
    "V" + (direction * ((targetY - sourceY) / 2 + sourceY)) +
    "H" + targetX +
    "V" + (direction * targetY);

    }

    setup();
    </script>
  • 相关阅读:
    2019/9/8
    实现简单的网页登录注册功能 (使用html和css以及javascript技术) 没有美化的日后补全
    测试一些以前的代码
    使用三层开发遵循的原则
    超市管理
    热身训练
    考试第三题
    考试第七题
    考试第10题
    考试第8题
  • 原文地址:https://www.cnblogs.com/gudun/p/10737427.html
Copyright © 2011-2022 走看看