d3 force layout with node groups

2019-06-03 17:03发布

I want to achieve something like this: enter image description here

With the following attributes:

  • groups are rounded rects, which never overlap
  • every node group has a horizontal centered title at the top, at least one node, and at least one link
  • every node has a label a type: source or target and a group
  • every node can have multiple links
  • the source of a link is always a source node, and the target is always a target node

I have hard time to figure out how to implement this, since I started with d3.js yesterday...

Currently I have something like this:

js:

var GraphView = Class.extend({
    init: function (data) {
        this.data = data;
    },
    render: function () {
        var width = 960;
        var height = 500;
        var svg = d3.select("body").append("svg")
                .attr("width", width)
                .attr("height", height);

        var force = d3.layout.force()
                .gravity(.05)
                .distance(100)
                .charge(-500)
                .size([width, height])
                .nodes(this.data.nodes)
                .links(this.data.links)
                .start();

        var link = svg.selectAll(".link")
                .data(this.data.links)
                .enter().append("line")
                .attr("class", function (d) {
                    return d.group.join(" ");
                });

        var node = svg.selectAll(".node")
                .data(this.data.nodes)
                .enter().append("g")
                .attr("class", function (d) {
                    return d.group.join(" ");
                })
                .call(force.drag);

        var component = node.filter(function (d) {
            return d.group[1] == "component";
        });

        var port = node.filter(function (d) {
            return d.group[1] == "port";
        });

        var input = port.filter(function (d) {
            return d.group[2] == "input";
        });

        var output = port.filter(function (d) {
            return d.group[2] == "output";
        });

        component.append("rect")
                .attr("x", -8)
                .attr("y", -8)
                .attr("width", 103)
                .attr("height", 64)
                .attr("rx", 15)
                .attr("ry", 15);

        port.append("circle")
                .attr("r", 6);

        component.append("text")
                .attr("dx", 24)
                .attr("dy", "1em")
                .text(function (d) {
                    return d.label
                });
        port.append("text")
                .attr("dx", 12)
                .attr("dy", ".35em")
                .text(function (d) {
                    return d.label
                });

        force.on("tick", function () {
            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;
                    });

            node.attr("transform", function (d) {
                return "translate(" + d.x + "," + d.y + ")";
            });
        });
    }
});

css:

.link.internal {
    stroke: #ccc;
    stroke-width: 1px;
}

.link.external {
    stroke: #000;
    stroke-width: 2px;
}

.link.external.error {
    stroke: #f00;
}

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

.node.component rect {
    fill: #ff0;
    stroke: #000;
    stroke-width: 2px;
}

.node.component text {
    font-weight: bold;
}

.node.port circle {
    stroke: #ccc;
    stroke-width: 2px;
}

.node.port.input circle {
    fill: #000;
}

.node.port.output circle {
    fill: #fff;
}

json:

{
    "nodes": [
        {"label": "Traverser", "group": ["node", "component"]},
        {"label": "Standard Output", "group": ["node", "port", "output"]},
        {"label": "Subscriber", "group": ["node", "component"]},
        {"label": "Standard Input", "group": ["node", "port", "input"]}
    ],
    "links": [
        {"source": 0, "target": 1, "group": ["link", "internal"]},
        {"source": 3, "target": 2, "group": ["link", "internal"]},
        {"source": 1, "target": 3, "group": ["link", "external"]}
    ]
}

results:

enter image description here

sadly not close enough :S

Not a clue how to put the nodes into the rectangles, and how to add force layout to a rounded rect, which size depends on the node count and which does not have equal width and height, so I cannot use simply a center point to count the forces... :S Any ideas?

1条回答
孤傲高冷的网名
2楼-- · 2019-06-03 17:25

I think I have a solution:

enter image description here

This is not perfect, I have to drag-drop nodes for a short while, but it is just the minimum effort solution. I call the node groups by the name "components", and the nodes by the name "ports". I only used force layout nodes by the components and moved the links to the position of the ports. That's all. It is not perfect, but much better than meditate for days on how to solve this with d3.

Each component has an SVG something like this:

    <g class="component worker" transform="translate(0,0)">
        <use class="container" xlink:href="#container"/>
        <use class="icon" xlink:href="#worker"/>
        <text class="label" text-anchor="middle" alignment-baseline="middle" dy="40">Worker</text>
        <g class="input" transform="translate(-31,-16)">
            <use class="port" xlink:href="#port">
                <title>stdin</title>
            </use>
            <use y="8" class="port" xlink:href="#port"/>
            <use y="16" class="port" xlink:href="#port"/>
            <use y="24" class="port" xlink:href="#port"/>
            <use y="32" class="port" xlink:href="#port"/>
        </g>
        <g class="output" transform="translate(31,16)">
            <use class="port" xlink:href="#port">
                <title>stdout</title>
            </use>
            <use y="-8" class="port" xlink:href="#port"/>
            <use y="-16" class="port" xlink:href="#port"/>
            <use y="-24" class="port" xlink:href="#port"/>
            <use y="-32" class="port" xlink:href="#port"/>
        </g>
    </g>

Ofc. there are no nodes with 5 inputs and 5 outputs, but that's just a template... So I got the port name by reading the tooltip after mouseover. It would be unnecessary noise to display every port name at once...

The JSON changed as well:

{
    "nodes": [
        {"label": "Traverser", "groups": ["node", "component", "publisher", "traverser"], "inputs": [], "outputs": ["stdout"]},
        {"label": "Subscriber", "groups": ["node", "component", "subscriber"], "inputs": ["stdin"], "outputs": []}
    ],
    "links": [
        {"source": 0, "sourceIndex": 0, "target": 1, "targetIndex": 0, "groups": ["link"]}
    ]
}

and the js is something like this (without the defs part):

    var force = d3.layout.force()
            .gravity(.05)
            .distance(150)
            .charge(-1000)
            .size([width, height])
            .nodes(this.data.nodes)
            .links(this.data.links)
            .start();

    var node = svg.selectAll(".node").data(this.data.nodes).enter().append("g").attr({
        "class": function (d) {
            return d.groups.join(" ");
        }
    }).call(force.drag);
    node.append("use").attr({
        "class": "container",
        "xlink:href": "#container"
    });
    node.append("use").attr({
        "class": "icon",
        "xlink:href": function (d) {
            return d.icon;
        }
    });
    node.append("text").attr({
        "class": "label",
        "text-anchor": "middle",
        "alignment-baseline": "middle",
        dy: 40
    }).text(function (d) {
        return d.label
    });

    node.append("g").attr({
        "class": "input",
        transform: "translate(-31,-16)"
    }).selectAll("use").data(function (d) {
        return d.inputs;
    }).enter().append("use").attr({
        y: function (d, index) {
            return 8 * index;
        },
        "class": "port",
        "xlink:href": "#port"
    }).append("title").text(String);

    node.append("g").attr({
        "class": "output",
        transform: "translate(31,-16)"
    }).selectAll("use").data(function (d) {
        return d.outputs;
    }).enter().append("use").attr({
        y: function (d, index) {
            return 8 * index;
        },
        "class": "port",
        "xlink:href": "#port"
    }).append("title").text(String);

    var link = svg.selectAll(".link").data(this.data.links).enter().append("path").attr({
        "class": function (d) {
            return d.groups.join(" ");
        },
        "marker-end": "url(#arrow)"
    });

    force.on("tick", function () {
        link.attr("d", function (d) {
            var sx = d.source.x + 31 + 6;
            var sy = d.source.y - 16 + d.sourceIndex * 8;
            var tx = d.target.x - 31 - 6;
            var ty = d.target.y - 16 + d.targetIndex * 8;
            return "M" + sx + "," + sy + " " + tx + "," + ty;
        });
        node.attr("transform", function (d) {
            return "translate(" + d.x + "," + d.y + ")";
        });
    });
查看更多
登录 后发表回答