|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style type="text/css"> |
|
body { |
|
width: 1024px; |
|
margin-top: 0; |
|
margin: auto; |
|
font-family: "Lato", "PT Serif", serif; |
|
color: #222222; |
|
padding: 0; |
|
font-weight: 300; |
|
line-height: 33px; |
|
-webkit-font-smoothing: antialiased; |
|
} |
|
.line{ |
|
fill: none; |
|
stroke: #000; |
|
stroke-width: 4px; |
|
} |
|
|
|
.circle{ |
|
fill-opacity: 0.5; |
|
} |
|
|
|
.box{ |
|
fill:#ecf0f1; |
|
stroke: #2c3e50; |
|
stroke-dasharray: 2px,2px; |
|
} |
|
|
|
.axis path, |
|
.axis line { |
|
fill: none; |
|
stroke: none; |
|
shape-rendering: crispEdges; |
|
} |
|
|
|
</style> |
|
<body> |
|
<script src="//d3js.org/d3.v3.min.js"></script> |
|
<script> |
|
|
|
var margin = { top: 60, right: 40, bottom: 60, left: 80 }, |
|
width = 960 - margin.left - margin.right, |
|
height = 640 - margin.top - margin.bottom; |
|
bbox = {h: height , w: width, mt: 0} |
|
|
|
var duration = 50, |
|
maxRadius = 40, // maximum radius of circle |
|
padding = 1, // padding between circles; also minimum radius |
|
k = 10, // initial number of candidates to consider per circle |
|
m = 5, // initial number of circles to add per frame |
|
n = 800, // remaining number of circles to add |
|
step = 0, |
|
count = 0, |
|
data = d3.range(2).map(function(d,i) { return {x:0, y:0}; }); |
|
newCircle = bestCircleGenerator(maxRadius, padding, bbox.w, bbox.h, bbox.mt); |
|
|
|
var x = d3.scale.linear().domain([0, n]).range([0, width]); |
|
|
|
var y = d3.scale.linear().domain([0, 1]).range([height, 0]); |
|
|
|
var color = d3.scale.threshold().domain([maxRadius*0.33, maxRadius*0.66, maxRadius]).range(['#2ecc71', '#f1c40f', '#e74c3c']); |
|
|
|
var line = d3.svg.line().interpolate("basis") |
|
.x(function(d, i) { return x(d.x); }) |
|
.y(function(d, i) { return y(d.y); }); |
|
|
|
var svg = d3.select("body").append("svg") |
|
.attr("width", width + margin.left + margin.right) |
|
.attr("height", height + margin.top + margin.bottom) |
|
.append("g") |
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); |
|
|
|
svg.append("rect") |
|
.attr("width", bbox.w) |
|
.attr("height", bbox.h) |
|
.attr('y', bbox.mt) |
|
.attr('class', 'box'); |
|
|
|
var yaxis = svg.append("g") |
|
.attr("class", "y axis") |
|
.call(y.axis = d3.svg.axis().scale(y).orient("left").ticks(5)) |
|
.append("text") |
|
.attr("class", "label") |
|
.attr("transform", "rotate(-90)") |
|
.attr("y", -56) |
|
.attr("dy", ".71em") |
|
.style("text-anchor", "end") |
|
.text("Ratio of Total Circle Area to Total Graph Area") |
|
|
|
var g = svg.append('g') |
|
|
|
var path = svg.append("path") |
|
.datum(data) |
|
.attr("class", "line"); |
|
|
|
function tick(){ |
|
var interval = setInterval(function() { |
|
for (var i = 0; i < m && --n >= 0; ++i) { |
|
step+=1 |
|
var circle = newCircle(k, step); |
|
|
|
if (circle[0]){ // if a new circle was returned |
|
g.append("circle") |
|
.attr('class', 'circle') |
|
.attr("cx", circle[0]) |
|
.attr("cy", circle[1]) |
|
.attr("r", 0) |
|
.style("fill", color(circle[2])) |
|
.transition() |
|
.attr("r", circle[2]); |
|
|
|
// Update our count. Note: some of the area extends beyond the box, which is okay. |
|
count += circle[2]*circle[2]*Math.PI |
|
|
|
// update the line every 5 circles to limit DOM manipulation. |
|
if (step % 5 == 0) { |
|
// update data |
|
data.push({x: step, y: count/ (bbox.w * bbox.h)}); |
|
// redraw the line |
|
path.attr("d", line) |
|
} |
|
} |
|
|
|
// As we add more circles, generate more candidates per circle. |
|
// Since this takes more effort, gradually reduce circles per frame. |
|
if (k < 500) k *= 1.1, m *= .998; |
|
} |
|
|
|
if(n < 0){ |
|
console.log('restart') |
|
clearInterval(interval); |
|
|
|
// Reset parameters |
|
newCircle = bestCircleGenerator(maxRadius, padding, bbox.w, bbox.h, bbox.mt); |
|
data = d3.range(2).map(function(d,i) { return {x:0, y:0}; }), |
|
n = 800, step = 0, count = 0, k = 10, m = 5; |
|
|
|
d3.selectAll('circle').transition().remove() |
|
d3.selectAll('.line').transition().remove() |
|
path = svg.append("path").datum(data).attr("class", "line"); |
|
tick(); |
|
} |
|
}, duration); |
|
} |
|
|
|
// See, http://bl.ocks.org/mbostock/6224050 |
|
function bestCircleGenerator(maxRadius, padding, w, h, topmargin) { |
|
var quadtree = d3.geom.quadtree().extent([[0, 0], [w, h]])([]), |
|
searchRadius = maxRadius * 2, |
|
maxRadius2 = maxRadius * maxRadius, |
|
topmargin = topmargin || 0; |
|
|
|
return function(k, step) { |
|
var bestX, bestY, bestDistance = padding; |
|
for (var i = 0; i < k; ++i) { |
|
var x = Math.random() * w, |
|
y = Math.random() * h + topmargin, |
|
rx1 = x - searchRadius, |
|
rx2 = x + searchRadius, |
|
ry1 = y - searchRadius, |
|
ry2 = y + searchRadius; |
|
|
|
/* The radius size is set to the stage of growth. Otherwise, the algrithm first chooses the |
|
maximum radius on the first step. We still see the "carrying capacity" effect if maxRadius2 |
|
is the default value, just with an initial linear growth as opposed to exponential. |
|
*/ |
|
var minDistance = Math.min( Math.exp(step*0.01), maxRadius2 ) ; |
|
|
|
quadtree.visit(function(quad, x1, y1, x2, y2) { |
|
if (p = quad.point) { |
|
var p, |
|
dx = x - p[0], |
|
dy = y - p[1], |
|
d2 = dx * dx + dy * dy, |
|
r2 = p[2] * p[2]; |
|
if (d2 < r2) return minDistance = 0, true; |
|
var d = Math.sqrt(d2) - p[2]; |
|
if (d < minDistance) minDistance = d; |
|
} |
|
return !minDistance || x1 > rx2 || x2 < rx1 || y1 > ry2 || y2 < ry1; |
|
}); |
|
|
|
if (minDistance > bestDistance) bestX = x, bestY = y, bestDistance = minDistance; |
|
} |
|
|
|
var best = [bestX, bestY, bestDistance - padding]; |
|
quadtree.add(best); |
|
return best; |
|
}; |
|
} |
|
|
|
tick() |
|
|
|
d3.select(self.frameElement).style("height", (height + margin.top + margin.bottom) + "px"); |
|
</script> |