D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
mforando
Full window
Github gist
Radial Chart Force Based Label Placement
Built with
blockbuilder.org
<!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>
https://d3js.org/d3.v4.min.js