Efficiently find points inside a circle sector - geometry

I have a set of 2d points distributed randomly. I need to perform a time intensive operation on a small subset of these points but I need to first figure out what points I need to perform this time intensive operation on. To determine what points I need they must pass a series of geometric criteria.
The most basic criteria is are they within a certain distance of a specific point. The second most basic criteria is whether they are contained within a circle sector (a 2-D cone) extending out from that specific point. (Edit: This operation is called regularly with a different specific point each time but the same set of 2d points.)
My initial thought was to create a grid containing the 2d points, then iterate along the cone grabbing grid squares that it intersects. Depending on the size of the grid it would filter out the vast majority of unneeded 2d points. Unfortunately the embedded system I'm running on is severely memory constrained so a large (by our standards not anyone elses) 2d array would be too memory intensive.
I have been trying to investigate using KD trees to speed up the calculation but I haven't been able to find an algorithm relating circle sectors and kd-trees.
Is there an efficient algorithm for finding what 2d points lie within a circle sector?
Just a note our particular system is slow at both floating point math and trigonometry so a solution that involves less of those is superior one that requires a lot of it.

It's possible to check if a point is inside a sector with only integer arithmetic and the basic operations of addition, subtraction and multiplication.
For a point to be inside a circular sector, it has to meet the following tests:
It has to be positioned counter-clockwise from the start "arm" of the sector.
It has to be positioned clockwise from the end arm of the sector.
It has to be closer to the center of the circle than the sector's radius.
Clockwise tests
To test if a vector v2 is clockwise to a another vector v1, do the following:
Find the counter-clockwise normal vector of v1. The normal vector is at a 90 degrees angle to the original vector. This is straightforward to do: if v1=(x1,y1), then the counter-clockwise normal is n1=(-y1,x1).
Find the size of the projection of v2 on the normal. This can be done by calculating the dot product of v2 and the normal.
projection = v2.x*n1.x + v2.y*n1.y
If the projection is a positive number, then the v2 is positioned counter-clockwise to v1. Otherwise, v2 is clockwise to v1.
Here's a counter-clockwise example:
And a clockwise example:
The steps can be combined:
function areClockwise(v1, v2) {
return -v1.x*v2.y + v1.y*v2.x > 0;
}
Radius test
The radius test is straightforward. Just check if the distance of the point from the center of the circle is less than the desired radius. To avoid computing square roots, we can compare the square of the distance with the square of the radius instead.
function isWithinRadius(v, radiusSquared) {
return v.x*v.x + v.y*v.y <= radiusSquared;
}
Putting it together
The complete sector test looks something like:
function isInsideSector(point, center, sectorStart, sectorEnd, radiusSquared) {
var relPoint = {
x: point.x - center.x,
y: point.y - center.y
};
return !areClockwise(sectorStart, relPoint) &&
areClockwise(sectorEnd, relPoint) &&
isWithinRadius(relPoint, radiusSquared);
}
The following sample page demonstrates this over several thousand points. You can experiment with the code at: http://jsbin.com/oriyes/8/edit.
Sample source code
<!DOCTYPE html>
<html>
<head>
<script src="http://code.jquery.com/jquery-1.8.2.min.js"></script>
<style>
.canvas {
position: absolute;
background: #f4f4f4;
border: 8px solid #f4f4f4;
width: 400px;
height: 400px;
}
.dot {
position: absolute;
font: 16px Arial;
}
.out { color: #ddd; }
.in { color: #00dd44; }
</style>
<script>
function isInsideSector(point, center, sectorStart, sectorEnd, radiusSquared) {
var relPoint = {
x: point.x - center.x,
y: point.y - center.y
};
return !areClockwise(sectorStart, relPoint) &&
areClockwise(sectorEnd, relPoint) &&
isWithinRadius(relPoint, radiusSquared);
}
function areClockwise(v1, v2) {
return -v1.x*v2.y + v1.y*v2.x > 0;
}
function isWithinRadius(v, radiusSquared) {
return v.x*v.x + v.y*v.y <= radiusSquared;
}
$(function() {
var $canvas = $("#canvas");
var canvasSize = 400;
var count = 4000;
// define the sector
var center = { x: canvasSize / 2, y: canvasSize / 2 };
var sectorStart = { x: 4, y: 1 };
var sectorEnd = { x: 1, y: 4 };
var radiusSquared = canvasSize * canvasSize / 4;
// create, draw and test a number of random points
for (var i = 0; i < count; ++i) {
// generate a random point
var point = {
x: Math.random() * canvasSize,
y: Math.random() * canvasSize
};
// test if the point is inside the sector
var isInside = isInsideSector(point, center, sectorStart, sectorEnd, radiusSquared);
// draw the point
var $point = $("<div class='dot'></div>")
.css({
left: point.x - 3,
top: canvasSize - point.y - 8 })
.html("β€’")
.addClass(isInside ? "in" : "out")
.appendTo($canvas);
}
});
</script>
</head>
<body>
<div id="canvas" class="canvas"></div>
</body>
</html>
Notes, caveats and limitations
You have to specify the boundaries of the sector in terms of vectors. The screenshot above, for example, shows a sector stretching between the vectors of (4,1) and (1,4).
If your sector is specified in other terms, e.g. angles, you will have to convert that to vectors first, e.g. using the tan() function. Fortunately, you only have to do this once.
The logic here works for sectors with an inner angle of less than 180 degrees. If your sectors can span a larger angle, you'll have to modify it.
Additionally, the code assumes that you know which of the bounding vectors of the sector is the "start" and which is the "end". If you don't, you can run the areClockwise() on them to find out.
Note that while all this can be done with integer arithmetic, both the radius and clockwise tests use a larger range of numbers, due to squaring x's and y's and multiplying them together. Make sure to use integers of sufficient bits to hold the results.

I know you don't want trigonometry, but you could convert each point (in your subset) to its polar coordinates (where the origin is your specific point) and threshold r,theta where r < R and T1 < theta < T2 corresponding to the sector. It's certainly memory efficient!

#Oren Trutner answer was great so I decided to make a visual example of it and make some improvements to make it work on all angles.
no further speaking, check the example below.
$(document).on('keypress',function (e) {
if(e.which === 13)
{
$("#calc").click();
}
});
function areClockwise(v1, v2) {
return -v1.x*v2.y + v1.y*v2.x > 0;
}
function vector(x = 0, y = 0) {
return {x:x,y:y}
}
function degToRad(degree) {
return degree * Math.PI / 180;
}
function isIn()
{
let illustration = $("#illustration");
illustration.html("");
let r = 250;
let fieldOfViewAngle = 150;
let x = Number($("#x").val());
let y = Number($("#y").val());
let startAngle = Number($("#startAngle").val());
let startSectorAngle = degToRad(startAngle);
let endSectorAngle = degToRad(startAngle+fieldOfViewAngle);
$("#startLine").attr("x2",250 + r*Math.cos(-startSectorAngle)).attr("y2",250 + r*Math.sin(-startSectorAngle));
$("#endLine").attr("x2",250 + r*Math.cos(-endSectorAngle)).attr("y2",250 + r*Math.sin(-endSectorAngle));
$("#point").attr("cx",250 +x).attr("cy",250 -y);
let sectorStartVector = vector(r * Math.cos(startSectorAngle),r * Math.sin(startSectorAngle));
let sectorEndVector = vector(r * Math.cos(endSectorAngle),r * Math.sin(endSectorAngle));
let relPoint = vector(x,y);
if(!this.areClockwise(sectorStartVector, relPoint) &&
this.areClockwise(sectorEndVector, relPoint))
$("#result").html("Result: in");
else{
$("#result").html("Result: out")
}
}
.flixy {
display: flex;
flex-direction: column;
}
.flixy > div {
margin-bottom: 20px;
width:300px
}
.flixy > div > input {
float: right;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="result"></div>
<div class="flixy">
<div class="input-group">
<label>X</label>
<input id="x">
</div>
<div class="input-group">
<label>Y</label>
<input id="y">
</div>
<div class="input-group">
<label>Start angle</label>
<input id="startAngle">
</div>
<div class="input-group">
<label>Radius</label>
<input value="250" disabled>
</div>
<div class="input-group">
<label>Theta</label>
<input value="150" disabled>
</div>
</div>
<button onclick="isIn()" id="calc">calc</button>
<div style="width: 500px;height: 500px; overflow: visible">
<svg width="500" height="500" style="overflow: visible">
<circle cx="250" cy="250" r="250" stroke="black" stroke-width="3" fill="yellow"></circle>
<line id="startLine" x1="250" y1="250" x2="500" y2="250" style="stroke:#2fa360;stroke-width:2" />
<line id="endLine" x1="250" y1="250" x2="500" y2="250" style="stroke:#1d68a7;stroke-width:2" />
<circle id="point" cx="250" cy="250" r="5" fill="red"></circle>
</svg>
</div>

Related

How do i calculate four colors linear grdient?

If I have four colours (A, B, C & D) on four points on a line and I want to fill with a gradient that blends nicely between the four colours how would I calculate the colour of the point E?
and A is starting point and D is ending point, before starting point and after ending point fill starting colour and end colour. inside line need to blend colour according to the distance and angle.
The closer E is to any of the other points, the strong that colour should affect the result.
I need like this one.
Any idea how to do that? Speed and simplicity is preferred to accuracy.
Well, in simple terms, take point ß that is halfway between A and B. Assuming the use of RGB colors, if A is red rgb(255, 0, 0) and B is yellow rgb(255, 255, 0), then ß's color will be halfway between these: rgb(255, 128, 0), that is, orange.
As you can see this can be calculated by using a weighted average per color channel - weighted by how close your point is to A and B.
Here's a code example you can run right here:
const slider = document.getElementById("range")
const between = document.getElementById("between")
slider.addEventListener("input", ev => {
const distFromA = ev.target.value
const G = distFromA / 3.92
// Not calculating R and B values, as these don't change in this specific example
const R = 255
const B = 0
between.style.background = `rgb(${R}, ${G}, ${B})`
})
#A { background: red; color: white; }
#B { background: yellow; }
#between { background: gainsboro; }
#between, #A, #B { display: inline-block; width: 50px; height: 50px; }
<aside id=A>A</aside>
<aside id=between>ß</aside>
<aside id=B>B</aside>
<nav><input type=range min=0 max=1000 id=range /></nav>
Do this for each pixel and you get a gradient πŸ‘

SVG animation of a thick dashed rounded shadowed line over an arbitrary background

I am trying to create an animation of a dashed line over an arbitrary (e. g. not known in advance, not necessarily constant) background - let's say, a photograph. I want the dashes to be thick and rounded at the ends. I also want them to be outlined with a different color, so they are visible regardless of the background.
My initial googling gave me this example. From what I understand, first it draws "anti-dashes", so to speak, of the background color, then animates a solid line being drawn under them. Obviously, that won't do what I want: no way to make the ends rounded, no way to outline each entire dash, and, most importantly, it requires a constant background.
I tried a different approach: drew the required curve, with all its thickness, roundness, shadows etc., then used an animated solid line as a visibility mask. Basically, it does what I want, except for possible self-intersections of the curve (see the "artifact" at the self-intersection point on the animation below).
Is it possible to fix that somehow? (just in case, the same code at codepen)
function init()
{
//some "magic numbers" to make a smooth curve
var to_draw = "M23.742,10.709 c-2.305,23.611-8.81,46.563-9.021,70.829c-0.252,28.966,22.237,43.666,47.06,55.482c23.642,11.255,42.368,15.766,68.461,16.631 c19.993,0.663,40.08,2.97,59.853-1.723c23.301-5.531,45.542-17.598,66.978-27.933c19.248-9.281,38.831-21.86,41.946-45.201 c5.539-41.51-54.993-47.073-81.885-42.17C159.05,47.212,89.37,104.633,77.387,164.629c-5.896,29.522-4.312,60.884,12.703,86.354 c19.17,28.697,49.512,49.927,78.596,67.591";
//set the same base line for all three paths
["line", "shadow", "hide"].forEach(
(element, index, array) =>
{
document.getElementById(element).setAttribute('d', to_draw);
});
//get the lenght of the resulting curve
var path = document.querySelector('.line');
var length = path.getTotalLength();
//set the dash length to cover the entire curve
//
//set the offset so that our long dash is initially off the curve,
//then it slides along the curve when the CSS animation plays
var elem = document.querySelector(".hide");
elem.style.strokeDasharray = length;
elem.style.strokeDashoffset = length;
}
.base
{
stroke-linejoin: round;
stroke-linecap: round;
stroke-dasharray: 10, 20;
fill: none;
}
.line
{
stroke: rgba(0, 255, 0, 0.7);
stroke-width: 5;
}
.shadow
{
stroke: rgba(0, 0, 0, 0.7);
stroke-width: 7;
}
.hide
{
stroke: white;
stroke-width: 8;
stroke-linejoin: round;
stroke-linecap: round;
stroke-dasharray: 0;
stroke-dashoffset: 0;
animation: dash 3s linear alternate infinite;
fill: none;
}
#keyframes dash
{
to
{
stroke-dashoffset: 0;
}
}
<!DOCTYPE html>
<html>
<head></head>
<body onload = "init()">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="612px" height="792px" viewBox="0 0 612 792" enable-background="new 0 0 612 792" xml:space="preserve">
<circle cx="230" cy="150" r="40" stroke="black" stroke-width="3" fill="red" />
<mask id = "theMask">
<path id = "hide" class = "hide" />
</mask>
<path id = "shadow" class = "base shadow" mask = "url(#theMask)" />
<path id = "line" class = "base line" mask = "url(#theMask)" />
</svg>
</body>
</html>
To deal with crossings, you will need to break the line up into two or more sections and animate them sequentially.
So in your case, you would need two lines. One from the start of the path, that goes past the intersection. Then a second one that starts where the first one ends and crosses back over the first one.
Your case is complicated a little further by the fact that you have dashed lines. If you are not careful, you will be able to see where the second line begins, because the dash pattern will restart abruptly. To deal with that you will either have to:
Split the line at a length that coincides with a point where the dash pattern repeats, or
Give the second line a dash offset that matches the point in the dash pattern that the first line was when it ended.

How to determine width of text in OpenSCAD?

In OpenSCAD, I want to be able to create a module which accepts a string then create a 3-D object with that string embedded in the surface as a text. I want the object to be slightly larger than the text, so I need to know how wide the text is in order to create a properly-sized object.
I'm not sure how to query the width of the text (the height is set by an input variable), or if that's even possible.
If it's not possible, is there a function that will accept a string and a font and predict the width of the rendered text?
There is currently no way to query the actual size of the generated text geometry. However, depending on the model that shall be created, it might be enough to calculate a rough estimation and use scale() to fit the text into the known size.
// Fit text into a randomly generated area
r = rands(10, 20, 2);
length = 3 * r[0];
w = r[1];
difference() {
cube([length, w, 1]);
color("white")
translate([0, w / 2, 0.6])
linear_extrude(1, convexity = 4)
resize([length, 0], auto = true)
text("This is a Test", valign = "center");
}
If you use one of the Liberation fonts bundled with OpenSCAD or the fonts in the Microsoft Core Fonts pack, you can use my font measurement OpenSCAD library. E.g.:
use <fontmetrics.scad>;
length = measureText("This is a Test", font="Arial:style=Italic", size=20.);
The library is here. I used some Python scripts to extract metrics (including kerning pairs) from the ttf file, and you can use the scripts to add information about more fonts.
I have found a way to determine the widths of text characters in OpenSCAD. I made a JavaScript thing that lets you input a font name and style, and it outputs an array of width proportions for ascii and extended ascii characters (codes 0-255). Then for any given character, you multiply this proportion by the font size to get the width of an individual character. From there it's trivial to get the width of a string, or the angular widths of characters wrapped around a cylinder.
The tool to generate the OpenSCAD width array is here: https://codepen.io/amatulic/pen/eYeBLva
...and the code is reproduced below, which you can run from this reply, or paste into your own HTML file and load into your browser locally.
Just input the font properties, click the button, and scroll down to see usage instructions.
The secret sauce lies in the fact that JavaScript's 'canvas' support has a 'measureText()' method that measures the pixel length of any text for a given font, used like this:
canvasContext.measureText(string).width
So what this code does is use a dummy canvas on the page to get a context to which a font is assigned, arbitrarily 20 pixels in size. Then it generates an array of widths for every character from 0 to 255, dividing each by 20 to get a unitless width proportion compared to font size. It then outputs a line of OpenSCAD code that you can paste into your OpenSCAD script. Then you use OpenSCAD's ord() function to convert any character to a numeric code, which then serves as an index of the width array. You then multiply this width by the font size to get the character width.
<html>
<!--
by Alex Matulich, February 2022
Thingiverse: https://www.thingiverse.com/amatulic/designs
Website: https://www.nablu.com
-->
<head>
<script type="text/javascript">
var sctx;
function initialize() {
var canvas = document.getElementById("canvas");
sctx = canvas.getContext("2d");
}
function charwidth(fontname, style) {
sctx.font = (style + " 20px " + fontname).trim();
var charlen = [];
for (i = 0; i < 256; ++i) //{ charlen[i] = 10; console.log(i); }
charlen[i] = sctx.measureText(String.fromCharCode(i)).width / 20;
return charlen;
}
function generate() {
var fontname = document.getElementById("fontname").value;
var fontstyle = document.getElementById("fontstyle").value;
var widths = charwidth(fontname, fontstyle);
var arrayname = toCamelCase(fontname) + toCamelCase(fontstyle);
var outputhtml = arrayname + " = [<br/>\n" + widths[0].toString();
var len = widths.length;
for (i = 1; i < len; ++i) outputhtml += ', ' + widths[i].toString();
outputhtml += "<br/>\n];\n";
document.getElementById("output").innerHTML = outputhtml;
document.getElementById('usage').innerHTML = "<h3>Usage</h3>\n<p>The array above shows character width as a multiple of font size. To get the width of a character <code><char></code> given font size <code><fontsize></code> using the font \"" + fontname + " " + fontstyle + "\":</p>\n<p><code> charwidth = " + arrayname + "[ord(char)] * fontsize;<code></p>\n";
document.getElementById('sample').innerHTML = "<h3>Font sample</h3>\n<p style=\"font: " + fontstyle + " 20px " + fontname + ";\">" + fontname + " " + fontstyle + ": 0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz</p>\n";
}
// convert the input array to camel case
function toCamelCase(stringinput) {
if (stringinput.length == 0) return '';
var inputArray = stringinput.match(/[A-Z\xC0-\xD6\xD8-\xDE]?[a-z\xDF-\xF6\xF8-\xFF]+|[A-Z\xC0-\xD6\xD8-\xDE]+(?![a-z\xDF-\xF6\xF8-\xFF])|\d+/g);
result = "";
for (let i = 0, len = inputArray.length; i < len; i++) {
let currentStr = inputArray[i];
let tempStr = currentStr.toLowerCase();
// convert first letter to upper case (the word is in lowercase)
tempStr = tempStr.substr(0, 1).toUpperCase() + tempStr.substr(1);
result += tempStr;
}
return result;
}
</script>
</head>
<body onload="initialize()">
<h1>OpenSCAD proportional font widths</h1>
<form>
<fieldset>
<legend>Identify the font</legend>
<input type="text" id="fontname" name="fontname" value="Liberation Sans">
<label for="fontname">Font name</label><br />
<input type="text" id="fontstyle" name="fontstyle" value="bold">
<label for="fontstyle">Font style (bold, italic, etc. or leave blank)<br />
</fieldset>
<input type="button" onclick="generate()" value="Generate OpenSCAD font width proportions">
</form>
<h2>Copy and paste this code into OpenSCAD</h2>
<div id="output" style="border:5px ridge silver; padding:1em; font-family:monospace;">
</div>
<div id="usage">
</div>
<div id="sample">
</div>
<canvas id="canvas"></canvas>
</body>
</html>

How to calculate flex shrink when flex items have a different flex basis

Let's say I have a simple flex container with 2 flex items where the flex items' sizes exceed the containers size - where flex-shrink will be used..like so:
.container {
width: 600px;
outline: 5px solid;
display: flex;
}
section {
flex: 0 1 600px;
background: salmon;
}
aside {
flex: 0 1 200px;
background: aqua;
}
<div class="container">
<section>
<h2>Main content here</h2>
</section>
<aside>
<h2>Some side content</h2>
</aside>
</div>
Codepen demo
In the above example: The container is 600px, the section flex-item has flex basis of 600px and the aside has flex-basis of 200px - so the negative space is 200px.
So being that both flex items have the same flex-shrink factor of 1 - I expected that both flex items would shrink by 100px with the section getting a width of 600px - 100px = 500px and the aside getting 200px - 100px = 100px
But the result was actually that the section shrinks by 150px to 450px and the aside shrinks by 50px to 150px
So then I looked at the spec and I found this:
Note: The flex shrink factor is multiplied by the flex base size when
distributing negative space. This distributes negative space in
proportion to how much the item is able to shrink, so that e.g. a
small item won’t shrink to zero before a larger item has been
noticeably reduced.
So now I understand that when calculating flex-shrink on a flex item not only is the flex shrink factor taken into account, but also the flex base size (here, defined by the flex-basis property)
The problem is that I can't seem to do the math to calculate flex-shrink.
So just to continue with the above example: say I change the shrink factor of the section to 2...
.container {
width: 600px;
outline: 5px solid;
display: flex;
}
section {
flex: 0 2 600px;
background: salmon;
}
aside {
flex: 0 1 200px;
background: aqua;
}
<div class="container">
<section>
<h2>Main content here</h2>
</section>
<aside>
<h2>Some side content</h2>
</aside>
</div>
Codepen demo #2
... the result is that section gets a width of 428px and aside gets a width of 121px
Can someone explain how to calculate this?
Neglecting lots of details, the algorithm is something like this
let sumScaledShrinkFactors = 0,
remainingFreeSpace = flexContainer.innerMainSize;
for (let item of flexItems) {
remainingFreeSpace -= item.outerFlexBasis;
item.scaledShrinkFactor = item.innerFlexBasis * item.flexShrinkFactor;
sumScaledShrinkFactors += item.scaledShrinkFactor;
}
for (let item of flexItems) {
let ratio = item.scaledShrinkFactor / sumScaledShrinkFactors;
item.innerWidth = item.innerFlexBasis + ratio * remainingFreeSpace;
}
So the formula is like
flexBasis * (1 + shrinkFactor / sumScaledShrinkFactors * remainingFreeSpace)
First case
1*600px + 1*200px ─┐ width
β”‚ ───────
600px * (1 + 1 / 800px * -200px) = 450px
200px * (1 + 1 / 800px * -200px) = 150px
β”‚ ───────
600px - (600px + 200px) β”€β”€β”€β”€β”˜ 600px
Second case
2*600px + 1*200px ──┐ width
β”‚ ───────
600px * (1 + 2 / 1400px * -200px) β‰ˆ 429px
200px * (1 + 1 / 1400px * -200px) β‰ˆ 171px
β”‚ ───────
600px - (600px + 200px) β”€β”€β”€β”€β”€β”˜ 600px

How to avoid the overlapping of text elements on the TreeMap when child elements are opened in D3.js?

I created a Tree in D3.js based on Mike Bostock's Node-link Tree. The problem I have and that I also see in Mike's Tree is that the text label overlap/underlap the circle nodes when there isn't enough space rather than extend the links to leave some space.
As a new user I'm not allowed to upload images, so here is a link to Mike's Tree where you can see the labels of the preceding nodes overlapping the following nodes.
I tried various things to fix the problem by detecting the pixel length of the text with:
d3.select('.nodeText').node().getComputedTextLength();
However this only works after I rendered the page when I need the length of the longest text item before I render.
Getting the longest text item before I render with:
nodes = tree.nodes(root).reverse();
var longest = nodes.reduce(function (a, b) {
return a.label.length > b.label.length ? a : b;
});
node = vis.selectAll('g.node').data(nodes, function(d, i){
return d.id || (d.id = ++i);
});
nodes.forEach(function(d) {
d.y = (longest.label.length + 200);
});
only returns the string length, while using
d.y = (d.depth * 200);
makes every link a static length and doesn't resize as beautiful when new nodes get opened or closed.
Is there a way to avoid this overlapping? If so, what would be the best way to do this and to keep the dynamic structure of the tree?
There are 3 possible solutions that I can come up with but aren't that straightforward:
Detecting label length and using an ellipsis where it overruns child nodes. (which would make the labels less readable)
scaling the layout dynamically by detecting the label length and telling the links to adjust accordingly. (which would be best but seems really difficult
scale the svg element and use a scroll bar when the labels start to run over. (not sure this is possible as I have been working on the assumption that the SVG needs to have a set height and width).
So the following approach can give different levels of the layout different "heights". You have to take care that with a radial layout you risk not having enough spread for small circles to fan your text without overlaps, but let's ignore that for now.
The key is to realize that the tree layout simply maps things to an arbitrary space of width and height and that the diagonal projection maps width (x) to angle and height (y) to radius. Moreover the radius is a simple function of the depth of the tree.
So here is a way to reassign the depths based on the text lengths:
First of all, I use the following (jQuery) to compute maximum text sizes for:
var computeMaxTextSize = function(data, fontSize, fontName){
var maxH = 0, maxW = 0;
var div = document.createElement('div');
document.body.appendChild(div);
$(div).css({
position: 'absolute',
left: -1000,
top: -1000,
display: 'none',
margin:0,
padding:0
});
$(div).css("font", fontSize + 'px '+fontName);
data.forEach(function(d) {
$(div).html(d);
maxH = Math.max(maxH, $(div).outerHeight());
maxW = Math.max(maxW, $(div).outerWidth());
});
$(div).remove();
return {maxH: maxH, maxW: maxW};
}
Now I will recursively build an array with an array of strings per level:
var allStrings = [[]];
var childStrings = function(level, n) {
var a = allStrings[level];
a.push(n.name);
if(n.children && n.children.length > 0) {
if(!allStrings[level+1]) {
allStrings[level+1] = [];
}
n.children.forEach(function(d) {
childStrings(level + 1, d);
});
}
};
childStrings(0, root);
And then compute the maximum text length per level.
var maxLevelSizes = [];
allStrings.forEach(function(d, i) {
maxLevelSizes.push(computeMaxTextSize(allStrings[i], '10', 'sans-serif'));
});
Then I compute the total text width for all the levels (adding spacing for the little circle icons and some padding to make it look nice). This will be the radius of the final layout. Note that I will use this same padding amount again later on.
var padding = 25; // Width of the blue circle plus some spacing
var totalRadius = d3.sum(maxLevelSizes, function(d) { return d.maxW + padding});
var diameter = totalRadius * 2; // was 960;
var tree = d3.layout.tree()
.size([360, totalRadius])
.separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
Now we can call the layout as usual. There is one last piece: to figure out the radius for the different levels we will need a cumulative sum of the radii of the previous levels. Once we have that we simply assign the new radii to the computed nodes.
// Compute cummulative sums - these will be the ring radii
var newDepths = maxLevelSizes.reduce(function(prev, curr, index) {
prev.push(prev[index] + curr.maxW + padding);
return prev;
},[0]);
var nodes = tree.nodes(root);
// Assign new radius based on depth
nodes.forEach(function(d) {
d.y = newDepths[d.depth];
});
Eh voila! This is maybe not the cleanest solution, and perhaps does not address every concern, but it should get you started. Have fun!

Resources