Zoom to bounding box of path on externally loaded svg using D3 - javascript

I'm using D3 to load an external SVG map that isn't using topojson (as the map was hand created and non a traditional map). I'm trying to target the elements #lines path so that when clicked, each path zooms and fills its bounding box.
I'm trying to use this example from Mike Bostock but can't figure out how to replicate it with the data that isn't using topojson. See this line:
.data(topojson.feature(us, us.objects.states).features)
It this even possible?
Here is the code I'm using to load the SVG.
var mapContainer = $('.map');
d3.xml("../assets/subwaymap.svg", function(error, subwayMap) {
if (error) throw error;
$('.map').append(subwayMap.documentElement)
I've tried getting the bounding box using .getBBOX but am confused at how it connects. It seems like all examples I've seen use d3.create("svg") and then tack all the functionality within that but since my data is already appended to the DOM, would this be necessary? Fairly new to D3. Thanks!

Two initial considerations: d3.create("svg") is rarely used in real D3 codes. Also, you don't have data appended to the DOM, just SVG elements you loaded (unless you're calling that "data").
Back to your question, you don't need path.bounds to make your code work, actually you don't even need d3.zoom. All you need is get the element's box (with getBBox) and do the appropriate transform.
The real problem, though, is that you need to wrap all the elements in a <g>, because you cannot apply the transform to the root SVG in SVG 1.1 (apparently this is possible in SVG 2).
Here is a basic demo. In this demo I'm using an external SVG made with different elements (circle, rectangle, text...), which represents the SVG you're appending. You get this SVG with:
const svg = d3.select("svg");
Then, considering you somehow manage to fix the <g> problem I mentioned, you get that group...
const g = svg.select("g");
... and you select the elements you want to zoom in (here, everything), binding an event listener:
const elements = g.selectAll("*")
.on("click", clicked);
In this demo I'm using Bostock's math, to save (my) time, but you can change it. Click the element to zoom in, click it again to zoom out.
const width = 500,
height = 400;
const svg = d3.select("svg");
const g = svg.select("g");
const elements = g.selectAll("*")
.each(function() {
d3.select(this).datum({})
})
.on("click", clicked);
function clicked(d) {
d.clicked = !d.clicked;
const bounds = this.getBBox();
const x0 = bounds.x;
const x1 = bounds.x + bounds.width;
const y0 = bounds.y;
const y1 = bounds.y + bounds.height;
g.transition().duration(1000).attr("transform", d.clicked ? "translate(" + (width / 2) + "," + (height / 2) + ") scale(" + (1 / Math.max((x1 - x0) / width, (y1 - y0) / height)) + ") translate(" + (-(x0 + x1) / 2) + "," + (-(y0 + y1) / 2) + ")" : "transform(0,0) scale(1)");
}
<svg width="500" height="400">
<g>
<circle cx="50" cy="50" r="30" stroke="black" stroke-width="3" fill="teal"></circle>
<rect x="300" y="20" rx="20" ry="20" width="150" height="150" style="fill:tomato;stroke:black;stroke-width:3"/>
<polygon points="200,100 250,190 160,210" style="fill:lavender;stroke:purple;stroke-width:3" />
<path d="M 140 350 q 150 -200 350 0" stroke="blue" stroke-width="5" fill="none" />
<text x="30" y="300" transform="rotate(-30, 30, 300)">Foo Bar Baz</text>
</g>
</svg>
<script src="https://d3js.org/d3.v5.min.js"></script>

Related

getBoundingClientRect() returns inaccurate values for complex SVG's in Chrome

I'm trying to calculate the bounding box of transformed SVG elements and for that I'm using getBoundingClientRect() and mapping the x and y values to SVG coordinates. However, this function seems to produce wrong outputs in Chrome and Edge when the shape has curves and a rotation. In the other hand, Firefox is able to produce the expected result.
Here's an example.
<svg height="600" width="600">
<g transform="rotate(-50, 240, 174)" fill="#A1B6FF">
<path transform="translate(100, 100)"
d="M0, 0 Q 140 128.76 280 0 v 148 Q 140 276.76 0 148 v -148z">
</path>
</g>
</svg>
Is there any way to achieve this with more precision like Firefox?
I deleted my previous answer as it was plain wrong, hope this is a better one:
<div>
<svg id="svg" width="600" height="600" version="1.1" viewBox="0 0 600 600" xmlns="http://www.w3.org/2000/svg">
<g id="svgElem" transform="rotate(-50, 240, 174)" fill="#A1B6FF">
<path transform="translate(100, 100)"
d="M0, 0 Q 140 128.76 280 0 v 148 Q 140 276.76 0 148 v -148z">
</path>
</g>
</svg>
</div>
<script type="text/javascript">
let svgElem = document.getElementById('svgElem');
let bBox = svgElem.getBBox();
console.dir(bBox);
</script>
The SVGRect returned by getBBox is identical Firefox/Chromium.
However as stated here on MDN
The returned value is a SVGRect object, which defines the bounding box. This value is irrespective of any transformation attribute applied to it or the parent elements
So you always get the bounding box of the svg before tranforms are applied this way. If you use getBoundingClientRect to get a DOMRect you will find out that Chrome seems to just apply the transforms on the original bounding rect and then calculate the bounding box of that.
You would achieve the same with something like this (more or less useless code just for illustration):
<script type="text/javascript">
const svg = document.getElementById('svg');
let svgElem = document.getElementById('svgElem');
const bBox = svgElem.getBBox(); // MDN: The returned value is a SVGRect object, which defines the bounding box. This value is irrespective of any transformation attribute applied to it or the parent elements
console.dir(bBox);
const boundingClientRect = svgElem.getBoundingClientRect();
console.dir(boundingClientRect);
// create a rect without transforms
const rect1 = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect1.setAttribute('x', bBox.x);
rect1.setAttribute('y', bBox.y);
rect1.setAttribute('width', bBox.width);
rect1.setAttribute('height', bBox.height);
rect1.setAttribute('fill', '#00ff0040');
svg.appendChild(rect1);
const ctm = svgElem.getCTM();
const topLeftX = ctm.a * bBox.x + ctm.c * bBox.y + ctm.e;
const topLeftY = ctm.b * bBox.x + ctm.d * bBox.y + ctm.f;
const topRightX = ctm.a * (bBox.x + bBox.width) + ctm.c * bBox.y + ctm.e;
const topRightY = ctm.b * (bBox.x + bBox.width) + ctm.d * bBox.y + ctm.f;
const bottomLeftX = ctm.a * bBox.x + ctm.c * (bBox.y + bBox.height) + ctm.e;
const bottomLeftY = ctm.b * bBox.x + ctm.d * (bBox.y + bBox.height) + ctm.f;
const bottomRightX = ctm.a * (bBox.x + bBox.width) + ctm.c * (bBox.y + bBox.height) + ctm.e;
const bottomRightY = ctm.b * (bBox.x + bBox.width) + ctm.d * (bBox.y + bBox.height) + ctm.f;
const x = Math.min(topLeftX, topRightX, bottomLeftX, bottomRightX);
const y = Math.min(topLeftY, topRightY, bottomLeftY, bottomRightY);
const width = Math.max(topLeftX, topRightX, bottomLeftX, bottomRightX) - x;
const height = Math.max(topLeftY, topRightY, bottomLeftY, bottomRightY) - y;
const rect2 = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect2.setAttribute('x', x);
rect2.setAttribute('y', y);
rect2.setAttribute('width', width);
rect2.setAttribute('height', height);
rect2.setAttribute('fill', '#ff000040');
svg.appendChild(rect2);
</script>
Or you could just check the Developer tools of Firefox/Chromium to see the dfifferences (just to say putting a group around doesn't work either).
Maybe SVG version 2 will make a difference in the future:
Chrome Platfor Status SVG2
So now what? If getBBox is the only function that seems to be working but only for svgs without inner transforms, can these transforms be applied dynamically with javascript?
Turns out someone went the extra mile:
flatten.js
put the script in a file 'flatten.js' and remove the leftovers at the top if still there (html, title..)
<div>
<svg id="svg" width="600" height="600" version="1.1" viewBox="0 0 600 600" xmlns="http://www.w3.org/2000/svg">
<g id="svgElem" transform="rotate(-50, 240, 174)" fill="#A1B6FF">
<path transform="translate(100, 100)"
d="M0, 0 Q 140 128.76 280 0 v 148 Q 140 276.76 0 148 v -148z">
</path>
</g>
</svg>
</div>
<script src="flatten.js"></script>
<script type="text/javascript">
const svg = document.getElementById('svg');
let svgElemClone = document.getElementById('svgElem').cloneNode(true); // flatten will directly change the element so a clone is made
svgElemClone.id = 'svgElemClone';
svg.appendChild(svgElemClone);
flatten(svgElemClone, true);
const bBox = svgElemClone.getBBox();
console.dir(bBox);
</script>
So this might be a workaround way to get the "real" bounding box.
As for getBoundingClientRect:
MDN says: "The returned value is a DOMRect object which is the smallest rectangle which contains the entire element, including its padding and border-width."
IMHO there is a bug in Chromium's implementation.

svg : find x,y coordinates of rect vertices

I have various svg rects on a web page on which a transform is applied in the form :
transform="translate(418 258) rotate(-45.2033 15 18) scale(2.5 2.5)"
I need to get the x,y coordinates of the 4 vertices of each rect after the transform is applied.
Here is an exemple of code :
<g transform="translate(418 258) rotate(-45.25 15 18) scale(2.5 2.5)">
<rect id="box" x="0" y="0" width="31" height="37" style="fill:none;stroke:rgb(102, 102, 102);stroke-width:1.5px;">
</rect>
</g>
I have tried the following formula in plain js :
x' = x * cos(angle) + y * sin(angle)
y' = -x * sin(angle) + y * cos(angle)
but the results are slightly different from the svg display in various browsers.
I guess this can be done using js/svg primitives, but I don't know how, and didn't find any code example. Perhaps changing the rects into paths after transform would do the trick...
Last but not least, I'm using jquery but not d3.
Thanks in advance.
You can read the transform attribute and convert it to a matrix.
Then for each of the rectangle's four corners, you can use that matrix to calculate the transformed corner locations.
See the following demo.
This demo assumes that there is an element with id of "box", and that the transform you care about is just the one on the parent <g> element. If your circumstances are more complex than that, then you will need to do some more work on this code.
// Get a reference to the "box" rect element
var box = document.getElementById("box");
// Get its x, y, width and height
var bx = box.x.baseVal.value;
var by = box.y.baseVal.value;
var bw = box.width.baseVal.value;
var bh = box.height.baseVal.value;
// Get the box's parent element (the <g>)
var parentGroup = box.parentNode;
// Read the transform attribute and convert it to a transform matrix object
var transformMatrix = parentGroup.transform.baseVal.consolidate().matrix;
// For each of the rectangle's four corners, use the transform matrix to calculate its new coordinates
console.log("point 1 = ", doPoint(bx, by));
console.log("point 2 = ", doPoint(bx + bw, by));
console.log("point 3 = ", doPoint(bx + bw, by + bh));
console.log("point 4 = ", doPoint(bx, by + bh));
function doPoint(x, y)
{
// We need a reference to the <svg> element for the next step
var svg = box.ownerSVGElement;
// Create an SVGPoint object with the correct x and y values
var pt = svg.createSVGPoint();
pt.x = x;
pt.y = y;
// Use the "matrixTransform()" method on SVGPoint to apply the transform matrix to the coordinate values
pt = pt.matrixTransform(transformMatrix);
// Return the updated x and y values
return {x: pt.x, y: pt.y};
}
<svg>
<g transform="translate(418 258) rotate(-45.25 15 18) scale(2.5 2.5)">
<rect id="box" x="0" y="0" width="31" height="37" style="fill:none;stroke:rgb(102, 102, 102);stroke-width:1.5px;">
</rect>
</g>
</svg>

How can I find the translated co-ordinates of an SVG element?

I have a SVG element with x and y set, but I'm also translating it by a certain vector using transform="translate(a, b)", which changes the co-ordinates it's rendered to but obviously doesn't update its x and y attributes. Is there a way to get the actual co-ordinates, which in this case would be x + a and y + b, without having to directly parse the values out of the transform attribute?
Not that this is a D3-specific question, but my code looks like this:
svg.selectAll(selector)
.attr("x", x)
.attr("y", y)
.attr("width", width)
.attr("height", height)
.attr("transform", `translate(${a}, ${b})`);
OK, this is not really D3 related, but pure SVG/javascript:
I use this function here that I call "flatten", basically you want to reset matrix to non-transformed one (matrix(1 0 0 1 0 0)) and update path points with their sort of flattened values:
flattenShape(item, matrix) {
let points = item.pathPoints;
let l = points.length;
for (let i = 0; i<l; i++) {
let cache = this.mainSVG.createSVGPoint();
cache.x = points[i].x;
cache.y = points[i].y;
cache = cache.matrixTransform(matrix);
points[i].x = cache.x;
points[i].y = cache.y;
}
item.d = this.constructPath(points);
item.transform = "matrix(1 0 0 1 0 0)";
};
item - your SVG element, matrix - you need to get the actual SVGMatrix of the element in question. I get it using:
let matrix = YOUR_SVG_ELEMENT.transform.baseVal.consolidate().matrix;
So my approach is maybe too specific but generally speaking:
consolidate matrix of the SVG element that you are transforming.
use SVGPoint to perform: matrixTransform(matrix) for each coordinate in question.
reset transform attribute to initial state e.g. matrixTransform(matrix)
You can use the getBoundingClientRect() method to get the position of the node.
Here's a snippet showing two rects with one of them translated:
var svgXY = d3.select('svg').node().getBoundingClientRect();
var rect1 = d3.select('rect#test1').node().getBoundingClientRect();
console.log('Rect 1: { top: ' + (rect1.top-svgXY.top) + ', left: ' + (rect1.left-svgXY.left) + '}');
var rect2 = d3.select('rect#test2').node().getBoundingClientRect();
console.log('Rect 2: { top: ' + (rect2.top-svgXY.top) + ', left: ' + (rect2.left-svgXY.left) + '}');
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.js"></script>
<svg width="300" height="200">
<rect x="20" y="40" fill="red" width="100" height="40" id="test1"></rect>
<rect x="20" y="40" fill="green" width="100" height="40" transform="translate(40, 30)" id="test2"></rect>
</svg>
Or if you're using jQuery, you can get the position by using $('rect#test1').position().
Hope this helps.
Edit:
body had a margin of 8px by default and hence x was equal to 28. I've added the CSS and check out the snippet now.

How to get Mid point of <g> tag in svg using javascript

I am working in SVG tags using javascript. I tried to get group tag <g> midpoint in svg. Is it possible to get mid point value of group tag using javascript?
Here's my demo group tag <g>
<g id="object_7" transform="translate(573,703) scale(0.5,0.51)" style="pointer-events:inherit">
<path d="m-40,-19l3,-3l74,0l3,3l0,37l-3,3l-74,0l-3,-3l0,-37z" id="uid127" stroke-linejoin="round" stroke-linecap="round" fill="#1e1d19" stroke="#000000"/>
<path d="m-9,21l4,2l10,0l4,-2" id="uid129" stroke-linejoin="round" stroke-linecap="round" fill-opacity="0" fill="none" stroke="#000"/>
<path d="m-40,-19l3,-3l74,0l3,3l-77,40l-3,-3l0,-37z" id="uid131" stroke-linejoin="round" stroke-linecap="round" fill-opacity="0.12" fill="#000000"/>
</g>
Here I need to get midpoint point of group tag. I used to get mouse co-ordinates for getting center of x and y position in group tag, but I did not achieve it. Can anyone please guide me?
You can get the bounding box of the <g> element by getting a reference to it and calling the function getBBox().
var bbox = document.getElementById("object_7").getBBox();
Note however that this is the union of all the bounding boxes of the group's children. If the group has a transform, it is not reflected in the bbox value. If you are adding elements to the group, this is probably the one you want.
If you want the bounds of the object in screen space, then you can get the group element's transform and apply it to the centre point you have calculated.
var ctm = document.getElementById("object_7").getCTM()
// Calculate the centre of the group
var cx = bbox.x + bbox.width/2;
var cy = bbox.y + bbox.height/2;
// Transform cx,cy by the group's transform
var pt = document.getElementById("mysvg").createSVGPoint();
pt.x = cx;
pt.y = cy;
pt = pt.matrixTransform(ctm);
// centre point in screen coordinates is in pt.x and pt.y
Demo here
If you want to get absolute middle point/position of g tag in screen:
let el = document.getElementById("object_7")
let midX = (el.getBoundingClientRect().left + el.getBoundingClientRect().right) / 2
let midY = (el.getBoundingClientRect().top + el.getBoundingClientRect().bottom) / 2
It also works for other svg elements.

Trying to rotate and transform SVG path - do I need trigonometry calculations?

I'm trying to manipulate with mouse SVG path which represents symbol of electronics resistor. This symbol requires manipulation with end of the "leads" (if you can picture real resistor); therefore I am trying to achieve draging 1st point arround (2nd is still there) and to all points of path to behave proportionally in when drag the 1st point on new coordinates.
Try to group, try with trigonometry functions...so code is like:
`<g id="r" > <!-- R - group for symbol of electronics resistor -->
<path d="M 200 20 v80 h30 v150 h-60 v-150 h30 m 0 150 v80 "
fill="none" stroke="blue" stroke-width="5" />
<circle cx="200" cy="20" r="10"
fill="blue" />
<circle cx="200" cy="330" r="10"
fill="blue"/>
</g>`
Please, help.
I think I've made roughly what you want: http://dl.dropbox.com/u/169269/resistor.svg
Click and drag on the resistor to scale and rotate it to that mouse position. In this version, the line thickness and circles also scale, but it shouldn't be too difficult to avoid that.
It does require trigonometry and transformations. The key part is the drag function, which I explain in more detail at: http://www.petercollingridge.co.uk/interactive-svg-components/draggable-svg-element
function drag(evt)
{
if(selected_element != 0)
{
var resistor_x = 200;
var resistor_y = 100;
var mouse_x = evt.pageX;
var mouse_y = evt.pageY;
dx = resistor_x - mouse_x;
dy = resistor_y - mouse_y;
d = Math.sqrt(dx*dx + dy*dy); // Find distance to mouse
theta = 90+Math.atan2(dy, dx)*180/Math.PI; //Find angle to mouse in degrees
transform = "translate(200, 100) rotate(" + theta + ") scale(" + d/310 + ")" ;
selected_element.setAttributeNS(null, "transform", transform);
}
}
Note that this code assumes the resistor is 310 pixels long and rotating about (200, 100). Scaling and rotation transformations work centred on (0,0), so you should draw the resistor with the static point at (0,0) and then translate it to where you want.

Categories

Resources