inf3rno
inf3rno

Reputation: 26147

d3 force layout with node groups

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

With the following attributes:

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?

Upvotes: 2

Views: 2355

Answers (1)

inf3rno
inf3rno

Reputation: 26147

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 + ")";
        });
    });

Upvotes: 1

Related Questions