Skip to content

Instantly share code, notes, and snippets.

@mforando
Created October 23, 2019 17:22
Show Gist options
  • Save mforando/5ad78563d9202095e02a7f31570f66d9 to your computer and use it in GitHub Desktop.
Save mforando/5ad78563d9202095e02a7f31570f66d9 to your computer and use it in GitHub Desktop.
Radial Chart Force Based Label Placement
license: mit
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
</style>
</head>
<div class="slidecontainer">
<label>Label Circle Size</label><input type="range" min="1" max="250" value="200" class="slider" id="labelcirclesize">
</div>
<div class="slidecontainer">
<label>Center Circle Size</label><input type="range" min="1" max="250" value="125" class="slider" id="centersize">
</div>
<div class="slidecontainer">
<label>Number of Data Points</label><input type="range" min="1" max="50" value="10" class="slider" id="numpoints">
</div>
<div class="slidecontainer">
<button type="button" id="newpoints">Generate New Points</button>
</div>
<svg>
<g>
<circle id="centercircle"></circle>
<circle id="labelcircle"></circle>
</g>
</svg>
<body>
<script>
var width = 500;
var height = 500;
function generatePoints(count){
return d3.range(count).map(function(d,i){
var angle = Math.random()*Math.PI*2;
return {"angle":angle, "id":i}
})
}
var simulation = d3.forceSimulation()
var points = generatePoints(10);
updateRadialChart(width, width/4, width/3, points);
d3.selectAll("#newpoints")
.on("click", function(){
var val = d3.select('#centersize').property("value");
var lblsize = d3.select("#labelcirclesize").property("value");
var numpoints = d3.select("#numpoints").property("value");
points = generatePoints(numpoints);
updateRadialChart(width, val, lblsize, points);
})
d3.selectAll("#numpoints")
.on("change", function(){
var val = d3.select('#centersize').property("value");
var lblsize = d3.select("#labelcirclesize").property("value");
var numpoints = d3.select("#numpoints").property("value");
points = generatePoints(numpoints);
updateRadialChart(width, val, lblsize, points);
})
d3.selectAll("#centersize, #labelcirclesize")
.on("change", function(){
var val = d3.select('#centersize').property("value");
var lblsize = d3.select("#labelcirclesize").property("value");
updateRadialChart(width, val, lblsize, points);
})
function updateRadialChart(width, innercirclewidth, outercirclewidth, randAngles){
simulation.stop();
var svg = d3.select("svg")
.attr("width", width)
.attr("height", width)
.style("overflow","visible")
var chart = svg.select("g")
.attr("transform","translate(" + width/2 + "," + width/2 + ")")
var radiusInner = innercirclewidth;
var radiusOuter = outercirclewidth;
var mainCircle = chart.select("#centercircle")
.attr("r",radiusInner)
.attr("fill","white")
.attr("stroke","black")
var labelCircle = chart.select("#labelcircle")
.attr("r",radiusOuter)
.attr("fill","none")
.attr("stroke","gray")
.style("stroke-dasharray",4)
var scaleBand = d3.scaleBand()
.domain(randAngles
.sort(function(a,b){return d3.ascending(a.angle,b.angle)})
.map(function(d,i){return i}))
.range([-width/2 + 20,width/2 - 20])
var circles = chart.selectAll(".randcircles")
.data(randAngles)
circles.enter()
.append("circle")
.attr("class","randcircles")
.merge(circles)
.attr("r",3)
.attr("cx",function(d){return Math.cos(d.angle)*radiusInner})
.attr("cy",function(d){return Math.sin(d.angle)*radiusInner})
circles.exit().remove();
var lbls = chart
.selectAll(".labels")
.data(randAngles)
lbls.enter()
.append("text")
.attr("class","labels")
.merge(lbls)
.style("dominant-baseline","middle")
.text(function(d){return d3.format('0.2f')(d.angle)})
.attr("x",function(d){
var xVal = 200;
if(d.angle < Math.PI*1.5 && d.angle > Math.PI*0.5 ){
xVal = -200;
}
return xVal;
})
.style("text-anchor",function(d){
var anchor = "start";
if(d.angle < Math.PI*1.5 && d.angle > Math.PI*0.5 ){
anchor = "end";
}
return anchor;
})
.attr("y",function(d,i){
return Math.sin(d.angle) * radiusOuter;
})
lbls.exit().remove();
var lbldata = [];
d3.selectAll(".labels")
.each(function(d){
var xVal = 200;
if(d.angle < Math.PI*1.5 && d.angle > Math.PI*0.5 ){
xVal = -200;
}
lbldata.push({"data":d,
"bbox": d3.select(this).node().getBBox(),
"fx": xVal,
"X":xVal,
"y": Math.sin(d.angle) * radiusOuter
});
})
function ticked(){
var lbls = d3.selectAll(".labels")
.data(lbldata)
lbls.enter()
.append("text")
.attr("class","labels")
.merge(lbls)
.attr("y",function(d,i){return d.y - width/2;})
var polyLineData = lbldata.map(function(d,i){
//adjust the second point to use the Y position of the force label, but the angle of the original point.
return [[Math.cos(d.data.angle)*radiusInner,Math.sin(d.data.angle)*radiusInner],
[Math.cos(d.data.angle)*radiusOuter,d.y - width/2],
[d.x,d.y - width/2]]
})
var paths = chart.selectAll("polyline")
.data(polyLineData)
paths.enter()
.append("polyline")
.merge(paths)
.attr("points",function(d){return d})
.attr("stroke","black")
.attr("fill","none")
paths.exit().remove();
};
// https://bl.ocks.org/cmgiven/547658968d365bcc324f3e62e175709b
var collisionForce = rectCollide()
.size(function (d) { return [d.bbox.width, d.bbox.height] })
var boxForce = boundedBox()
.bounds([[-10, -10], [width + 10, height + 10]])
.size(function (d) { return [d.bbox.width, d.bbox.height] })
simulation
.on('tick', ticked)
.force('box', boxForce)
.force('collision', collisionForce)
.force("y",d3.forceY().y(function(d){return Math.max(0,Math.sin(d.data.angle) * radiusOuter + width/2);}).strength(.5))
.nodes(lbldata);
simulation.alpha(1).restart();
function rectCollide() {
var nodes, sizes, masses
var size = [0, 0]
var strength = .05
var iterations = 1
function force() {
var node, size, mass, xi, yi
var i = -1
while (++i < iterations) { iterate() }
function iterate() {
var j = -1
var tree = d3.quadtree(nodes, xCenter, yCenter).visitAfter(prepare)
while (++j < nodes.length) {
node = nodes[j]
size = sizes[j]
mass = masses[j]
xi = xCenter(node)
yi = yCenter(node)
tree.visit(apply)
}
}
function apply(quad, x0, y0, x1, y1) {
var data = quad.data
var xSize = (size[0] + quad.size[0]) / 2
var ySize = (size[1] + quad.size[1]) / 2
if (data) {
if (data.index <= node.index) { return }
var x = xi - xCenter(data)
var y = yi - yCenter(data)
var xd = Math.abs(x) - xSize
var yd = Math.abs(y) - ySize
if (xd < 0 && yd < 0) {
var l = Math.sqrt(x * x + y * y)
var m = masses[data.index] / (mass + masses[data.index])
if (Math.abs(xd) < Math.abs(yd)) {
node.vx -= (x *= xd / l * strength) * m
data.vx += x * (1 - m)
} else {
node.vy -= (y *= yd / l * strength) * m
data.vy += y * (1 - m)
}
}
}
return x0 > xi + xSize || y0 > yi + ySize ||
x1 < xi - xSize || y1 < yi - ySize
}
function prepare(quad) {
if (quad.data) {
quad.size = sizes[quad.data.index]
} else {
quad.size = [0, 0]
var i = -1
while (++i < 4) {
if (quad[i] && quad[i].size) {
quad.size[0] = Math.max(quad.size[0], quad[i].size[0])
quad.size[1] = Math.max(quad.size[1], quad[i].size[1])
}
}
}
}
}
function xCenter(d) { return d.x + d.vx + sizes[d.index][0] / 2 }
function yCenter(d) { return d.y + d.vy + sizes[d.index][1] / 2 }
force.initialize = function (_) {
sizes = (nodes = _).map(size)
masses = sizes.map(function (d) { return d[0] * d[1] })
}
force.size = function (_) {
return (arguments.length
? (size = typeof _ === 'function' ? _ : constant(_), force)
: size)
}
force.strength = function (_) {
return (arguments.length ? (strength = +_, force) : strength)
}
force.iterations = function (_) {
return (arguments.length ? (iterations = +_, force) : iterations)
}
return force
}
function boundedBox() {
var nodes, sizes
var bounds
var size = ([0, 0])
function force() {
var node, size
var xi, x0, x1, yi, y0, y1
var i = -1
while (++i < nodes.length) {
node = nodes[i]
size = sizes[i]
xi = node.x + node.vx
x0 = bounds[0][0] - xi
x1 = bounds[1][0] - (xi + size[0])
yi = node.y + node.vy
y0 = bounds[0][1] - yi
y1 = bounds[1][1] - (yi + size[1])
if (x0 > 0 || x1 < 0) {
node.x += node.vx
node.vx = -node.vx
if (node.vx < x0) { node.x += x0 - node.vx }
if (node.vx > x1) { node.x += x1 - node.vx }
}
if (y0 > 0 || y1 < 0) {
node.y += node.vy
node.vy = -node.vy
if (node.vy < y0) { node.vy += y0 - node.vy }
if (node.vy > y1) { node.vy += y1 - node.vy }
}
}
}
force.initialize = function (_) {
sizes = (nodes = _).map(size)
}
force.bounds = function (_) {
return (arguments.length ? (bounds = _, force) : bounds)
}
force.size = function (_) {
return (arguments.length
? (size = typeof _ === 'function' ? _ : constant(_), force)
: size)
}
return force
}
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment