Thursday, 20 December 2018

New layout after opening a group not base on the last layout with cola.js

I recently learned about a excellent JS library cola.js . It can do a force layout and support groups. Learn more here : Cola.js

I create a simple demo to show a force layout with opening group feature. But I was confused the openning behavior.

I think when open a group, the new layout should be a minor adjustments base on the last layout. But now it relayout all nodes. Why ?

Here is my demo link : Cola open group demo

Is it possible that the relayout base on the last opening state ? Not a newly full relayout?

    var w = 480, h = 420, cola;
var data = {
    "nodes": [
        {"name": "Top","width": 60,"height": 60},
        {"name": "A","width": 60,"height": 60},
        {"name": "B","width": 60,"height": 60},
        {"name": "C","width": 60,"height": 60},
        {"name": "D","width": 60,"height": 60},
        {"name": "E","width": 60,"height": 60},
        {"name": "F","width": 60,"height": 60},
        {"name": "G","width": 60,"height": 60},
        {"name": "H","width": 60,"height": 60},
        {"name": "I","width": 60,"height": 60}
    ],
    "links": [
        {"source": 0,"target": 6},
        {"source": 0,"target": 4},
        {"source": 0,"target": 3},
        {"source": 0,"target": 7},
        {"source": 0,"target": 8},
        {"source": 6,"target": 0},
        {"source": 6,"target": 7},
        {"source": 4,"target": 0},
        {"source": 4,"target": 3},
        {"source": 3,"target": 0},
        {"source": 3,"target": 4},
        {"source": 7,"target": 0},
        {"source": 7,"target": 6},
        {"source": 7,"target": 4},
        {"source": 8,"target": 0},
        {"source": 8,"target": 7}
    ],
    "groups": [
        {"leaves": [0,1,2],"groups": [1],"name": "Product"},
        {"leaves": [7],"name": "Businness"},
        {"leaves": [3,4,6,8,9],"name": "Tech"}
    ]
};

cola = cola.d3adaptor()
        .linkDistance(150)
        .avoidOverlaps(true)
        .handleDisconnected(true)
        .size([w, h]);

    svg = d3.select("body").append("svg")
        .attr("width", w)
        .attr("height", h)
        .on("dblclick.zoom", null);

    svg.append('rect')
        .attr("width", w)
        .attr("height", h)
        .style("fill", "none")
        .style("pointer-events", "all");

    svg = svg.append('g');

    update(data);

    cola.on("tick", function () {
        svg.selectAll(".link")
            .attr("x1", function (d) { return d.source.x; })
            .attr("y1", function (d) { return d.source.y; })
            .attr("x2", function (d) { return d.target.x; })
            .attr("y2", function (d) { return d.target.y; });

        svg.selectAll(".nodeimage").attr("x", function(d){ return d.x - 25 / 2 }).attr("y", function(d){ return d.y - 25 / 2 });

        svg.selectAll(".group")
            .attr("x", function (d) { return d.bounds.x; })
            .attr("y", function (d) { return d.bounds.y; })
            .attr("width", function (d) { return d.bounds.width(); })
            .attr("height", function (d) { return d.bounds.height(); });

        svg.selectAll(".label").attr("x", function (d) {
                var w = this.getBBox().width;
                return d.x - w/2;
            })
            .attr("y", function (d) {
                var h = this.getBBox().height;
                return d.y + 25;
            });

        svg.selectAll('.groupclosebutton')
            .attr("x", function (d) { return d.bounds.x + d.bounds.width() - 20; })
            .attr("y", function (d) { return d.bounds.y + 2; });


        svg.selectAll('.groupname')
            .attr("x", function (d) { return d.bounds.x + 5; })
            .attr("y", function (d) { return d.bounds.y + 15; });
    });

function update(data){
    //data.groups.forEach(function (g) { g.padding = 15; });
    cola.nodes(data.nodes).links(data.links).groups(data.groups).start();

    var color = ['#d3d4e5', '#f7e0c8', '#dee8f2', '#cbe5c4', '#ededeb'];
    var group = svg.selectAll(".group").data(data.groups, function(d) { return d.name;});
    group.enter().append("rect")//, ":last-child"
        .attr("rx", 8).attr("ry", 8)
        .attr("class", "group")
        .style("fill", function (d, i) { return color[i%5]; })
        .call(cola.drag);
    group.exit().remove();

    var groupCloseBtns = svg.selectAll('.groupclosebutton').data(data.groups, function(d) { return d.name;});
    groupCloseBtns.enter().append('svg:image')
        .attr("class", "groupclosebutton")
        .attr("width", 14)
        .attr("height", 14)
        .attr("xlink:href", function(d){
            var img = "http://icons.iconarchive.com/icons/custom-icon-design/flatastic-1/24/delete-icon.png"; 
            return img;
        });
    groupCloseBtns.exit().remove(); 

    var groupName = svg.selectAll(".groupname").data(data.groups, function(d) { return d.name;});
    groupName.enter().append("text")
        .attr("class", "groupname")
        .attr("width", "40px")
        .attr("height", "13px")
        .text(function (d) { return d.name; });
    groupName.exit().remove();

    var link = svg.selectAll(".link").data(data.links, function(d) { return d.source.name+'-'+d.target.name;});
    link.enter().append("line").attr("class", "link").style("stroke", "rgb(168, 168, 168)");
    link.exit().remove();

    var nodes = svg.selectAll('.nodeimage').data(data.nodes, function(d) { return d.name;});
    nodes.enter().append('svg:image')
        .attr("class", "nodeimage")
        .call(cola.drag)
        .attr("xlink:href", function(d){
            var img = "http://icons.iconarchive.com/icons/hopstarter/sleek-xp-basic/24/Folder-icon.png";
            return img;
        })
        .attr('temp', function(d){
            var self = d3.select(this);
            self.attr("width", 25);
            self.attr("height", 25);
        })
        .on("dblclick", function(node, index, selection){
            d3.event.preventDefault();
            openGroup(node);
        });
    nodes.exit().transition().attr("width", 0).attr("width", 0).remove();

    var label = svg.selectAll(".label").data(data.nodes, function(d) { return d.name;});
    label.enter().append("text")
        .attr("class", "label")
        .attr("width", "40")
        .attr("height", 15)
        .text(function (d) { return d.name; })
        .call(cola.drag);
    label.exit().remove();
}

function openGroup(node){
    var i,j, flag,maxnodes = 3, groupDeletedIndex = -1;

    // Delete the node
    for(i = 0; i < this.data.nodes.length; i++){
        if(this.data.nodes[i].name == node.name){
            this.data.nodes.splice(i, 1);
            break;
        }
    }
    // Delete old links linked to the node
    for(i = this.data.links.length - 1; i >= 0; i--){
        if(this.data.links[i].source.name == node.name
                || this.data.links[i].target.name == node.name){
            this.data.links.splice(i, 1);
        }
    }
    // Delete the relationship of the node
    flag = false;
    for(i = 0; i < this.data.groups.length; i++){
        for(j = 0; j < this.data.groups[i].leaves.length; j++){
            if(this.data.groups[i].leaves[j].name == node.name){
                this.data.groups[i].leaves.splice(j, 1);
                flag = true;
                groupDeletedIndex = i;
                break;
            }
        }
        if(flag)break;
    }

    // Create new nodes belong to openning group
    for(var i = 0; i < maxnodes; i++){
        var obj = { 
            name : node.name+'child'+i , 
            width : 100, 
            height : 100, 
            x:node.x, 
            y:node.y,
            px:node.px,
            py:node.py
        };
        this.data.nodes.push(obj);
        if(i%3!=0){
            this.data.links.push({// Create demo links
                source : this.data.nodes.length-1,
                target : Math.floor(Math.random(this.data.nodes.length-1)) 
            });
        }
    }
    // Create a group to contain the new nodes and push to groups
    this.data.groups.push({
        leaves : [], 
        name : node.name, 
        bounds : {x:node.x, y:node.y, X:node.x+100, Y:node.y+100},
        padding : 15
    });
    var begin = this.data.nodes.length - maxnodes;
    for(var i = 0; i < maxnodes; i++){
        this.data.groups[this.data.groups.length-1].leaves.push(begin+i);
    }

    if(groupDeletedIndex > -1){
        if(!this.data.groups[groupDeletedIndex].groups){
            this.data.groups[groupDeletedIndex].groups = [];
        }
        this.data.groups[groupDeletedIndex].groups.push(this.data.groups.length-1);
    }
    update(this.data);
}



from New layout after opening a group not base on the last layout with cola.js

No comments:

Post a Comment