Built with blockbuilder.org
forked from shimizu's block: D3 v4 - force layout
forked from JazzTap's block: D3 v4 - force layout
license: gpl-3.0 |
Built with blockbuilder.org
forked from shimizu's block: D3 v4 - force layout
forked from JazzTap's block: D3 v4 - force layout
<html> | |
<head> | |
<style> | |
.links path { | |
fill: none; | |
stroke-width: 3px; | |
stroke: #000; | |
} | |
.nodes circle { | |
fill-opacity: 0.2; | |
} | |
</style> | |
</head> | |
<body> | |
<svg id="graph" width="700" height="1000"> | |
</svg> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/4.1.1/d3.min.js"></script> | |
<script> | |
!(function(){ | |
var svg = d3.select("#graph").append("g") | |
.attr('transform', 'translate(200 200)') | |
console.log("reload") | |
// KNOT | |
// generator | |
n = 4; two_n = 2*n | |
ids = d3.range(-n,0).concat(d3.range(1,n+1)) | |
var drawFrom = function(rest) { | |
i = Math.floor(Math.random()*rest.length) | |
pick = rest[i]; rest.splice(i,1) // drop one item, the picked one | |
return pick | |
} | |
code = d3.range(0,2*n).map(function() {return drawFrom(ids)} ) | |
console.log(code) | |
// constant override (trefoil) | |
// each index appears twice in the gauss code, once under (-) and once over (+) | |
// code = [-1, 3, -2, 1, -3, 2] // origin 1 indexed, by unfortunate convention | |
// two_n = code.length; n = two_n/2 | |
nodes = d3.range(0,2*n).map(function(i) { return {} }) | |
// state - starts as [0]s, should end as [2]s | |
entrances = d3.range(0,n).map(function() {return 0}) | |
exits = d3.range(0,n).map(function() {return 0}) | |
// sliding window with wrap, shifted to origin 0 | |
// ports indexed as [left, right] per crossing | |
links = d3.range(0,two_n).map(function(i) { | |
j = i==0 ? two_n-1 : i-1 | |
u = Math.abs(code[j])-1; v = Math.abs(code[i])-1 | |
ret = {source: 2*u+1, target: 2*v} | |
return ret | |
}) | |
console.log(links) | |
// FIXME: orient crossings | |
// Math.sign(code[i]) for i in [0,2n) | |
skeletons = d3.range(0,n).map(function(i) { | |
return {source: 2*i, target: 2*i+1, fake: true} | |
// TODO: draw bisector w/o force | |
}) | |
// D3 SETUP | |
var port = svg.append("g") | |
.attr("class", "nodes") | |
.selectAll("circle").data(nodes) | |
.enter().append("circle") | |
.attr("r", 10) | |
.attr("fill", function(d,i) { | |
return i==0 ? '#955' | |
: i==1 ? '#595' | |
: i==2 ? '#559' | |
: '#aaa' }) | |
.call(d3.drag() | |
.on("start", dragstarted) | |
.on("drag", dragged) | |
.on("end", dragended)); | |
port.append("text") | |
.text("+") | |
var link = svg.append("g") | |
.attr("class", "links") | |
.selectAll("path").data(links.concat(skeletons)) | |
.enter().append("svg:path") | |
.attr("stroke-dasharray", function(d) { | |
return d.fake ? [10,5] : [0,0] }) | |
.attr("stroke-opacity", .5) | |
.attr("pointer-events", "none"); | |
// simulation callback, binding | |
var simulation = d3.forceSimulation() | |
.force("link", d3.forceLink().id(function(d) { return d.index }), | |
d3.forceLink().distance(function(d) {return d.fake ? 10 : 50})) | |
.force("charge", d3.forceManyBody().strength(-100)) // separate | |
.force("y", d3.forceY(0)) // smoosh together | |
.force("x", d3.forceX(0)) // ditto | |
// .force("collide",d3.forceCollide( function(d){return d.r+8 }).iterations(16)) | |
// .force("center", d3.forceCenter(200, 200)) | |
var ticked = function() { | |
// elliptical segments: http://stackoverflow.com/a/16371890 | |
console.log(links.map( function(u) {return u.prev.x} )) | |
link.attr("d", function(d) { | |
d.prev = nodes[d.source.id - 1] | |
d.next = nodes[d.target.id + 1] | |
var sx = d.source.x, sy = d.source.y, | |
tx = d.target.x, ty = d.target.y | |
// position along internal joint | |
var sdx, sdy, tdx, tdy | |
if (!d.fake) { | |
sdx = d.source.x - d.prev.x, sdy = d.source.y - d.prev.y, | |
tdx = d.next.x - d.target.x, tdy = d.next.y - d.target.y | |
} | |
else {sdx = sdy = tdx = tdy = 0} | |
var xlen = tx - sx, ylen = ty - sy, | |
dr = d.fake ? 0 : Math.sqrt(xlen*xlen + ylen*ylen); | |
// try to correct location of the link-as-node | |
// var x = sx + dx/2 + dr, | |
// y = sy + dy/2 + dr | |
return "M" + sx + "," + sy + | |
"A" + dr + "," + dr + | |
" 0 0,1 " + tx + "," + ty; | |
}) | |
port | |
.attr("cx", function(d) { return d.x; }) | |
.attr("cy", function(d) { return d.y; }) | |
.selectAll("text") | |
.attr("x", function(d) { return d.x; }) | |
.attr("y", function(d) { return d.y; }) | |
/* var portsIn = function(d,i) { | |
return d3.range(4*i, 4*i+4).map(function(i) {return ports[i]}) }; | |
overlay | |
.attr("cx", function(d,i) { | |
return d3.mean(portsIn(d,i).map(function(u) {return u.x}) ) }) | |
.attr("cy", function(d,i) { | |
return d3.mean(portsIn(d,i).map(function(u) {return u.y}) ) }); */ | |
} | |
simulation | |
// avoid link centers: http://blockbuilder.org/sxywu/9354026 | |
.nodes(nodes.concat(links)) | |
// .on("tick", ticked); // TOGGLE off to prevent error spew | |
simulation | |
.force("link") | |
.links(links.concat(skeletons)); | |
function dragstarted(d) { | |
if (!d3.event.active) simulation.alphaTarget(0.3).restart(); | |
d.fx = d.x; | |
d.fy = d.y; | |
} | |
function dragged(d) { | |
d.fx = d3.event.x; | |
d.fy = d3.event.y; | |
} | |
function dragended(d) { | |
if (!d3.event.active) simulation.alphaTarget(0); | |
d.fx = null; | |
d.fy = null; | |
} | |
// TODO: rigid skeletons | |
// https://bl.ocks.org/enjalot/36014350348825ded276 | |
/* skeletons = d3.range(0,two_n).map(function(i){ | |
ln = function(a,b, through=false) { | |
return {internal: true, fake: !through, | |
source: a, target: b}} | |
first = i < n | |
j = first ? 5*i : 5*(i-n) // select intersection | |
// console.log(first, j) | |
// [through, leg, leg] | |
// return first ? [ln(j+1,j+2,true), ln(j+1,j), ln(j+2,j)] | |
// : [ln(j+3,j+4,true), ln(j+3,j), ln(j+4,j)] | |
// [across, diagonal, diagonal] w/o central node | |
// return first ? [ln(j,j+1,true), ln(j,j+2), ln(j,j+3)] | |
// : [ln(j,j+1,true), ln(j,j-2), ln(j,j-1)] | |
}) | |
skeletons = [].concat.apply([], skeletons) */ | |
/* var overlay = svg.append("g") | |
.attr("class", "overlay") | |
.selectAll("circle").data(crossings) | |
.enter().append("circle") | |
.attr("r", 30) // TODO: adjust for wide spreads? | |
.attr("fill", '#99e') | |
.attr("fill-opacity", .1) | |
// FIXME: add drag event affecting all ports | |
.on("click", function(d, i) { | |
up = [d.ul, d.ur, d.br, d.bl].map(function(goal, corner){ | |
return {target: ports[goal], | |
source: ports[4*i+corner], idx: i} }) | |
Array.prototype.splice.apply(links, [4*i, 4].concat(up)) | |
console.log(up) | |
// general update pattern bl.ocks.org/mbostock/1095795 | |
link = link.data(links) | |
link.exit().remove() | |
link = link.enter().append("svg:path").merge(link) | |
ticked() // prevent staleness | |
}) */ | |
}()); | |
</script> | |
</body> | |
</html> |