Created by Christopher Manning
This generates a Sierpinski Triangle graph using a force-directed layout.
- Use the mousewheel to increase or decrease the iterations.
Created by Christopher Manning
This generates a Sierpinski Triangle graph using a force-directed layout.
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>Force-Directed Sierpinski Triangle</title> | |
| <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/d3/2.10.0/d3.v2.min.js"></script> | |
| <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5/dat.gui.min.js"></script> | |
| <style type="text/css"> | |
| body, svg { | |
| margin: 0; | |
| } | |
| circle { | |
| fill: #fff; | |
| stroke: #999; | |
| stroke: steelBlue; | |
| stroke-width: 2.5px; | |
| } | |
| line { | |
| stroke: #999; | |
| stroke-width: 5px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <script type="text/javascript"> | |
| Math.TAU = Math.PI*2; | |
| var width = window.innerWidth || 960, | |
| height = (window.innerHeight || 500) + 150 | |
| var config = { "iterations": 5, "simulate": true, "friction": 0.9, "linkStrength": 1, "linkDistance": 450, "charge": 10, "gravity": .01, "theta": .8 }; | |
| var gui = new dat.GUI(); | |
| var iterationsChanger = gui.add(config, "iterations", 1, 7).step(1).listen(); | |
| iterationsChanger.onChange(function(value) { | |
| sierpinski(value) | |
| }); | |
| var fl = gui.addFolder('Force Layout'); | |
| var simulateChanger = fl.add(config, "simulate"); | |
| simulateChanger.onChange(function(value) { | |
| value ? force.start() : force.stop() | |
| }); | |
| var frictionChanger = fl.add(config, "friction", 0, 1); | |
| frictionChanger.onChange(function(value) { | |
| force.friction(value) | |
| force.start() | |
| }); | |
| var linkDistanceChanger = fl.add(config, "linkDistance", 0, height); | |
| linkDistanceChanger.onChange(function(value) { | |
| force.start() | |
| }); | |
| var linkStrengthChanger = fl.add(config, "linkStrength", 0, 1); | |
| linkStrengthChanger.onChange(function(value) { | |
| force.linkStrength(value) | |
| force.start() | |
| }); | |
| var chargeChanger = fl.add(config,"charge", 0, 100); | |
| chargeChanger.onChange(function(value) { | |
| force.charge(-value) | |
| force.start() | |
| }); | |
| var gravityChanger = fl.add(config,"gravity", 0, 1); | |
| gravityChanger.onChange(function(value) { | |
| force.gravity(value) | |
| force.start() | |
| }); | |
| var thetaChanger = fl.add(config,"theta", 0, 1); | |
| thetaChanger.onChange(function(value) { | |
| force.theta(value) | |
| force.start() | |
| }); | |
| var cx = width/2, | |
| cy = height/2, | |
| radius = 3, | |
| nodes = [], | |
| links = [], | |
| triangles = [], | |
| node, | |
| link, | |
| stage = 0, | |
| currentIterations = 0 | |
| var zoom = d3.behavior.zoom() | |
| .scale(config["iterations"]) | |
| .scaleExtent([1, 7]) | |
| .on("zoom", function(d,i) { | |
| config["iterations"] = Math.ceil(d3.event.scale) | |
| sierpinski(config["iterations"]) | |
| }); | |
| var force = d3.layout.force() | |
| .linkDistance(function(){ return config["linkDistance"] / Math.pow(2, config["iterations"] - 1) }) | |
| .linkStrength(config["linkStrength"]) | |
| .charge(-config["charge"]) | |
| .gravity(config["gravity"]) | |
| .friction(config["friction"]) | |
| .theta(config["theta"]) | |
| .size([width, height]) | |
| var svg = d3.select("body").append("svg") | |
| .attr("width", width) | |
| .attr("height", height) | |
| .call(zoom) | |
| sierpinski(config['iterations']) | |
| function sierpinski(iterations) { | |
| iterations = parseInt(iterations) | |
| // prevent dat-gui from calling this more than once per iteration | |
| if (stage == iterations) return | |
| stage = iterations | |
| do { | |
| currentIterations += currentIterations < iterations ? 1 : -1 | |
| renderSierpinski(currentIterations) | |
| } while(currentIterations != iterations) | |
| restart() | |
| } | |
| function renderSierpinski(iteration) { | |
| numLinks = Math.pow(3, iteration) | |
| numNodes = (Math.pow(3, iteration) + 3) / 2 | |
| if(links.length < numLinks) { | |
| // triangles are created counterclockwise | |
| if(iteration == 1) { | |
| r = config["linkDistance"]/Math.sqrt(3) | |
| // this triangle is upsidedown because the force layout flips the initial triangle | |
| nodes.push({x: cx + r*Math.cos(3*Math.TAU/4), y: cy + r*Math.sin(3*Math.TAU/4)}) | |
| nodes.push({x: cx + r*Math.cos(5*Math.TAU/12), y: cy + r*Math.sin(5*Math.TAU/12)}) | |
| nodes.push({x: cx + r*Math.cos(Math.TAU/12), y: cy + r*Math.sin(Math.TAU/12)}) | |
| links.push({source: nodes[0], target: nodes[1]}) | |
| links.push({source: nodes[1], target: nodes[2]}) | |
| links.push({source: nodes[2], target: nodes[0]}) | |
| triangles.push([links[0], links[1], links[2]]) | |
| } else { | |
| chunk = 3 | |
| for (i=0,j=links.length; i<j; i+=chunk) { | |
| rl = links[i] | |
| bl = links[i+1] | |
| ll = links[i+2] | |
| nodes.push({x: (rl.source.x + rl.target.x)/2, y: (rl.source.y + rl.target.y)/2}) | |
| nodes.push({x: (bl.source.x + bl.target.x)/2, y: (bl.source.y + bl.target.y)/2}) | |
| nodes.push({x: (ll.source.x + ll.target.x)/2, y: (ll.source.y + ll.target.y)/2}) | |
| nl = nodes.length | |
| rn = nodes[nl-3] | |
| bn = nodes[nl-2] | |
| ln = nodes[nl-1] | |
| links.push({source: rl.source, target: rn}) | |
| links.push({source: rn, target: ln}) | |
| links.push({source: ln, target: rl.source}) | |
| links.push({source: rn, target: bl.source}) | |
| links.push({source: bl.source, target: bn}) | |
| links.push({source: bn, target: rn}) | |
| links.push({source: ln, target: bn}) | |
| links.push({source: bn, target: ll.source}) | |
| links.push({source: ll.source, target: ln}) | |
| } | |
| links.splice(0, Math.pow(3, iteration-1)) | |
| } | |
| } else { | |
| chunk = 9 | |
| for (i=0,j=links.length; j/chunk>0; j-=chunk,i++) { | |
| o = i * chunk | |
| links.push({source: links[o].source, target: links[o+3].target}) | |
| links.push({source: links[o+3].target, target: links[o+7].target}) | |
| links.push({source: links[o+7].target, target: links[o].source}) | |
| } | |
| links.splice(0, Math.pow(3, iteration+1)) | |
| nodes.length = numNodes | |
| } | |
| } | |
| function restart() { | |
| force.nodes(nodes).links(links) | |
| force.start() | |
| link = svg.selectAll("line.link") | |
| .data(links) | |
| link.enter().insert("line") | |
| .attr("class", "link") | |
| link.exit().remove() | |
| node = svg.selectAll("circle.node") | |
| .data(nodes) | |
| node.enter().insert("circle") | |
| .attr("class", "node") | |
| .attr("r", radius) | |
| .call(force.drag) | |
| node.exit().remove() | |
| } | |
| force.on("tick", function() { | |
| node | |
| .attr("transform", function(d) { | |
| return "translate("+d.x+"," + d.y+ ")" | |
| }) | |
| 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 }) | |
| config['simulate'] ? null : force.stop() | |
| }) | |
| </script> | |
| </body> | |
| </html> |