|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
body { |
|
/*margin: 200px 500px 100px 500px;*/ |
|
} |
|
#inputs { |
|
display: inline-block; |
|
margin: 0 0 0 100px; |
|
border: none; |
|
padding: 0 0 0 1em; |
|
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").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"}), |
|
constraint = inputs.append("label") |
|
.attr("for", "sideConstraint") |
|
.text("constraint: ") |
|
.append("input") |
|
.attr({id: "sideConstraint", class: "numIn", type: "number", min:"0", max: "1", step: "0.05", inputmode: "numeric"}); |
|
windUp = inputs.append("label") |
|
.attr("for", "windUp") |
|
.text("windUp: ") |
|
.append("input") |
|
.attr({id: "windUp", class: "numIn", type: "number", min:"0", max: "1", step: "0.01", inputmode: "numeric"}); |
|
|
|
var elapsedTime = ElapsedTime("#metrics", { |
|
border: 0, margin: 0, "box-sizing": "border-box", |
|
padding: "0 0 0 6px", 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 = 0, // separation between nodes |
|
maxRadius = 6; |
|
|
|
var n = 2000, // total number of nodes |
|
m = 10; // number of distinct layers |
|
|
|
var color = d3.scale.category10() |
|
.domain(d3.range(m)); |
|
|
|
var y = d3.scale.ordinal() |
|
.domain(d3.range(m)) |
|
.rangePoints([height, 0], 1), |
|
w = d3.scale.ordinal() |
|
.domain(d3.range(m)) |
|
.rangeBands([height, 0]), |
|
wRange = w.range(); |
|
|
|
var force = d3.layout.force() |
|
.size([width, height]) |
|
.gravity(0) |
|
.charge(0) |
|
.friction(0.7) |
|
.on("tick", tick) |
|
.on("start", function () { |
|
elapsedTime.start(1000); |
|
}); |
|
|
|
var svg = d3.select("body").append("svg") |
|
.attr("width", width) |
|
.attr("height", height) |
|
.append("g"), |
|
bubble = Bubble(svg); |
|
svg.selectAll("wells").data(wRange).enter().append("rect") |
|
.attr({width: width, height: w.rangeBand(), y: function(d){return d}}) |
|
var viz = update(force, n, padding); |
|
|
|
nodeCount |
|
.property("value", n) |
|
.on("change", function() { |
|
viz = update(force, this.value, padding); |
|
this.blur(); |
|
}); |
|
constraint |
|
.property("value", 0.05) |
|
.value = function () { return 1 - this.property("value")}; |
|
windUp |
|
.property("value", 0.01) |
|
.value = function () { return +this.property("value")}; |
|
|
|
elapsedTime.selection.style({ |
|
width: (width - parseFloat(window.getComputedStyle(d3.select("#inputs").node()).getPropertyValue("width"))) + "px" |
|
}); |
|
console.log("getBoundingClientRect\t" + d3.select("#inputs").node().getBoundingClientRect().width |
|
+ "getComputedStyle\t" + parseFloat(window.getComputedStyle(d3.select("#inputs").node()).getPropertyValue("width"))) |
|
|
|
function tick(e) { |
|
var a = e.alpha; |
|
elapsedTime.mark(a); |
|
viz.circle |
|
.each(gravity(a)) |
|
.each(viz.collide(1.9)) |
|
.attr({ |
|
cx : function (d) { |
|
return d.x; |
|
}, |
|
cy : function (d) { |
|
return d.y; |
|
} |
|
}) |
|
.style("stroke", function (d) { |
|
return d.frustration() > 1 ? "white" : "black"; |
|
} |
|
); |
|
force.alpha(a/0.99*0.999) |
|
} |
|
|
|
// Move nodes toward cluster focus. |
|
function gravity(alpha) { |
|
return function(d) { |
|
//reflect off the edges of the container |
|
var r = d.radius, c = constraint.value(); |
|
if (d.x - r <= 0 && d.q.px >= d.q.x) boundary(d, "x", [0, width], c); |
|
if (d.x + r >= width && d.q.px <= d.q.x) boundary(d, "x", [width, 0], c); |
|
if (d.y - r <= 0 && d.q.py >= d.q.y) boundary(d, "x", [width, 0], 0); |
|
if (d.y + r >= height && d.q.py <= d.q.y) boundary(d, "x", [width, 0], 0); |
|
//find the layers |
|
d.y += (d.cy - d.y) * d.frustration() * alpha; |
|
}; |
|
function boundary(p, y, b, c) { |
|
if(p.q[y] === p.q["p"+y]) { |
|
p[y] += (c === 0) || (Math.random() > c) ? ((b[0] < b[1]) ? 1 : -1) :0; |
|
}else { |
|
p["p" + y] = 2 * p[y] - p["p" + y]; |
|
} |
|
} |
|
} |
|
|
|
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, 3), |
|
mq = Math.pow(d.radius, 3), |
|
mT = m + mq; |
|
if (l < r) { |
|
//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 * 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) { |
|
force.nodes(d3.range(n).map(function() { |
|
var layer = Math.floor(Math.random() * m), |
|
// v = (layer + 1) / m * -Math.log(Math.random()); |
|
v = -Math.log(Math.random()); |
|
return { |
|
radius: Math.sqrt(v) * maxRadius, |
|
color : layer, |
|
cy : y(layer), |
|
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; |
|
} |
|
})() |
|
}; |
|
})); |
|
// var collide = Collide(force.nodes(), padding); |
|
force.start(); |
|
force.nodes().forEach(function(d) { |
|
d.q = {}; |
|
Object.keys(d).forEach(function(p){ |
|
if(!isNaN(d[p])) Object.defineProperty(d.q, p, { |
|
get: function () {return Math.round(d[p])} |
|
}); |
|
}) |
|
}); |
|
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; |
|
}) |
|
// .style("fill", function (d) { |
|
// return d.color; |
|
// }) |
|
.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> |
|
|