|
<!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> |