Skip to content

Instantly share code, notes, and snippets.

@filipinascimento
Created November 19, 2018 03:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save filipinascimento/20357a893d16df569e8925d14d9533f5 to your computer and use it in GitHub Desktop.
Save filipinascimento/20357a893d16df569e8925d14d9533f5 to your computer and use it in GitHub Desktop.
Barabasi Albert Model visualization
<!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>
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);
}
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