Skip to content

Instantly share code, notes, and snippets.

@mtaptich
Last active January 8, 2016 17:30
Show Gist options
  • Save mtaptich/9491bd5a7a0b2501227c to your computer and use it in GitHub Desktop.
Save mtaptich/9491bd5a7a0b2501227c to your computer and use it in GitHub Desktop.
Logistic Growth

A simple visualization of the logistic model using d3.geom.quadtree and Mitchell’s Best-Candidate algorithm as its backbone. The graph depicts the the ratio of total circle area to total graph area. Think of each additional circle as the marginal growth per unit increase along the x-axis.

The logistic model characterizes many activities found in human societies and the environment. For instance, logistic growth in populations occurs where competition for natural resources is high. As resources are depleted, growth is moderated and eventually plateaus to the particular environment's natural carrying capacity.

<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment