Skip to content

Instantly share code, notes, and snippets.

@bwswedberg
Last active March 25, 2019 21:14
Show Gist options
  • Save bwswedberg/93cb6fea2dd3d653727e01b2db967dba to your computer and use it in GitHub Desktop.
Save bwswedberg/93cb6fea2dd3d653727e01b2db967dba to your computer and use it in GitHub Desktop.
Circular Fractal

Experimenting with fractals. This demonstrates how to create and animate a circular fractal. Each circle:

  1. never intersects a sibling,
  2. is completely contained by its parent, and
  3. will paint the entirety of its immediate parent every 360 degrees

In this example, the animation mutates data on-the-fly without rebinding because the data selection does not change in order or size. More complicated versions would be better off using d3.join(...).

<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg {
display: block;
width: 100%;
}
circle {
stroke: none;
}
.connector {
fill: none;
stroke: #000;
stroke-linecap: round;
}
</style>
<svg viewBox="0 0 960 500"></svg>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
var svg = d3.select('svg');
var viewBox = svg.attr('viewBox').split(' ');
var width = +viewBox[2];
var height = +viewBox[3];
var treeHeight = 6;
var tree = d3.hierarchy(createTree(treeHeight, 2, {
children: [],
childIndex: 0,
width: Math.min(+viewBox[2], +viewBox[3]),
x: 0,
y: 0
}));
var color = d3.scaleOrdinal()
.domain(d3.range(treeHeight + 1).reverse())
.range(d3.schemePuBuGn[treeHeight + 1]);
var strokeWidth = d3.scaleLinear()
.domain([0, treeHeight])
.range([0.1, 4]);
var g = svg.append('g')
.attr('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')')
var circle = g.selectAll('circle')
.data(tree.descendants()).enter()
.append('circle')
.attr('r', function(d) { return d.data.width / 2 })
.style('fill', function(d) { return color(d.height) });
var path = g.selectAll('path')
.data(tree.descendants()).enter()
.append('path')
.attr('class', 'connector')
.style('stroke-width', function(d) {
return strokeWidth(d.height) + 'px';
});
d3.timer(tick);
function createTree(height, childrenAmount, data) {
var node = data;
var childIndex = 0;
while (height > 0 && node.children.length < childrenAmount) {
node.children.push(createTree(height - 1, childrenAmount, {
children: [],
childIndex: childIndex++,
width: node.width / childrenAmount
}));
}
return node;
}
function tick(elapsed) {
var speed = 0.05;
var angle = (elapsed * speed) % 360;
var radians = angle * (2 * Math.PI / 360);
var sin = Math.sin(radians);
var cos = Math.cos(radians);
circle.attr('transform', function(d) {
if (!d.parent) return '';
var radius = d.data.width / 2;
var parentRadius = d.parent.data.width / 2;
var originX = d.parent.data.x;
var originY = d.parent.data.y;
// direction: clockwise vs counter-clockwise
var dir = (d.depth % 2) ? 1 : -1;
var x = originX - (parentRadius * cos * dir) + (d.data.width * d.data.childIndex * cos * dir) + (radius * cos * dir);
var y = originY - (parentRadius * sin) + (d.data.width * d.data.childIndex * sin) + (radius * sin);
// Mutate data during update to streamline animation.
// Animation is preformed data that was in descending
// tree order. Nodes are updated before their children.
// More complicated version will require a proper data
// joining (i.e. selection.join(...))
d.data.x = x;
d.data.y = y;
return 'translate(' + x + ',' + y + ')';
});
path.attr('d', function(d, i) {
if (!d.parent) return;
var start = [d.parent.data.x, d.parent.data.y];
var end = [d.data.x, d.data.y];
return 'M' + start.join(',') + ' L' + end.join(',');
});
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment