|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
body { |
|
/*margin: 200px 500px 100px 500px;*/ |
|
} |
|
|
|
#inputs { |
|
display: inline-block; |
|
margin: 0 0 0 0.5em; |
|
} |
|
|
|
#panel { |
|
display: inline-block; |
|
margin: 0 0 0 100px; |
|
border: none; |
|
box-sizing: border-box; |
|
background-color: black; |
|
} |
|
|
|
#metrics { |
|
display: inline-block; |
|
} |
|
|
|
label, input { |
|
text-align: left; |
|
width: 3.5em; |
|
color: orange; |
|
/*padding-left: 1em;*/ |
|
background-color: black; |
|
outline: none; |
|
border: none; |
|
} |
|
|
|
circle { |
|
stroke: black; |
|
} |
|
|
|
svg { |
|
display: block; |
|
overflow: visible; |
|
border: none; |
|
background: black; |
|
margin: 0 100px 0 100px; |
|
} |
|
|
|
text { |
|
text-anchor: middle; |
|
} |
|
|
|
rect { |
|
stroke: #ccc; |
|
} |
|
|
|
</style> |
|
<body> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script> |
|
<!--<script src="d3 CB.js"></script>--> |
|
<script |
|
src="https://cdnjs.cloudflare.com/ajax/libs/tinycolor/1.1.2/tinycolor.min.js"></script> |
|
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/filters/shadow.js"></script> |
|
<script |
|
src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/elapsedTime/elapsed-time-1.0.js"></script> |
|
<script> |
|
var inputs = d3.select("body").append("div") |
|
.attr("id", "metrics") |
|
.attr("id", "panel") |
|
.append("div").attr({id: "inputs"}), |
|
nodeCount = inputs.append("label") |
|
.attr("for", "nodeCount") |
|
.text("nodes: ") |
|
.append("input") |
|
.attr({id : "nodeCount", |
|
class : "numIn", |
|
type : "number", |
|
min : "100", |
|
max : "5,000", |
|
step : "100", |
|
inputmode: "numeric" |
|
}); |
|
|
|
var elapsedTime = ElapsedTime("#panel", { |
|
border : 0, margin: 0, "box-sizing": "border-box", |
|
padding: "0 0 0 3px", background: "black", "color": "orange" |
|
}) |
|
.message(function(value) { |
|
var this_lap = this.lap().lastLap, aveLap = this.aveLap(this_lap) |
|
return 'alpha:' + d3.format(" >7,.3f")(value) |
|
+ '\ttick time:' + d3.format(" >8,.4f")(this_lap) |
|
+ ' (' + d3.format(" >4,.3f")(this.aveLap(this_lap)) + ')' |
|
+ '\tframe rate:' + d3.format(" >4,.1f")(1 / aveLap) + " fps" |
|
}), |
|
|
|
width = 960 - 200, |
|
height = 500 - elapsedTime.selection.node().clientHeight, |
|
padding = 4, // separation between nodes |
|
maxRadius = 7; |
|
|
|
var n = 500, // total number of nodes |
|
m = 1, // number of distinct layers |
|
c = 10, |
|
g = 0.2, g2 = 0.1, |
|
f1 = 0.5, f2 = 0.01, |
|
q2 = -40; |
|
|
|
var tick = (function() { |
|
var phase = -1, stage1 = true; |
|
|
|
function tick(e) { |
|
viz.circle.each(viz.collide(e.alpha * 40)); |
|
if(e.alpha < 0.02 || !(phase = ++phase % 4)) { |
|
elapsedTime.mark(e.alpha); |
|
viz.circle.attr({ |
|
cx: function(d) { |
|
return d.x; |
|
}, |
|
cy: function(d) { |
|
return d.y; |
|
} |
|
}); |
|
} |
|
if(stage1 && e.alpha < 0.03) { |
|
console.log("stage2") |
|
force.friction(f2) |
|
.charge(q2) |
|
.gravity(g2) |
|
.start().alpha(e.alpha); |
|
stage1 = false; |
|
} |
|
force.alpha(e.alpha / 0.99 * 0.998) |
|
} |
|
|
|
tick.reset = function() { |
|
stage1 = true; |
|
}; |
|
return tick; |
|
})(), |
|
force = d3.layout.force() |
|
.size([width, height]) |
|
.gravity(g) |
|
.charge(0) |
|
.friction(f1) |
|
.on("tick", tick) |
|
.on("start", function() { |
|
elapsedTime.start(1000); |
|
force |
|
.gravity(g) |
|
.charge(0) |
|
.friction(f1) |
|
tick.reset(); |
|
}); |
|
force.drag().on("dragend", function(){force.alpha(0.05)}) |
|
|
|
var svg = d3.select("body").append("svg") |
|
.attr("width", width) |
|
.attr("height", height) |
|
.append("g"), |
|
bubble = Bubble(svg); |
|
|
|
var viz = update(force, n, padding); |
|
|
|
nodeCount |
|
.property("value", n) |
|
.on("change", function() { |
|
viz = update(force, this.value, padding); |
|
this.blur(); |
|
}); |
|
|
|
elapsedTime.selection.style({ |
|
width: (width |
|
- parseFloat(window.getComputedStyle(d3.select("#inputs").node()).getPropertyValue("width")) |
|
- parseFloat(window.getComputedStyle(d3.select("#inputs").node()).getPropertyValue("margin-left"))) |
|
+ "px" |
|
}); |
|
|
|
function Collide(nodes, padding) { |
|
// Resolve collisions between nodes. |
|
var maxRadius = d3.max(nodes, function(d) { |
|
return d.radius |
|
}); |
|
return function collide(alpha) { |
|
var quadtree = d3.geom.quadtree(nodes); |
|
return function(d) { |
|
var r = d.radius + maxRadius + padding, |
|
nx1 = d.x - r, |
|
nx2 = d.x + r, |
|
ny1 = d.y - r, |
|
ny2 = d.y + r; |
|
quadtree.visit(function(quad, x1, y1, x2, y2) { |
|
var possible = !(x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1); |
|
if(quad.point && (quad.point !== d) && possible) { |
|
var x = d.x - quad.point.x, |
|
y = d.y - quad.point.y, |
|
l = Math.sqrt(x * x + y * y), |
|
r = d.radius + quad.point.radius + padding, |
|
m = Math.pow(quad.point.radius, 4), |
|
mq = Math.pow(d.radius, 4), |
|
mT = m + mq; |
|
if(l < r) { |
|
for(; Math.abs(l) == 0;) { |
|
x = Math.round(Math.random() * r); |
|
y = Math.round(Math.random() * r); |
|
l = Math.sqrt(x * x + y * y); |
|
} |
|
//move the nodes away from each other along the radial (normal) vector |
|
//taking relative mass into consideration, the sign is already established |
|
//in calculating x and y and the nodes are modelled as spheres for calculating mass |
|
l = (r - l) / l * (1 + alpha); |
|
d.x += (x *= l) * m / mT; |
|
d.y += (y *= l) * m / mT; |
|
quad.point.x -= x * mq / mT; |
|
quad.point.y -= y * mq / mT; |
|
} |
|
} |
|
return !possible; |
|
}); |
|
}; |
|
} |
|
} |
|
function initNodes(force, n, padding) { |
|
var rMax = Math.pow(500 / n * 50, 0.5); |
|
force.stop() |
|
.nodes(d3.range(n).map(function() { |
|
var u = Math.random(), |
|
v = -Math.log(u); |
|
return { |
|
radius : Math.pow(v, 0.8) * rMax, |
|
color : Math.floor(u * c), |
|
x : width / 2, |
|
y : height / 2, |
|
get v() { |
|
var d = this; |
|
return {x: d.x - d.px || d.x || 0, y: d.y - d.py || d.y || 0} |
|
}, |
|
frustration: (function() { |
|
//if they can't get home, they get angry, but, as soon as they're home, they're fine |
|
var anger = 1; |
|
return function() { |
|
var d = this, anxious = (Math.abs(d.cy - d.y) > w.rangeBand() |
|
/ 2); |
|
return anger = anxious ? anger + windUp.value() : 1; |
|
} |
|
})() |
|
} |
|
})) |
|
.start(); |
|
return Collide(force.nodes(), padding); |
|
} |
|
function update(force, n, padding) { |
|
return { |
|
collide: initNodes(force, n, padding), |
|
circle : (function() { |
|
var update = svg.selectAll("circle") |
|
.data(force.nodes()); |
|
update.enter().append("circle"); |
|
update.exit().remove(); |
|
update.attr("r", function(d) { |
|
return d.radius; |
|
}) |
|
.call(bubble.call) |
|
.call(force.drag) |
|
return update; |
|
})() |
|
}; |
|
} |
|
function Bubble(svg) { |
|
var colors = d3.range(20).map(d3.scale.category10()).map(function(d) { |
|
return filters.sphere(svg, d, 1) |
|
}); |
|
return { |
|
call: function(selection) { |
|
selection.style("fill", function(d) { |
|
return colors[d.color] |
|
}) |
|
}, |
|
map : function(d, i, data) { |
|
d.fill = colors[~~(Math.random() * 20)]; |
|
}, |
|
fill: function(d) { |
|
return d.fill |
|
} |
|
} |
|
} |
|
; |
|
|
|
</script> |
|
</body> |