I want to achieve something like this:
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:
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?
I think I have a solution:
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:
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:
and the js is something like this (without the defs part):