Built with blockbuilder.org
forked from shimizu's block: D3 v4 - force layout
forked from anonymous'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 anonymous's block: D3 v4 - force layout
| <html> | |
| <head> | |
| <style> | |
| .links path { | |
| fill: none; | |
| stroke-width: 3px; | |
| } | |
| .nodes circle { | |
| fill-opacity: 0.5; | |
| } | |
| </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)') | |
| n = 4 | |
| ids = d3.range(0,n).map(function(j){ | |
| return [4*j, 4*j+1] | |
| }) | |
| ids = [].concat.apply([], ids) | |
| var drawFrom = function(rest) { | |
| // select a random item, and remove it from the deck | |
| i = Math.floor(Math.random()*rest.length) | |
| pick = rest[i] | |
| rest.splice(i,1) // i = rest.indexOf(pick) | |
| // TODO: prevent ports from picking their own up/down mirror | |
| // console.log(rest, pick, i) | |
| return pick | |
| } | |
| // KNOT | |
| crossings = d3.range(0,n).map(function(j){ | |
| return {state: 0, // TODO: use 'state' for smoothings | |
| ul: 4*j+2, ur: 4*j+3, | |
| bl: drawFrom(ids), br: drawFrom(ids)} | |
| }) | |
| links = crossings.map(function(n, i) { | |
| return [n.ul, n.ur, n.br, n.bl].map(function(goal, corner){ | |
| return {target: goal, source: 4*i+corner} }) | |
| }) | |
| links = Array.prototype.concat.apply([], links) | |
| // FIXME: preserve over/underness of crossings. | |
| // tour links from base points on each knot? | |
| // FIGURE | |
| ports = d3.range(0,4*n).map(function(j){ | |
| return {id: j} | |
| }) | |
| /* FIXME: given a port, return its successor (link) and predecessor (link) */ | |
| fakelinks = d3.range(0,n).map(function(j){ | |
| i = 4*j | |
| // TODO: make permutations concise | |
| ln = function(a,b) {return {fake: true, | |
| source: a, target: b}} | |
| return [ln(i,i+1), ln(i,i+2), ln(i,i+3), | |
| ln(i+1,i+2), ln(i+1,i+3), ln(i+2,i+3)] | |
| }) | |
| links = Array.prototype.concat.apply(links, fakelinks) | |
| // d3 setup, binding | |
| var simulation = d3.forceSimulation() | |
| .force("link", d3.forceLink().id(function(d) { return d.index })) | |
| // .force("collide",d3.forceCollide( function(d){return d.r + 8 }).iterations(16) ) | |
| .force("charge", d3.forceManyBody().strength(-100)) | |
| // .force("center", d3.forceCenter(chartWidth / 2, chartWidth / 2)) | |
| .force("y", d3.forceY(0)) | |
| .force("x", d3.forceX(0)) | |
| var port = svg.append("g") | |
| .attr("class", "nodes") | |
| .selectAll("circle").data(ports) | |
| .enter().append("circle") | |
| .attr("r", 10) | |
| .attr("fill", function(d,i) { | |
| return i==0 ? '#511' : i==1 ? '#373' : i==2 ? '#559' : | |
| i%2==0 ? '#aaa' : '#ccc' }) | |
| .call(d3.drag() | |
| .on("start", dragstarted) | |
| .on("drag", dragged) | |
| .on("end", dragended)); | |
| var link = svg.append("g") | |
| .attr("class", "links") | |
| .selectAll("path").data(links) | |
| .enter().append("svg:path") | |
| .attr("stroke", function(d,k) { | |
| v = d.fake ? 0 : | |
| Math.floor(4 * (4*n - k)/n).toString(16) // \in [0,f] | |
| return d.source==0 ? '#955' : d.source==1 ? '#373' : d.source==2 ? '#559' : | |
| k ? '#'+v+v+v : '#000' }) | |
| .attr("stroke-dasharray", function(d) { | |
| return d.fake ? [10,5] : [0,0] }) | |
| .attr("stroke-opacity", function(d) { | |
| return d.fake ? .1 : 1 }) | |
| .attr("pointer-events", "none") | |
| 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) { | |
| // FIXME: must update incoming edges for horz smoothing, | |
| // else have (3,1) edges and not (2,2) ea. side | |
| out0 = d.state < 2 ? d.br : d.state == 2 ? d.bl : d.ur | |
| out1 = d.state < 2 ? d.bl : d.br | |
| d.state = (d.state + 1) % 4 | |
| console.log(d.state, out0, out1) | |
| // TODO: more concise state-switch | |
| if (d.state == 0) { // over | |
| d.ul = 4*i+2; d.ur = 4*i+3; | |
| d.bl = out1; d.br = out0; | |
| } | |
| else if (d.state == 1) { // under (FIXME: implement) | |
| d.ul = 4*i+2; d.ur = 4*i+3; | |
| d.bl = out1; d.br = out0; | |
| } | |
| else if (d.state == 2) { // vert | |
| d.ul = 4*i+3; d.ur = 4*i+2; | |
| d.bl = out0; d.br = out1; | |
| } | |
| else if (d.state == 3) { // horz | |
| d.ul = 4*i+1; d.ur = out0; | |
| d.bl = 4*i+2; d.br = out1; | |
| } | |
| 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(links[4*i]) | |
| 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() | |
| }) | |
| // simulation callback, binding | |
| var ticked = function() { | |
| // elliptical segments: http://stackoverflow.com/a/16371890 | |
| link.attr("d", function(d) { | |
| var dx = d.target.x - d.source.x, | |
| dy = d.target.y - d.source.y, | |
| dr = d.fake ? 0 : Math.sqrt(dx * dx + dy * dy); | |
| return "M" + d.source.x + "," + d.source.y + | |
| "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y; | |
| }); | |
| port | |
| .attr("cx", function(d) { return d.x; }) | |
| .attr("cy", 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 | |
| .nodes(ports) | |
| .on("tick", ticked); | |
| simulation | |
| .force("link") | |
| .links(links); | |
| 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; | |
| } | |
| }()); | |
| </script> | |
| </body> | |
| </html> |