Built with blockbuilder.org
Created
October 23, 2019 17:22
-
-
Save mforando/5ad78563d9202095e02a7f31570f66d9 to your computer and use it in GitHub Desktop.
Radial Chart Force Based Label Placement
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: mit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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