Created
November 19, 2018 03:42
-
-
Save filipinascimento/20357a893d16df569e8925d14d9533f5 to your computer and use it in GitHub Desktop.
Barabasi Albert Model visualization
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<title>Barabási-Albert Model Simulation</title> | |
<link rel="stylesheet" type="text/css" href="style.css"> | |
<!-- <script src="https://d3js.org/d3.v4.min.js"></script> --> | |
<script src="https://d3js.org/d3.v4.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script> | |
<link href="https://fonts.googleapis.com/css?family=Arimo" rel="stylesheet"> | |
</head> | |
<body> | |
<div id="container"> | |
<div id="graphDiv"></div> | |
<div id="infoDiv"> | |
Originally created by <a href="https://www.sarahschoettler.com/barabasialbert/">Sarah Schoettler </a> | |
<div id="controls"> | |
<p><b>Connections of each new node</b> | |
<input type="range" name="" id="mRange" min="1" max="5" step="1" value="1" /> | |
<span class="left">1</span><span class="right">5</span></p> | |
<p><b>Update Interval</b> | |
<input type="range" name="" id="tRange" min="100" max="2000" step="50" value="1000" /> | |
<span class="left">Faster</span><span class="right">Slower</span></p> | |
<p><input type="checkbox" id="prefAt" checked><label for="prefAt"><b>Preferential attachment</b></label></p> | |
<p><input type="button" name="Restart" id="restartButton" value="Restart"></p> | |
</div> | |
</div> | |
<div id="statsDiv"></div> | |
</div> | |
<script type="text/javascript" src="simulation.js"></script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var wGraph = 800 | |
var hGraph = 800 | |
var barWidth = 10 | |
// how many edges does each incoming node form | |
var m = 1; | |
// add a new node every two seconds | |
var twoSeconds = d3.interval(everyInterval, 1000); | |
d3.select("#restartButton").on("click", function() { | |
m = Number(document.getElementById("mRange").value) | |
t = Number(document.getElementById("tRange").value) | |
twoSeconds.stop(); | |
d3.interval(everyInterval, +t); | |
prefAt = document.getElementById("prefAt").checked | |
console.log("m=", m, ", prefAt = ", prefAt) | |
resetGraph(); | |
}) | |
// create svg | |
var graphSVG = d3.select("#graphDiv") | |
.append("svg") | |
.attr("height", hGraph) | |
.attr("width", wGraph) | |
// create g elements for edges and nodes | |
var edgesG = graphSVG.append("g") | |
var nodesG = graphSVG.append("g") | |
//var statsG = statsSVG.append("g") | |
// initial graph: 2 connected nodes | |
var nodesD = [{"id": 1, "weight": 0}] | |
var nodesWeighted = [1] | |
var newNode = 1; // ids of existing nodes | |
var edgesD = [] | |
// initialise variable to store max degree measured | |
var len = 0; | |
// scaling area of node to its degree | |
var rScale = d3.scalePow() | |
.exponent(0.5) | |
.domain([0,10]) | |
.range([1,15]) | |
// initialise simulation | |
var simulation = d3.forceSimulation() | |
.force("link", d3.forceLink().id(function(d) { return d.id; })) | |
.force("charge", d3.forceManyBody().strength(-20)) | |
simulation | |
.nodes(nodesD) | |
.on("tick", ticked); | |
simulation.force("link") | |
.links(edgesD); | |
// initialise stats display | |
var margin = {top: 20, right: 20, bottom: 35, left: 35}, | |
wStats = 400 - margin.left - margin.right, | |
hStats = 350 - margin.top - margin.bottom | |
// scales | |
var xScale = d3.scaleBand().rangeRound([0, wStats]).paddingInner([0.2]).paddingOuter([0.05]) | |
var yScale = d3.scaleLinear().range([hStats,0]) | |
// axes | |
var xAxis = d3.axisBottom().scale(xScale) | |
var yAxis = d3.axisLeft().scale(yScale) | |
// append stats svg and g elements for chart and axes | |
var statsG = d3.select("#statsDiv").append("svg") | |
.attr("width", wStats + margin.left + margin.right) | |
.attr("height", hStats + margin.top + margin.bottom) | |
.append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
statsG.append("g") | |
.attr("class", "x axis") | |
.attr("transform", "translate(0," + hStats + ")") | |
statsG.append("g") | |
.attr("class", "y axis") | |
statsG.append("text") | |
.attr("text-anchor", "end") | |
.attr("transform", "rotate(270)") | |
.attr("font-size", "14px") | |
.attr("y", -20) | |
.text("Number of nodes") | |
statsG.append("text") | |
.attr("text-anchor", "end") | |
.attr("font-size", "14px") | |
.attr("x", wStats) | |
.attr("y", 325) | |
.text("Degree") | |
// this will create the initial display, afterwards, it will automatically add a new node every 2 seconds and update() | |
update() | |
function everyInterval () { | |
newNode += 1; | |
nodesD.push({"id": newNode, "weight": m, "x":wGraph*Math.random(),"y":hGraph*Math.random()}); // add new node | |
for (var k=0; k<m; k++) { | |
var tgt = chooseTarget(newNode-1) | |
edgesD.push({source: newNode, target: tgt}); // add new link | |
nodesWeighted.push(newNode, tgt) // add nodes to weighted list because they each have one more link now | |
nodesD[tgt-1].weight += 1 | |
} | |
update() | |
} | |
function update() { | |
// update nodes and edges with the updated dataset, restart the simulation | |
nodesG.selectAll(".node") | |
.data(nodesD) | |
.enter() | |
.append("circle") | |
.attr("class", "node") | |
.attr("r", function(d) {return rScale(d.weight)}) | |
.call(d3.drag() | |
.on("start", dragstarted) | |
.on("drag", dragged) | |
.on("end", dragended)); | |
d3.selectAll(".node").transition().attr("r", function(d) {return rScale(d.weight)}) | |
edgesG.selectAll(".edge") | |
.data(edgesD) | |
.enter() | |
.append("line") | |
.attr("class", "edge") | |
// restart force layout | |
simulation.nodes(nodesD); | |
simulation.force("link").links(edgesD); | |
simulation.alpha(1).restart(); | |
// update stats display | |
updateStats() | |
} | |
function ticked() { | |
// assign updated positions to nodes and edges | |
edgesG.selectAll(".edge") | |
.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; }); | |
nodesG.selectAll(".node") | |
.attr("cx", function(d) { return d.x = Math.max(rScale(d.weight), Math.min(wGraph - rScale(d.weight), d.x)); }) // | |
.attr("cy", function(d) { return d.y = Math.max(rScale(d.weight), Math.min(hGraph - rScale(d.weight), d.y)); }); | |
} | |
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; | |
} | |
function chooseTarget() { | |
// choose a target for an incoming node | |
if (prefAt) { | |
chosen = nodesWeighted[Math.floor((Math.random() * nodesWeighted.length))]; | |
} | |
else { | |
chosen = nodesD[Math.floor(Math.random() * (nodesD.length-1))].id | |
} | |
return chosen | |
} | |
function updateStats() { | |
// update stats bar chart | |
var statsD = collectStats() | |
// fix horizontal scale and axis | |
keysList = []; | |
for (var i=0; i<statsD.length; i++) { | |
keysList.push(statsD[i][0]) | |
} | |
xScale.domain(keysList) | |
if (keysList.length>10) { | |
xAxis.tickValues(keysList.filter(function(d,i) {return !(i%Math.round(keysList.length/10))})) | |
} | |
else { | |
xAxis.tickValues(keysList) | |
} | |
// fix vertical scale and axis | |
yScale.domain([0, d3.max(statsD, function(d) {return d[1]})]) | |
var maxYTick = d3.max(statsD, function(d) {return d[1]}) | |
if (maxYTick<10) { | |
yAxis.ticks(maxYTick) | |
} | |
else { | |
yAxis.ticks(10) | |
} | |
// if (maxYTick<10) { | |
// yAxis.tickValues(d3.range(maxYTick)) | |
// } | |
// else { | |
// yAxis.tickValues() | |
// } | |
// var maxXTick = d3.max(statsD, function(d) {return d[0]})+1 | |
// if (maxXTick<10) { | |
// xAxis.tickValues(d3.range(maxXTick)) | |
// } | |
// else { | |
// xAxis.tickValues().ticks(10) | |
// } | |
// axes | |
statsG.select(".x.axis").transition().duration(100).call(xAxis) | |
statsG.select(".y.axis").transition().duration(100).call(yAxis) | |
statsG.selectAll(".bar") | |
.data(statsD) | |
.enter() | |
.append("rect") | |
.attr("class", "bar") | |
.attr("y", yScale(0)) | |
.attr("height", function(d) {return hStats-yScale(0)}) | |
// transition all new/ exisiting bars | |
statsG.selectAll(".bar") | |
.transition() | |
.duration(300) | |
.attr("x", function(d) {return xScale(Number(d[0])) }) | |
.attr("width", xScale.bandwidth()) | |
.attr("y", function(d) {return yScale(d[1])}) | |
.attr("height", function(d) {return hStats - yScale(d[1])}) | |
} | |
function collectStats() { | |
// collect stats | |
// return an array [[degree, frequency], ...] | |
var count = _.countBy(nodesD, function(n) {return n.weight}) | |
var keys = Object.keys(count).map(Number) | |
len = d3.max([len, d3.max(keys)]) | |
countA = [] | |
for (var i=0; i<=len; i++) { | |
countA.push([i, 0]) | |
} | |
for (var i=0; i<keys.length; i++) { | |
countA[keys[i]][1] = count[keys[i]] | |
} | |
return countA | |
} | |
function resetGraph() { | |
// clearInterval(twoSeconds) | |
// reset data | |
nodesD = [{"id": 1, "weight": 0}] | |
nodesWeighted = [1] | |
newNode = 1; // ids of existing nodes | |
edgesD = [] | |
len = 0 | |
// clear g elements | |
d3.selectAll(".node").remove() | |
d3.selectAll(".edge").remove() | |
// restart timer | |
// twoSeconds = d3.interval(everyInterval, 2000); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
html, body { | |
padding: 0; | |
margin: 0; | |
font-family: 'Arimo', sans-serif; | |
background-color: #dee8ea | |
} | |
#container { | |
width: 1200px; | |
height: 800px; | |
/*border: 1px solid #000;*/ | |
background-color: #fff; | |
position: absolute; | |
margin: auto; | |
top: 0; | |
right: 0; | |
bottom: 0; | |
left: 0; | |
} | |
#graphDiv { | |
height: 800px; | |
width: 800px; | |
display: block; | |
position: absolute; | |
left: 0; | |
top: 0; | |
} | |
#statsDiv { | |
height: 350px; | |
width: 400px; | |
display: block; | |
position: absolute; | |
right: 0; | |
bottom: 0; | |
} | |
#infoDiv { | |
height: 410px; | |
width: 360px; | |
padding: 20px; | |
display: block; | |
position: absolute; | |
right: 0; | |
top: 0; | |
} | |
.edge { | |
stroke: #bbb; | |
stroke-opacity: 1; | |
stroke-width: 1px; | |
} | |
.node { | |
stroke: #fff; | |
stroke-width: 1.5px; | |
fill: #074E67; | |
} | |
.bar { | |
fill: #074E67; | |
} | |
.axis { | |
font: 10px sans-serif; | |
} | |
.axis path, | |
.axis line { | |
fill: none; | |
stroke: #000; | |
shape-rendering: crispEdges; | |
} | |
/*inputs infobox*/ | |
#controls { | |
text-align: center; | |
margin-top: 25px; | |
} | |
input[type=range] { | |
-webkit-appearance: none; | |
margin: 10px 0 10px; | |
width: 80%; | |
} | |
input[type=range]:focus { | |
outline: none; | |
} | |
input[type=range]::-webkit-slider-runnable-track { | |
width: 80%; | |
height: 2px; | |
cursor: pointer; | |
background: #000; | |
} | |
input[type=range]::-webkit-slider-thumb { | |
border: 0px #fff; | |
height: 20px; | |
width: 10px; | |
background: #555; | |
cursor: pointer; | |
-webkit-appearance: none; | |
margin-top: -10px; | |
} | |
input[type=range]::-moz-range-track { | |
width: 80%; | |
height: 2px; | |
cursor: pointer; | |
background: #000; | |
} | |
input[type=range]::-moz-range-thumb { | |
border: 0px #000; | |
height: 20px; | |
width: 10px; | |
background: #555; | |
cursor: pointer; | |
} | |
.left { | |
float: left; | |
margin-left: 20px; | |
} | |
.right { | |
float: right; | |
margin-right: 20px; | |
} | |
.left, .right { | |
font-weight: bold; | |
line-height: 22px; | |
} | |
/*button*/ | |
#restartButton { | |
display: inline-block; | |
position: relative; | |
background-color: #555; | |
color: #fff; | |
width: 200px; | |
padding: 12px 0; | |
text-align: center; | |
text-decoration: none; | |
font-family: Arial, sans-serif; | |
font-size: 1em; | |
border: none; | |
cursor: pointer; | |
margin-top: 10px; | |
} | |
#restartButton:hover { | |
transition: background-color 0.5s; | |
background-color: #000 | |
} | |
/*checkboxes*/ | |
label { | |
display: inline-block; | |
text-align: center; | |
margin: 0; | |
cursor: pointer; | |
line-height: 20px; | |
} | |
label:before { | |
display: block; | |
content: ''; | |
height: 20px; | |
width: 20px; | |
border: 2px solid #555; | |
float: left; | |
margin-right: 5px; | |
transition: all 0.5s ease; | |
background-color: transparent; | |
cursor: pointer; | |
} | |
input[type="checkbox"]:checked + label:before { | |
background-color: #555; | |
} | |
input[type="checkbox"] { | |
display: none; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment