취업/Tree Graph

[php] Tree graph Collapsible Tree with Search

카슈밀 2022. 12. 7. 17:21
반응형

Collapsible Tree라는 그래프 라이브러리 기능 조사.

 

최소 요구 조건

  • 각 노드에서 부가 설명이 표시되어야 함.
  • 현재 위치 표시
  • 검색 기능 필요

그러다보니 조사를 했는데, php의 기능으로 특정라이브러리를 찾았다.

모든 기능이 충족되는 라이브러리가 FamilyTreeJS인데, 이건 유료 라이브러리...

 

이거 적용해보았는데, 트리가 넓어지는 게 100개 이상되면 맛이 가더라.

구현은 되는데, 확장 기능등이 작동되지 않음.

https://balkan.app/FamilyTreeJS

 

Family Tree JavaScript Component | BALKAN FamilyTreeJS

FamilyTree JS is a simple, flexible and highly customizable JavaScript plugin for presenting your family tree in an elegant way.

balkan.app

 

최초로 찾은 라이브러리

https://bl.ocks.org/mbostock/4339083

 

Collapsible Tree

Mike Bostock’s Block 4339083

bl.ocks.org

 

해당 라이브러리에 미니맵 기능이 추가된 것.

https://bl.ocks.org/bwswedberg/464a7dbc471ee2a94dd6278bc7d94710

 

Collapsible Tree with Minimap

Brian Swedberg’s Block 464a7dbc471ee2a94dd6278bc7d94710

bl.ocks.org

 

동일한 라이브러리에 미니맵이 완성된 것.

https://bl.ocks.org/jjzieve/a743242f46321491a950

 

Search Collapsible Tree

Jake Zieve’s Block a743242f46321491a950

bl.ocks.org

 

내가 만든 최종 기본+검색+위치 통합된 코드, 미니맵이 죽음...

그리고 무엇보다 4시간 넘게 고생했는데, 부가설명이 없음...

<!DOCTYPE html>
<meta charset="utf-8">

<style>
.node {
    cursor: pointer;
}

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

.found {
    fill: #ff4136;
    stroke: #ff4136;
}

.node text {
    font: 10px sans-serif;
}

.link {
    fill: none;
    stroke: #ccc;
    stroke-width: 1.5px;
}

/*Just to ensure the select2 box is "glued" to the top*/
.search {
    width: 100%;
}
</style>
<script src="https://code.jquery.com/jquery-3.6.1.js" integrity="sha256-3zlB5s2uwoUzrXK3BT7AX3FyvojsraNFxCc2vC/7pNI="
    crossorigin="anonymous"></script>
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/select2/3.5.0/select2.min.css">
</link>

<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/select2/3.5.0/select2.min.js"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<!-- This will be attached to select2, only static element on the page -->
<div id="search"></div>

<!-- Main -->
<script type="text/javascript">
//basically a way to get the path to an object
function searchTree(obj, search, path) {
    if (obj.name.toString() === search) { //if search is found return, add the object to the path and return it
        path.push(obj);
        return path;
    } else if (obj.children || obj
        ._children) { //if children are collapsed d3 object will have them instantiated as _children
        var children = (obj.children) ? obj.children : obj._children;
        for (var i = 0; i < children.length; i++) {
            path.push(obj); // we assume this path is the right one
            var found = searchTree(children[i], search, path);
            if (found) { // we were right, this should return the bubbled-up path from the first if statement
                return found;
            } else { //we were wrong, remove this parent from the path and continue iterating
                path.pop();
            }
        }
    } else { //not the right object, return false so it will continue to iterate in the loop
        return false;
    }
}

function extract_select2_data(node, leaves, index) {
    if (node.children) {
        for (var i = 0; i < node.children.length; i++) {
            index = extract_select2_data(node.children[i], leaves, index)[0];
        }
    } else {
        leaves.push({
            id: ++index,
            text: node.name.toString()
        });
    }
    return [index, leaves];
}

var div = d3.select("body")
    .append("div") // declare the tooltip div
    .attr("class", "tooltip")
    .style("opacity", 0);

var margin = {
        top: 20,
        right: 120,
        bottom: 20,
        left: 120
    },
    width = 960 - margin.right - margin.left,
    height = window.innerHeight - margin.top - margin.bottom;

var i = 0,
    duration = 750,
    root,
    select2_data;

var diameter = 960;

var tree = d3.layout.tree()
    .size([height, width]);

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

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

//recursively collapse children
function collapse(d) {
    if (d.children) {
        d._children = d.children;
        d._children.forEach(collapse);
        d.children = null;
    }
}

// Toggle children on click.
function click(d) {
    if (d.children) {
        d._children = d.children;
        d.children = null;
    } else {
        d.children = d._children;
        d._children = null;
    }
    update(d);
}

function openPaths(paths) {
    for (var i = 0; i < paths.length; i++) {
        if (paths[i].id !== "1") { //i.e. not root
            paths[i].class = 'found';
            if (paths[i]._children) { //if children are hidden: open them, otherwise: don't do anything
                paths[i].children = paths[i]._children;
                paths[i]._children = null;
            }
            update(paths[i]);
        }
    }
}

d3.json("mockup/re.json", function(error, values) {
    root = values;
    select2_data = extract_select2_data(values, [], 0)[1]; //I know, not the prettiest...
    root.x0 = height / 2;
    root.y0 = 0;
    root.children.forEach(collapse);
    update(root);
    //init search box
    $("#search").select2({
        data: select2_data,
        containerCssClass: "search"
    });
});
//attach search box listener
$("#search").on("select2-selecting", function(e) {
    var paths = searchTree(root, e.object.text, []);
    if (typeof(paths) !== "undefined") {
        openPaths(paths);
    } else {
        alert(e.object.text + " not found!");
    }
})

d3.select(self.frameElement).style("height", "800px");

function update(source) {
    // Compute the new tree layout.
    var nodes = tree.nodes(root).reverse(),
        links = tree.links(nodes);

    // Normalize for fixed-depth.
    nodes.forEach(function(d) {
        d.y = d.depth * 180;
    });

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

    // Enter any new nodes at the parent's previous position.
    var nodeEnter = node.enter()
        .append("g")
        .attr("class", "node")
        .attr("transform", function(d) {
            return "translate(" + source.y0 + "," + source.x0 + ")";
        })
        .on("click", click);

    nodeEnter.append("circle")
        .attr("r", 1e-6)
        .style("fill", function(d) {
            return d._children ? "lightsteelblue" : "#fff";
        });

    nodeEnter.append("text")
        .attr("x", function(d) {
            return d.children || d._children ? 10 : 10;
        })
        .attr("dy", ".35em")
        .attr("style", "display:block; white-space:nowrap;")
        .attr("text-anchor", function(d) {
            return d.children || d._children ? "end" : "start";
        })
        .text(function(d) {
            return d.name
        })
        .style("fill-opacity", 1e-6)



    // Transition nodes to their new position.
    var nodeUpdate = node.transition()
        .duration(duration)
        .attr("transform", function(d) {
            return "translate(" + d.y + "," + d.x + ")";
        });

    nodeUpdate.select("circle")
        .attr("r", 4.5)
        .style("fill", function(d) {
            if (d.class === "found") {
                return "#ff4136"; //red
            } else if (d._children) {
                return "lightsteelblue";
            } else {
                return "#fff";
            }
        })
        .style("stroke", function(d) {
            if (d.class === "found") {
                return "#ff4136"; //red
            }
        });

    nodeUpdate.select("text")
        .style("fill-opacity", 1);

    // Transition exiting nodes to the parent's new position.
    var nodeExit = node.exit().transition()
        .duration(duration)
        .attr("transform", function(d) {
            return "translate(" + source.y + "," + source.x + ")";
        })
        .remove();

    nodeExit.select("circle")
        .attr("r", 1e-6);

    nodeExit.select("text")
        .style("fill-opacity", 1e-6);

    // Update the links…
    var link = svg.selectAll("path.link")
        .data(links, function(d) {
            return d.target.id;
        });

    // Enter any new links at the parent's previous position.
    link.enter().insert("path", "g")
        .attr("class", "link")
        .attr("d", function(d) {
            var o = {
                x: source.x0,
                y: source.y0
            };
            return diagonal({
                source: o,
                target: o
            });
        });

    // Transition links to their new position.
    link.transition()
        .duration(duration)
        .attr("d", diagonal)
        .style("stroke", function(d) {
            if (d.target.class === "found") {
                return "#ff4136";
            }
        });

    // Transition exiting nodes to the parent's new position.
    link.exit().transition()
        .duration(duration)
        .attr("d", function(d) {
            var o = {
                x: source.x,
                y: source.y
            };
            return diagonal({
                source: o,
                target: o
            });
        })
        .remove();

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

</html>
728x90

'취업 > Tree Graph' 카테고리의 다른 글

[Tree] react-d3-tree 미니맵 만들기.  (0) 2022.12.13
[React.JS] react-d3-tree 그래프 기술 검토  (0) 2022.12.11