D3 Force Layout:如何动态定位泪珠形节点?
D3 Force Layout: How to dynamically orient teardrop-shaped nodes?
我想要具有动态方向的泪珠形节点,这样节点的方向性就可以单独显示其源节点和目标节点。
到目前为止,这是我的代码(泪珠的代码取自此示例:http://bl.ocks.org/mbostock/849853)及其生成的图形:
http://jsbin.com/kigiwinida/edit?html,output
但我最终想要的是这张图:
我认为使泪珠的方向动态化的关键是为 rotate() 函数提供正确的参数;使泪珠相应地旋转,以便与它们作为目标的边缘对齐。
像这样...
var w = window.innerWidth*2,
h = window.innerHeight*3,
r = 10,
charge = 300,
def_color = "#bdbdbd"; //default node color
var dataset = {"nodes":
[
{"x": 469, "y": 410},
{"x": 493, "y": 364},
{"x": 442, "y": 365},
{"x": 467, "y": 314},
{"x": 477, "y": 248},
{"x": 425, "y": 207},
{"x": 402, "y": 155},
{"x": 369, "y": 196},
{"x": 350, "y": 148},
{"x": 539, "y": 222},
{"x": 594, "y": 235},
{"x": 582, "y": 185},
{"x": 633, "y": 200}
],
"edges": [
{"source": 0, "target": 1},
{"source": 1, "target": 2},
{"source": 2, "target": 0},
{"source": 1, "target": 3},
{"source": 3, "target": 2},
{"source": 3, "target": 4},
{"source": 4, "target": 5},
{"source": 5, "target": 6},
{"source": 5, "target": 7},
{"source": 6, "target": 7},
{"source": 6, "target": 8},
{"source": 7, "target": 8},
{"source": 9, "target": 4},
{"source": 9, "target": 11},
{"source": 9, "target": 10},
{"source": 10, "target": 11},
{"source": 11, "target": 12},
{"source": 12, "target": 10}
]
};
//Initialize a default force layout, using the nodes and edges in dataset
var force = d3.layout.force()
.nodes(dataset.nodes)
.links(dataset.edges)
.size([w, h])
.linkDistance([70]);
//Create SVG container element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h)
.attr("class", "svg-container")
//Build the arrow
svg.append("defs").selectAll("marker")
.data(["suit", "licensing", "resolved"])
.enter().append("marker")
.attr("id", function (d) { return d; })
.attr("viewBox", "0 -5 10 10")
.attr("refX", 50)
//.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5 L10,0 L0, -5")
.style("stroke", "#ccc");
//Create edges as lines
var edges = svg.selectAll("line")
.data(dataset.edges)
.enter()
.append("line")
.attr("marker-end", "url(#suit)") //Add the arrow
.style("stroke", "#ccc")
.style("stroke-width", 1);
// Create the groups under svg
var gnodes = svg.selectAll('g.gnode')
.data(dataset.nodes)
.enter()
.append('g')
.classed('gnode', true);
// Add one circle in each group
var node = gnodes.append("path")
.attr("class", "node")
.attr("d", raindrop(r))
.style("fill", def_color)
.style("stroke", "black")
.call(force.drag);
function raindrop(size) {
var r = size;
return "M" + r + ",0"
+ "A" + r + "," + r + " 0 1,1 " + -r + ",0"
+ "C" + -r + "," + -r + " 0," + -r + " 0," + -3 * r
+ "C0," + -r + " " + r + "," + -r + " " + r + ",0"
+ "Z";
}
//add an orientation member to each node
dataset.nodes.forEach(function (n, i, a) {
n.orientation = {
sinkCount: 0,
totalX: 0,
totalY: 0,
reset: function () {
this.sinkCount = this.totalX = this.totalY = 0;
}
};
Object.defineProperty(n, "angle", {
get: function () {
var o = this.orientation;
//angle of average vector
return o.sinkCount ? Math.atan2(o.totalY / o.sinkCount, o.totalX / o.sinkCount) * 180 / Math.PI : 0;
}
});
});
function limitAngle(x) {
var pi = Math.PI;
return (x > pi) ? 2 * pi - x : (x < -pi) ? 2 * pi + x : x;
}
//add link behaviour to tweek source node angle
dataset.edges.forEach(function (l, i, a) {
Object.defineProperties(l, {
"angle": {
get: function () {
var s = this.source, t = this.target;
return Math.atan2(-(t.y - s.y), t.x - s.x);
}
},
"visitTarget": {
value: function () {
var n1 = this.target, n2 = this.source, t = n1.orientation;
t.totalX += (n2.x - n1.x);
t.totalY += -(n2.y - n1.y);
t.sinkCount += 1;
}
}
})
});
force.charge([-300])
.start();
//Every time the simulation "ticks", this will be called
force.on("tick", function () {
edges.attr("x1", function (d) { return d.x1 = d.source.x; })
.attr("y1", function (d) { return d.y1 = d.source.y; })
.attr("x2", function (d) { return d.x2 = d.target.x; })
.attr("y2", function (d) { return d.y2 = d.target.y; });
dataset.nodes.forEach(function (n, i, a) {
n.orientation.reset();
});
dataset.edges.forEach(function (e, i, a) {
e.visitTarget();
});
node.attr("transform", function (d) {
return 'translate(' + [d.x, d.y] + ') rotate(' + -(d.angle - 90) + ")"
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
我想要具有动态方向的泪珠形节点,这样节点的方向性就可以单独显示其源节点和目标节点。 到目前为止,这是我的代码(泪珠的代码取自此示例:http://bl.ocks.org/mbostock/849853)及其生成的图形:
http://jsbin.com/kigiwinida/edit?html,output
但我最终想要的是这张图:
我认为使泪珠的方向动态化的关键是为 rotate() 函数提供正确的参数;使泪珠相应地旋转,以便与它们作为目标的边缘对齐。
像这样...
var w = window.innerWidth*2,
h = window.innerHeight*3,
r = 10,
charge = 300,
def_color = "#bdbdbd"; //default node color
var dataset = {"nodes":
[
{"x": 469, "y": 410},
{"x": 493, "y": 364},
{"x": 442, "y": 365},
{"x": 467, "y": 314},
{"x": 477, "y": 248},
{"x": 425, "y": 207},
{"x": 402, "y": 155},
{"x": 369, "y": 196},
{"x": 350, "y": 148},
{"x": 539, "y": 222},
{"x": 594, "y": 235},
{"x": 582, "y": 185},
{"x": 633, "y": 200}
],
"edges": [
{"source": 0, "target": 1},
{"source": 1, "target": 2},
{"source": 2, "target": 0},
{"source": 1, "target": 3},
{"source": 3, "target": 2},
{"source": 3, "target": 4},
{"source": 4, "target": 5},
{"source": 5, "target": 6},
{"source": 5, "target": 7},
{"source": 6, "target": 7},
{"source": 6, "target": 8},
{"source": 7, "target": 8},
{"source": 9, "target": 4},
{"source": 9, "target": 11},
{"source": 9, "target": 10},
{"source": 10, "target": 11},
{"source": 11, "target": 12},
{"source": 12, "target": 10}
]
};
//Initialize a default force layout, using the nodes and edges in dataset
var force = d3.layout.force()
.nodes(dataset.nodes)
.links(dataset.edges)
.size([w, h])
.linkDistance([70]);
//Create SVG container element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h)
.attr("class", "svg-container")
//Build the arrow
svg.append("defs").selectAll("marker")
.data(["suit", "licensing", "resolved"])
.enter().append("marker")
.attr("id", function (d) { return d; })
.attr("viewBox", "0 -5 10 10")
.attr("refX", 50)
//.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5 L10,0 L0, -5")
.style("stroke", "#ccc");
//Create edges as lines
var edges = svg.selectAll("line")
.data(dataset.edges)
.enter()
.append("line")
.attr("marker-end", "url(#suit)") //Add the arrow
.style("stroke", "#ccc")
.style("stroke-width", 1);
// Create the groups under svg
var gnodes = svg.selectAll('g.gnode')
.data(dataset.nodes)
.enter()
.append('g')
.classed('gnode', true);
// Add one circle in each group
var node = gnodes.append("path")
.attr("class", "node")
.attr("d", raindrop(r))
.style("fill", def_color)
.style("stroke", "black")
.call(force.drag);
function raindrop(size) {
var r = size;
return "M" + r + ",0"
+ "A" + r + "," + r + " 0 1,1 " + -r + ",0"
+ "C" + -r + "," + -r + " 0," + -r + " 0," + -3 * r
+ "C0," + -r + " " + r + "," + -r + " " + r + ",0"
+ "Z";
}
//add an orientation member to each node
dataset.nodes.forEach(function (n, i, a) {
n.orientation = {
sinkCount: 0,
totalX: 0,
totalY: 0,
reset: function () {
this.sinkCount = this.totalX = this.totalY = 0;
}
};
Object.defineProperty(n, "angle", {
get: function () {
var o = this.orientation;
//angle of average vector
return o.sinkCount ? Math.atan2(o.totalY / o.sinkCount, o.totalX / o.sinkCount) * 180 / Math.PI : 0;
}
});
});
function limitAngle(x) {
var pi = Math.PI;
return (x > pi) ? 2 * pi - x : (x < -pi) ? 2 * pi + x : x;
}
//add link behaviour to tweek source node angle
dataset.edges.forEach(function (l, i, a) {
Object.defineProperties(l, {
"angle": {
get: function () {
var s = this.source, t = this.target;
return Math.atan2(-(t.y - s.y), t.x - s.x);
}
},
"visitTarget": {
value: function () {
var n1 = this.target, n2 = this.source, t = n1.orientation;
t.totalX += (n2.x - n1.x);
t.totalY += -(n2.y - n1.y);
t.sinkCount += 1;
}
}
})
});
force.charge([-300])
.start();
//Every time the simulation "ticks", this will be called
force.on("tick", function () {
edges.attr("x1", function (d) { return d.x1 = d.source.x; })
.attr("y1", function (d) { return d.y1 = d.source.y; })
.attr("x2", function (d) { return d.x2 = d.target.x; })
.attr("y2", function (d) { return d.y2 = d.target.y; });
dataset.nodes.forEach(function (n, i, a) {
n.orientation.reset();
});
dataset.edges.forEach(function (e, i, a) {
e.visitTarget();
});
node.attr("transform", function (d) {
return 'translate(' + [d.x, d.y] + ') rotate(' + -(d.angle - 90) + ")"
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>