How is the getBBox() SVGRect calculated? - svg

I have a g element that contains one or more path elements. As I mentioned in another question, I scale and translate the g element by computing a transform attribute so that it fits on a grid in another part of the canvas.
The calculation is done using the difference between two rectangles, the getBBox() from the g element and the rectangle around the grid.
Here is the question -- after I do the transform, I update the contents of the g element and call getBBox() again, without removing the transform. The resulting rectangle appears to be calculated without considering the transform. I would have expected it to reflect the change. Is this behavior consistent with the SVG specification? How do I get the bounding box of the transformed rectangle?
This, BTW, is in an HTML 5 document running in Firefox 4, if that makes any difference.
Update: Apparently this behavior seems pretty clearly in violation of the specification. From the text here at w3c:
SVGRect getBBox()
Returns the tight bounding box in current user space (i.e., after application of the ‘transform’ attribute, if any) on the geometry of all contained graphics elements, exclusive of stroking, clipping, masking and filter effects). Note that getBBox must return the actual bounding box at the time the method was called, even in case the element has not yet been rendered.
Am I reading this correctly? If so this seems to be an errata in the SVG implementation Firefox uses; I haven't had a chance to try any other. I would file a bug report if someone could point me to where.

People often get confused by the behavioral difference of getBBox and getBoundingClientRect.
getBBox is a SVG Element's native method as equivalent to find the offset/clientwidth of HTML DOM element. The width and height is never going to change even when the element is rotated. It cannot be used for HTML DOM Elements.
getBoundingClientRect is common to both HTML and SVG elements. The bounded rectangle width and height will change when the element is rotated or when more elements are grouped.

The behaviour you see is correct, and consistent with the spec.
The transform gets applied, then the bbox is calculated in "current user units", i.e. the current user space. So if you want to see the result of a transform on the element you'd need to look at the bbox of a parent node or similar.
It's a bit confusing, but explained a lot better in the SVG Tiny 1.2 spec for SVGLocatable
That contains a number of examples that clarify what it's supposed to do.

there are at least 2 easy but somewhat hacky ways to do what you ask... if there are nicer (less hacky) ways, i haven't found them yet
EASY HACKy #1:
a) set up a rect that matches the "untransformed" bbox that group.getBBox() is returning
b) apply the group's "unapplied transform" to that rect
c) rect.getBBox() should now return the bbox you're looking for
EASY HACKY #2: (only tested in chrome)
a) use element.getBoundingClientRect(), which returns enough info for you to construct the bbox you're looking for

Apparently getBBox() doesn't take the transformations into consideration.
I can point you here, unfortunately I wasn't able to make it working: http://tech.groups.yahoo.com/group/svg-developers/message/22891

SVG groups have nasty practice - not to accumulate all transformations made. I have my way to cope with this issue. I'm using my own attributes to store current transformation data which I include in any further transformation. Use XML compatible attributes like alttext, value, name....or just x and y for storing accumulated value as atribute.
Example:
<g id="group" x="20" y="100" transform="translate(20, 100)">
<g id="subgroup" alttext="45" transform="rotate(45)">
<line...etc...
Therefore when I'm making transformations I'm taking those handmade attribute values, and when writing it back, I'm writing both transform and same value with attributes I made just for keeping all accumulated values.
Example for rotation:
function symbRot(evt) {
evt.target.ondblclick = function () {
stopBlur();
var ptx=symbG.parentNode.lastChild.getAttribute("cx");
var pty=symbG.parentNode.lastChild.getAttribute("cy");
var currRot=symbG.getAttributeNS(null, "alttext");
var rotAng;
if (currRot == 0) {
rotAng = 90
} else if (currRot == 90) {
rotAng = 180
} else if (currRot == 180) {
rotAng = 270
} else if (currRot == 270) {
rotAng = 0
};
symbG.setAttributeNS(null, "transform", "rotate(" + rotAng + "," + ptx + ", " + pty + ")");
symbG.setAttributeNS(null, "alttext", rotAng );
};
}

The following code takes into account the transformations (matrix or otherwise) from parents, itself, as well as children. So, it will work on a <g> element for example.
You will normally want to pass the parent <svg> as the third argument—toElement—as to return the computed bounding box in the coordinate space of the <svg> (which is generally the coordinate space we care about).
/**
* #param {SVGElement} element - Element to get the bounding box for
* #param {boolean} [withoutTransforms=false] - If true, transforms will not be calculated
* #param {SVGElement} [toElement] - Element to calculate bounding box relative to
* #returns {SVGRect} Coordinates and dimensions of the real bounding box
*/
function getBBox(element, withoutTransforms, toElement) {
var svg = element.ownerSVGElement;
if (!svg) {
return { x: 0, y: 0, cx: 0, cy: 0, width: 0, height: 0 };
}
var r = element.getBBox();
if (withoutTransforms) {
return {
x: r.x,
y: r.y,
width: r.width,
height: r.height,
cx: r.x + r.width / 2,
cy: r.y + r.height / 2
};
}
var p = svg.createSVGPoint();
var matrix = (toElement || svg).getScreenCTM().inverse().multiply(element.getScreenCTM());
p.x = r.x;
p.y = r.y;
var a = p.matrixTransform(matrix);
p.x = r.x + r.width;
p.y = r.y;
var b = p.matrixTransform(matrix);
p.x = r.x + r.width;
p.y = r.y + r.height;
var c = p.matrixTransform(matrix);
p.x = r.x;
p.y = r.y + r.height;
var d = p.matrixTransform(matrix);
var minX = Math.min(a.x, b.x, c.x, d.x);
var maxX = Math.max(a.x, b.x, c.x, d.x);
var minY = Math.min(a.y, b.y, c.y, d.y);
var maxY = Math.max(a.y, b.y, c.y, d.y);
var width = maxX - minX;
var height = maxY - minY;
return {
x: minX,
y: minY,
width: width,
height: height,
cx: minX + width / 2,
cy: minY + height / 2
};
}

I made a helper function, which returns various metrics of svg element (also bbox of transformed element).
The code is here:
SVGElement.prototype.getTransformToElement =
SVGElement.prototype.getTransformToElement || function(elem) {
return elem.getScreenCTM().inverse().multiply(this.getScreenCTM());
};
function get_metrics(el) {
function pointToLineDist(A, B, P) {
var nL = Math.sqrt((B.x - A.x) * (B.x - A.x) + (B.y - A.y) * (B.y - A.y));
return Math.abs((P.x - A.x) * (B.y - A.y) - (P.y - A.y) * (B.x - A.x)) / nL;
}
function dist(point1, point2) {
var xs = 0,
ys = 0;
xs = point2.x - point1.x;
xs = xs * xs;
ys = point2.y - point1.y;
ys = ys * ys;
return Math.sqrt(xs + ys);
}
var b = el.getBBox(),
objDOM = el,
svgDOM = objDOM.ownerSVGElement;
// Get the local to global matrix
var matrix = svgDOM.getTransformToElement(objDOM).inverse(),
oldp = [[b.x, b.y], [b.x + b.width, b.y], [b.x + b.width, b.y + b.height], [b.x, b.y + b.height]],
pt, newp = [],
obj = {},
i, pos = Number.POSITIVE_INFINITY,
neg = Number.NEGATIVE_INFINITY,
minX = pos,
minY = pos,
maxX = neg,
maxY = neg;
for (i = 0; i < 4; i++) {
pt = svgDOM.createSVGPoint();
pt.x = oldp[i][0];
pt.y = oldp[i][1];
newp[i] = pt.matrixTransform(matrix);
if (newp[i].x < minX) minX = newp[i].x;
if (newp[i].y < minY) minY = newp[i].y;
if (newp[i].x > maxX) maxX = newp[i].x;
if (newp[i].y > maxY) maxY = newp[i].y;
}
// The next refers to the transformed object itself, not bbox
// newp[0] - newp[3] are the transformed object's corner
// points in clockwise order starting from top left corner
obj.newp = newp; // array of corner points
obj.width = pointToLineDist(newp[1], newp[2], newp[0]) || 0;
obj.height = pointToLineDist(newp[2], newp[3], newp[0]) || 0;
obj.toplen = dist(newp[0], newp[1]);
obj.rightlen = dist(newp[1], newp[2]);
obj.bottomlen = dist(newp[2], newp[3]);
obj.leftlen = dist(newp[3], newp[0]);
// The next refers to the transformed object's bounding box
obj.BBx = minX;
obj.BBy = minY;
obj.BBx2 = maxX;
obj.BBy2 = maxY;
obj.BBwidth = maxX - minX;
obj.BBheight = maxY - minY;
return obj;
}
and full functional example is here:
http://jsbin.com/acowaq/1

Related

SVG or Canvas bevel & emboss to find center-line of text

The photoshop bevel & emboss effect makes it easy to find the central 'ridge' which coincides with the center-line of text characters. This is done by increasing the appropriate effect settings to max-out the bevel, thereby creating such a ridge.
This Photoshop example was processed to further accentuate the center ridge
Is it possible to achieve the same effect with an SVG filter or Canvas technique in the browser?
Once this effect is in place, I could obtain the coordinates of the center line which I want.
Alternatively, is there an existing algorithm to get this center line via mathematical means from a raster image or vector shape?
SVG filters are a powerful feature that can be like photoshop in the browser. You can achieve the desired result by chaining a handful of filter primitives together.
<filter id="filterData">
<feGaussianBlur stdDeviation="5" />
<feDiffuseLighting surfaceScale="500">
<feDistantLight azimuth="90" elevation="90" />
</feDiffuseLighting>
<feComposite result="composite" operator="in" in2="SourceGraphic" />
</filter>
The first primitive blurs the text. Then a lighting primitive uses the result of the blurred primitive as a bump map to give the text depth. You will have to play with the surfaceScale attribute depending to the thickness of the text. The composite primitive will cut the final result to the area of the unfiltered text, the 'SourceGraphic'.
[codepen example] https://codepen.io/lahaymd/pen/EdNXam
Somehow this one tickled my fancy, although I am not sure this is an efficient way to get a result.
What is the center line? I define it as the set of all points inside the contour that fullfill the following condition: There must be at least one straight line going through the point where the distance to the nearest contour line is a local maximum along the line just at that point. In practice, testing a horizontal and a vertical line is enough.
I tried to implement that using two functions from the SVGGeometryElement interface: .getPointAtLength() and .isPointInFill(). The second one has so far only been implemented in Chrome, so that is the only browser this will work with.
The <text> element does not implement the SVGGeometryElement interface, so it must be converted to a <path>. That is something that cannot be done in a browser, you'll need an appropriate grafics program for that.
Finding, for 1000 * 500 points, which of ca. 5000 points along the contour of the two letters is the nearest one is a lot of computation. Therefore this contains a crude mechanism to only test those contour points that are in the vincinity. Nonetheless, give it a few seconds to complete. If you compute only one letter at that size and halve the canvas size, the execution time will aproximately quarter.
const width = 1000;
const height = 500;
const letter = document.querySelector('path');
const svg = document.querySelector('svg');
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'white';
function isInside(x, y) {
const point = svg.createSVGPoint();
point.x = x;
point.y = y;
return letter.isPointInFill(point);
}
// a 21 * 11 array of arrays
const fields = new Array(21).fill(0).map(() => {
return new Array(11).fill(0).map(() => []);
});
// a list of points along the contour
const length = Math.floor(letter.getTotalLength());
Array.from(new Array(length), (x, i) => {
return letter.getPointAtLength(i);
}).forEach(point => {
// find out if a contour point is inside a 100 * 100 rectangle
let rx1= Math.round(point.x / 100) * 2;
let ry1 = Math.round(point.y / 100) * 2;
// or a 100 * 100 rectangle that is offset by 50
let rx2 = Math.round((point.x + 50) / 100) * 2 - 1;
let ry2 = Math.round((point.y + 50) / 100) * 2 - 1;
// push the point into all four lists for the rectangles it is part of
fields[rx1][ry1].push(point);
fields[rx1][ry2].push(point);
fields[rx2][ry1].push(point);
fields[rx2][ry2].push(point);
});
const data = new Float32Array(width * height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
// only handle points inside the contour
if (isInside(x, y)) {
// find out which 50 * 50 rectangle the inside point is part of
const rx = Math.round(x / 50);
const ry = Math.round(y / 50);
// find the nearest contour point from the list for the
// appropriate 100 * 100 rectangle
const d = fields[rx][ry].reduce((min, point) => {
const dist = Math.hypot(point.x - x, point.y - y)
return Math.min(min, dist);
}, 100);
// store that distance value
data[y * width + x] = d;
}
}
}
data.forEach((v, i, a) => {
// find out if the distance to the nearest contour point
// is a local maximum, vertically or horizontally
const vert = a[i - width] < v && a[i + width] < v;
const hor = a[i - 1] < v && a[i + 1] < v;
if (vert || hor) {
// color that point as part of the center line
ctx.fillRect(i % width, Math.floor(i / width), 1, 1);
}
});
<svg width="1000" height="500" style="position:absolute">
<path id="letter" d="M 374.512,316.992 H 220.703 L 193.75,379.687 Q 183.789,402.832 183.789,414.258 183.789,423.34 192.285,430.371 201.074,437.109 229.785,439.16 V 450 H 104.688 V 439.16 Q 129.59,434.766 136.914,427.734 151.855,413.672 170.02,370.605 L 309.766,43.6523 H 320.02 L 458.301,374.121 Q 475,413.965 488.477,425.977 502.246,437.695 526.562,439.16 V 450 H 369.824 V 439.16 Q 393.555,437.988 401.758,431.25 410.254,424.512 410.254,414.844 410.254,401.953 398.535,374.121 Z M 366.309,295.312 298.926,134.766 229.785,295.312 Z M 810.742,247.266 Q 852.051,256.055 872.559,275.391 900.977,302.344 900.977,341.309 900.977,370.898 882.227,398.145 863.477,425.098 830.664,437.695 798.145,450 731.055,450 H 543.555 V 439.16 H 558.496 Q 583.398,439.16 594.238,423.34 600.977,413.086 600.977,379.687 V 123.047 Q 600.977,86.1328 592.48,76.4648 581.055,63.5742 558.496,63.5742 H 543.555 V 52.7344 H 715.234 Q 763.281,52.7344 792.285,59.7656 836.23,70.3125 859.375,97.2656 882.52,123.926 882.52,158.789 882.52,188.672 864.355,212.402 846.191,235.84 810.742,247.266 Z M 657.227,231.445 Q 668.066,233.496 681.836,234.668 695.898,235.547 712.598,235.547 755.371,235.547 776.758,226.465 798.437,217.09 809.863,198.047 821.289,179.004 821.289,156.445 821.289,121.582 792.871,96.9727 764.453,72.3633 709.961,72.3633 680.664,72.3633 657.227,78.8086 Z M 657.227,421.289 Q 691.211,429.199 724.316,429.199 777.344,429.199 805.176,405.469 833.008,381.445 833.008,346.289 833.008,323.145 820.41,301.758 807.812,280.371 779.395,268.066 750.977,255.762 709.082,255.762 690.918,255.762 678.027,256.348 665.137,256.934 657.227,258.398 Z"/>
</svg>
<canvas width="1000" height="500" style="position:absolute"></canvas>

Combining two Matrix Transformations under the same Transformation with SVG

My current task is attempting to combine objects with similar matrices under the same transformation matrix. The two matrices will always have the first 4 digits of it's transform be equal. I am having difficulty calculating the x="???" and y="???" for the second tspan. Any help towards the proper equation would be greatly appreciated.
Input
<svg>
<text transform="matrix(0 1 1 0 100 100)"><tspan x=0 y=0>foo</tspan></text>
<text transform="matrix(0 1 1 0 110 110)"><tspan x=0 y=0>bar</tspan></text>
</svg>
Output
<svg>
<text transform="matrix(0 1 1 0 100 100)">
<tspan x="0" y="0">foo</tspan>
<tspan x="???" y="???">bar</tspan>
</text>
</svg>
EDIT 1
I guess my question is more along the lines of given a point (x,y), how do I apply an existing matrix transformation to that point so that the position will not move, but the element will now be nested inside of another element.
EDIT 2
I have got this code to work for matrices with 0s in the (a,d) or (b,c) positions. Slanted/Skewed matrices I still have not got working. Any thoughts on this?
var aX = floatX[0];
var bX = floatX[1];
var cX = floatX[2];
var dX = floatX[3];
var eX = floatX[4];
var fX = floatX[5];
var aY = floatY[0];
var bY = floatY[1];
var cY = floatY[2];
var dY = floatY[3];
var eY = floatY[4];
var fY = floatY[5];
var xX = (eX * aX) + (fX * bX);
var xY = (eX * cX) + (fX * dX);
var yX = (eY * aY) + (fY * bY);
var yY = (eY * cY) + (fY * dY);
var c1 = cX - aX;
var c2 = dX + bX;
return new float[] { (yX - xX) / (c1 * c2), (yY - xY) / (c1 * c2) };
One thought that may work if my logic isn't flawed, is to find the transform for one element to the other, which can then be used to transform a point of 0,0 (as that's the original x,y) to a new location.
Once we know what the difference in transforms is (assuming that the first 4 figures in the matrix are the same as mentioned in the question, it won't work otherwise), we can figure what the difference in x,y is.
First, there's a bit of code as some browsers have removed this feature..
SVGElement.prototype.getTransformToElement = SVGElement.prototype.getTransformToElement || function(elem) {
return elem.getScreenCTM().inverse().multiply(this.getScreenCTM());
};
This is an svg method that some browsers support, but including as a polyfill in case yours doesn't (like Chrome). It finds the transform from one element to another.
We can then use this, to find the transform from the first to the second text element.
var text1 = document.querySelector('#myText1')
var text2 = document.querySelector('#myText2')
var transform = text2.getTransformToElement( text1 )
Or if you don't want the polyfill, this 'may' work (matrices aren't a strong point of mine!). getCTM() gets the current transformation matrix of an element.
var transform = text1.getCTM().inverse().multiply( text2.getCTM() )
Now we know what the transform between them was. We also know the original x,y was 0,0. So we can create an svg point 0,0 and then transform it with that matrix we've just figured, to find the new x,y.
var pt = document.querySelector('svg').createSVGPoint();
pt.x = 0; pt.y = 0;
var npt = pt.matrixTransform( transform );
Then just a delayed example to show it being moved. Set the tspan with the new x,y we've just figured from the previous transform.
setTimeout( function() {
alert('new x,y is ' + npt.x + ',' + npt.y)
tspan2.setAttribute('x', npt.x);
tspan2.setAttribute('y', npt.y);
},2000);
jsfiddle with polyfill
jsfiddle without polyfill

Wrapping long text in d3.js

I want to wrap long text elements to a width. The example here is taken from Bostock's wrap function, but seems to have 2 problems: firstly the result of wrap has not inherited the element's x value (texts are shifted left); secondly it's wrapping on the same line, and lineHeight argument has no effect.
Grateful for suggestions. http://jsfiddle.net/geotheory/bk87ja3g/
var svg = d3.select("body").append("svg")
.attr("width", 300)
.attr("height", 300)
.style("background-color", '#ddd');
dat = ["Ukip has peaked, but no one wants to admit it - Nigel Farage now resembles every other politician",
"Ashley Judd isn't alone: most women who talk about sport on Twitter face abuse",
"I'm on list to be a Mars One astronaut - but I won't see the red planet"];
svg.selectAll("text").data(dat).enter().append("text")
.attr('x', 25)
.attr('y', function(d, i){ return 30 + i * 90; })
.text(function(d){ return d; })
.call(wrap, 250);
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 1,
lineHeight = 1.2, // ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
Bostock's original function assumes that the text element has an initial dy set. It also drops any x attribute on the text. Finally, you changed the wrap function to start at lineNumber = 1, that needs to be 0.
Refactoring a bit:
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0, //<-- 0!
lineHeight = 1.2, // ems
x = text.attr("x"), //<-- include the x!
y = text.attr("y"),
dy = text.attr("dy") ? text.attr("dy") : 0; //<-- null check
tspan = text.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", x).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
Updated fiddle.
The problem is this line:
dy = parseFloat(text.attr("dy"))
In the example you've linked to, dy is set on the text elements, but not in your case. So you're getting NaN there which in turn causes the dy for the tspan to be NaN. Fix by assigning 0 to dy if NaN:
dy = parseFloat(text.attr("dy")) || 0
Complete demo here.

Read inner and outer radius of arc - d3.js

I'm working on a project which deals with visual representation of large relational data. We are using pie chart to display the data components(In order). Due to lack of space we are displaying only 10 at a time.
Consider the following example:
Assume that I have 100 data components, out of which I'll display only 10 at a given point of time. The logic i'm using is, I'm setting start and end angle to 0(Zero) for other 90 components. Where in for those 10 components i'm calculation the start and end angle as below-
var angle = 360;
var count = 10;
if(data.length > count) angle = angle/count; //data is array of data component names
else angle = angle/data.length;
//Initially I'll be displaying first ten components
for(var i=0; i<data.length; i++){
var startAngle = i * angle;
var endAngle = startAngle + angle;
var pi = = Math.PI/180;
var arc = d3.svg.arc()
.innerRadius(innerRadius) //dynamic value, calculated based on available space
.outerRadius(outerRadius) //dynamic value, calculated based on available space
.startAngle((startAngle)*pi)
.endAngle((endAngle)*pi);
//Hiding rest of the data components
if(i >= count){
arc.startAngle(0);
arc.endAngle(0);
}
arcGroup.append("path")
.attr("d", arc)
.attr("stroke", "#2E2E2E")
.attr("stroke-width", "1")
.attr("fill","gold");
var text = arcGroup.append("text")
.attr("transform", "translate(" + arc.centroid() + ")")
.attr("text-anchor", "middle")
.attr("font-family","noto_sansregular")
.attr("font-size", 40)
.attr("font-weight","Bold")
.attr("fill", "#000000")
.attr("y",0)
.style("visibility", "visible")
.text(data[i]);
//Hiding text of hidden arcs
if(i >= count) text.style("visibility", "hidden");
}
Then if user wants to see the rest of the components, i'm providing two buttons to rotate (clock or anti-clock) the content.
If current view is -> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
On rotate by one cell clockwise, resulting view should be -> 100, 1, 2, 3, 4, 5, 6, 7, 8, 9
In this case I need to hide component '10' and display component '100', and shift rest of the cells. To achieve this i just need to change the start and end angles of the arcs. I can create new arc object with calculated angles.
The problem here is I don't know how to get the inner and outer radius of the arc, which are dynamically created.
Just like that,
...
arcGroup.append("path")
.filter(function(d) {
// You're Not restricted to the "filter" function
var innerRadius = d.innerRadius()(d);
var outerRadius = d.outerRadius()(d);
})
.attr("d", arc)
...
Technically, it is possible to retrieve the innerRadius and outerRadius from the d attribute of the path elements, but it will require parsing the DSL and will be tedious. These values are not stored nicely on the elements itself by d3.
Hence, it would be better if you recompute the innerRadius and outerRadius while updating the elements:
function showFromIdx(firstIndex) {
argGroup.selectAll('path')
.data( d3.range(data.length)
.map(function (d) {
return (d - firstIndex + data.length) % data.length;
})
)
.attr('d', function (d) {
// You will have to calculate the radii again here.
var innerRadius = foo(d), outerRadius = bar(d);
return d3.svg.arc()
.startAngle(i < count ? i * angle : 0)
.endAngle(i < count ? (i + 1) * angle : 0)
.innerRadius(innerRadius)
.outerRadius(outerRadius)(d);
});
}
Here are several functions that I wrote to get inner and outer radiuses of arcs created using d3. Please tell me if you find mistakes in code.
function getInnerRadiusFromArc(arc) {
var numbersInPattern = _getArcNumbers(arc);
// Possibly, that's sector, so it starts from 0.
// Or maybe that's something else.
if (numbersInPattern.length < 4) {
return 0;
}
// Getting minimum from the array.
var innerRadius = Math.min.apply(null, numbersInPattern);
return innerRadius;
}
function getOuterRadiusFromArc(arc) {
var numbersInPattern = _getArcNumbers(arc);
// Getting maximum from the array.
var outerRadius = Math.max.apply(null, numbersInPattern);
return outerRadius;
}
function _getArcNumbers(arc) {
// Path description parameter, containing necessary data.
var pathDescription = arc.getAttribute("d");
// We need to get all patterns like A<number>,<number>.
// RegExp source:
// http://www.regular-expressions.info/floatingpoint.html
const numberRegExp = /[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?/g;
var arcPattern = new RegExp("A" + numberRegExp.source + "," + numberRegExp.source, "g");
var arcParameters = pathDescription.match(arcPattern);
var numbersInPattern = [];
// We get all the numbers from array ["A<number>,<number>", "A<number>,<number>", ...].
for (let parameterIndex = 0; parameterIndex < arcParameters.length; parameterIndex++) {
let parameter = arcParameters[parameterIndex];
let numbers = parameter.match(numberRegExp);
if (numbers !== null) {
numbersInPattern = numbersInPattern.concat(numbers);
}
}
// Transform strings in our array to numbers.
numbersInPattern = numbersInPattern.map(function (numberString) {
return parseFloat(numberString);
});
return numbersInPattern;
}

D3 force layout with CSS positioning

I'm placing circles on a map corresponding to GPS coordinates. Each circle is contained within an svg container which is placed on the page using CSS top and left properties. In my implementation, these containers often sit atop one another.
I am trying to implement collision detection and/or add a slight negative charge to these containers so that overlaps cause containers to distance themselves from one another.
Thus far, my tests with force layouts have either resulted in no change, or resulted in an error ('cannot set property index of null' or 'cannot set property x of null'). It's apparent that I'm doing something wrong but I have been unable to identify a path to resolution from the articles I've read online.
Any ideas on how I can stop the containers from sitting atop one another?
var self = this;
var data = [{lat: 127, lon: 36, name: 'a', radius: 9},{lat:127, lon: 36, name: 'b', radius: 9}];
// Position SVG containers correctly
var latLngToPx = function(d) {
var temp = new google.maps.LatLng(d.lat, d.lon);
temp = self.map.projection.fromLatLngToDivPixel(temp);
d.x = temp.x;
d.y = temp.y;
return d3.select(this)
.style('left', d.x + 'px')
.style('top', d.y + 'px');
};
var collide = function(node) {
var r = node.radius + 16,
nx1 = node.x - r,
nx2 = node.x + r,
ny1 = node.y - r,
ny2 = node.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = node.radius + quad.point.radius;
if (l < r) {
l = (l - r) / l * 0.5;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
}
var svgBind = d3.select(settings[type].layer).selectAll('svg')
.data(data, function(d){ return d.name; })
.each(latLngToPx);
var svg = svgBind.enter().append('svg')
.each(latLngToPx)
// svg[0] contains the svg elements
var nodes = svg[0];
var force = d3.layout.force()
.nodes(nodes)
.charge(-100)
.start();
force.on('tick', function(){
var q = d3.geom.quadtree(nodes),
i = 0,
n = nodes.length;
while (++i < n) {
q.visit(collide(nodes[i]));
}
svg
.style('left', function(d){ return (d.x - lm.config.offset) + 'px';})
.style('top', function(d){ return (d.y - lm.config.offset) + 'px';});
});
var circ = svg.append('circle')
.attr('r', settings[type].r)
.attr('cx',10)
.attr('cy',10)
You shouldn't need to do the collision detection yourself -- the force layout should take care of that for you. Here are the basic steps you need to take.
To each data element that represents a circle, add x and y members that contain their current (screen) coordinates. This is what the force layout will operate on.
Pass the array of these elements to the force layout as nodes. There's no need to set links to start with, although you might want to do so later to control the placement of nodes with respect to each other.
Start the force layout.
For each tick, redraw the elements at the appropriate position.
Tweak the parameters of the force layout to your liking.
You are doing most of this already, I'm just mentioning it again to clarify. The code would look something like this.
function latLngToPx(d) {
var temp = new google.maps.LatLng(d.lat, d.lon);
temp = self.map.projection.fromLatLngToDivPixel(temp);
d.x = temp.x;
d.y = temp.y;
};
data.forEach(function(d) { latLngToPx(d); });
var nodes = d3.select("body").selectAll("svg").data(data).enter().append("svg");
var force = d3.layout.force().nodes(data);
force.on("tick", function() {
nodes.style('left', function(d){ return (d.x - lm.config.offset) + 'px';})
.style('top', function(d){ return (d.y - lm.config.offset) + 'px';});
});

Resources