Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get absolute coordinates of object inside a <g> group?

Tags:

svg

d3.js

This may be a FAQ, so feel free to point me to another answer. The topic is difficult to search on.

If I want to use d3.js to get an attribute that's explicitly declared in an SVG object, or that I've explicitly put there using D3, I can easily get the value of the attribute using d3.select. For example, this prints 300:

...
<circle id="mycircle" r="10" cx="100" cy="200">
...
d3.select("#mycircle").attr("cx", 300);
console.log(d3.select("#mycircle").attr("cx"));

What if I don't explicit set the value of the attribute, but it is implicitly "set" from a <g> group? Or: How can I use code to find out where a <g> group is centered? I'd like some way of determining where in the absolute coordinate system of the <svg> object the things inside the <g> are. If I knew where the <g> was, how it's oriented in space, etc., I could figure out where points inside it are. How can I do that?

BigBadaboom remarks in a comment on an answer to this question that what is inherited is not a pair of coordinates, but a transform attribute. So I can select a <g> and get the value of the transform attribute:

console.log(d3.select("#mygroup").attr("transform"));

which prints, for example:

"rotate(-125.93)translate(0,-25)"

Do I have to parse that to find out where the <g> is situated in the absolute coordinate system?

like image 464
Mars Avatar asked Sep 25 '14 23:09

Mars


3 Answers

Others here have already mentioned SVGLocatable.getBBox() which is useful for grabbing the bounding box of an element in terms of its own local coordinate system. Unfortunately, as you noticed, this doesn't take into account any of the transformations done on the element or on its parent elements.

There are a couple other functions available that will help you out a ton when dealing with those transforms.

SVGLocatable.getScreenCTM() gives you an SVGMatrix representing the transformations needed to convert from the viewport coordinates to the local coordinates of your element. This is great because it will take into account the transforms applied to the element it is called on, and any transforms applied to parent elements. Unfortunately, it also takes into account where exactly the element is on the screen, which means if you have content before your svg document, or even just some margins around it, the returned matrix will include that space as a translation.

Element.getBoundingClientRect() will allow you to account for that space. If you call this function on the SVG document itself, you can find out by how much the SVG is offset on the screen.

Then all you have to do is combine the two when you want to convert between coordinate systems. HERE is some good info on how an SVGMatrix works. The important thing to know for now is that an SVGMatrix is an object with six properties a, b, c, d, e, and f which represent a transformation as follows:

svg matrix equations

Lets say you have a variable svgDoc which is a reference to the svg document (not a d3 selection, but the element itself). Then you can create a function that will convert to the coordinate system of an svg element elem as follows.

function convertCoords(x,y) {

  var offset = svgDoc.getBoundingClientRect();

  var matrix = elem.getScreenCTM();

  return {
    x: (matrix.a * x) + (matrix.c * y) + matrix.e - offset.left,
    y: (matrix.b * x) + (matrix.d * y) + matrix.f - offset.top
  };
}

Then, say you wanted to put a dot in the middle of elem, you could do something like this:

var bbox = elem.getBBox(),
    middleX = bbox.x + (bbox.width / 2),
    middleY = bbox.y + (bbox.height / 2);

var absoluteCoords = convertCoords(middleX, middleY);

var dot = svg.append('circle')
  .attr('cx', absoluteCoords.x)
  .attr('cy', absoluteCoords.y)
  .attr('r', 5);

Of course, you'd probably want to generalize the convertCoords function so you can pass in the target element, but hopefully that'll get you off in the right direction. Good luck!

A better implementation would be a factory that generates a conversion function for any given element and svg document context:

function makeAbsoluteContext(element, svgDocument) {
  return function(x,y) {
    var offset = svgDocument.getBoundingClientRect();
    var matrix = element.getScreenCTM();
    return {
      x: (matrix.a * x) + (matrix.c * y) + matrix.e - offset.left,
      y: (matrix.b * x) + (matrix.d * y) + matrix.f - offset.top
    };
  };
}

This could be used as follows given the same elem and svgDoc as the naive example:

var bbox = elem.getBBox(),
    middleX = bbox.x + (bbox.width / 2),
    middleY = bbox.y + (bbox.height / 2);

// generate a conversion function
var convert = makeAbsoluteContext(elem, svgDoc);

// use it to calculate the absolute center of the element
var absoluteCenter = convert(middleX, middleY);

var dot = svg.append('circle')
  .attr('cx', absoluteCenter.x)
  .attr('cy', absoluteCenter.y)
  .attr('r', 5);
like image 82
jshanley Avatar answered Nov 03 '22 23:11

jshanley


@Jshanley's excellent answer is actually very easily implemented in raw JavaScript (or any framework) using SVGPoint's matrix transformation.

/**
* Get a new XY point in SVG-Space, where X and Y are relative to an existing element.  Useful for drawing lines between elements, for example

* X : the new X with relation to element, 5 would be '5' to the right of element's left boundary.  element.width would be the right edge.
* Y : the new Y coordinate, same principle applies
* svg: the parent SVG DOM element
* element: the SVG element which we are using as a base point.
*/
function getRelativeXY(x, y, svg, element){
  var p = svg.createSVGPoint();
  var ctm = element.getCTM();
  p.x = x;
  p.y = y;
  return p.matrixTransform(ctm);
}

See also: Rectangle coordinates after transform

In order to find the edges of your circle, for example:

var leftTangent = getRelativeXY(circle.cx-circle.r, circle.y, svg, circle);
var rightTangent = getRelativeXY(circle.cx+circle.r, circle.y, svg, circle);
var topTangent= getRelativeXY(circle.cx, circle.y-circle.r, svg, circle); 
var bottomTangent= getRelativeXY(circle.cx, circle.y+ circle.r, svg, circle);
var deadCenter= getRelativeXY(circle.cx, circle.y, svg, circle);

Admittedly not that interesting with a plain circle, but once the circle has been shifted or stretched it's a great tool for getting the coordinates.

W3's Spec

Microsoft's more easily understood tutorial

like image 26
InfernalRapture Avatar answered Nov 03 '22 21:11

InfernalRapture


D3 has a built in function to parse svg transforms: d3.transform

You could use it to get the translate array ([x, y]) of the transform, i.e.:

var transformText = d3.select("#mygroup").attr("transform");
var translate = d3.transform(transformText).translate;  //returns [0,-25]
like image 20
Josh Avatar answered Nov 03 '22 23:11

Josh