HTML & CSS
Article

How to Translate from DOM to SVG Coordinates and Back Again

By Craig Buckler

All the cool kids are using Scalable Vector Graphics. SVGs are great until you want to mix DOM and vector interactions — then life becomes more complicated.

SVGs have their own coordinate system. It is defined via the viewbox attribute, e.g. viewbox="0 0 800 600" which sets a width of 800 units and a height of 600 units starting at (0, 0). If you position this SVG in an 800×600 pixel area, each SVG unit maps directly to a screen pixel.

However, the beauty of vector images is they can be scaled to any size. Your SVG could be scaled in a 400×300 space or even stretched beyond recognition in a 100×1200 space. Adding further elements to an SVG becomes difficult if you don’t know where to put them.

(SVG coordinate systems can be confusing – Sara Soueidan’s viewport, viewBox and preserveAspectRatio article describes the options.)

Simple Separated SVG Synergy

You may be able to avoid translating between coordinate systems entirely.

SVGs embedded in the page (rather than an image or CSS background) become part of the DOM and can be manipulated in a similar way to other elements. For example, given a basic SVG with a single circle:

<svg id="mysvg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600" preserveAspectRatio="xMidYMid meet">
  <circle id="mycircle" cx="400" cy="300" r="50" />
<svg>

we can apply CSS effects:

circle {
  stroke-width: 5;
  stroke: #f00;
  fill: #ff0;
}

circle:hover {
  stroke: #090;
  fill: #fff;
}

and attach event handlers to modify their attributes:

var mycircle = document.getElementById('mycircle');

mycircle.addEventListener('click', function(e) {
  console.log('circle clicked - enlarging');
  mycircle.setAttributeNS(null, 'r', 60);
}, false);

The following example adds thirty random circles to an SVG image, applies a hover effect in CSS and uses JavaScript to increase the radius by ten units when a circle is clicked:

See the Pen SVG interaction by SitePoint (@SitePoint) on CodePen.

SVG to DOM Coordinate Translation

What if we want to overlay a DOM element on top of an SVG item, e.g. a menu or information box on a map? Again, because our HTML-embedded SVG elements form part of the DOM we can use the fabulous getBoundingClientRect() method to return all dimensions in a single call. Open the console in the example above to reveal the clicked circle’s new attributes following a radius increase.

Element.getBoundingClientRect() is supported in all browsers and returns an DOMrect object with the following properties in pixel dimensions:

  • .x and .left – x-coordinate, relative to the viewport origin, of the left side of the element
  • .right – x-coordinate, relative to the viewport origin, of the right side of the element
  • .y and .top – y-coordinate, relative to the viewport origin, of the top side of the element
  • .bottom – y-coordinate, relative to the viewport origin, of the bottom side of the element
  • .width – width of the element (not supported in IE8 and below but is identical to .right minus .left)
  • .height – height of the element (not supported in IE8 and below but is identical to .bottom minus .top)

All coordinates are relative to the browser viewport and will therefore change as the page is scrolled. The absolute location on the page can be calculated by adding window.scrollX to .left and window.scrollY to .top.

DOM to SVG Coordinate Translation

This is the tricky part. Presume you click an SVG and want to create or position an SVG element at that point. The event handler object will give you the DOM .clientX and .clientY pixel coordinates but these must be translated to SVG units.

It’s tempting to think you can calculate the x and y coordinates of an SVG point by applying a multiplication factor to the pixel location. For example, if a 1000 unit-width SVG is placed in a 500px width container, you can multiply any cursor x coordinate by two to get the SVG location. It rarely works!…

  • There is no guarantee the SVG will fit exactly into your container.
  • If the page or element dimensions change – perhaps in response to the user resizing the browser – the x and y factors must be re-calculated.
  • The SVG could be transformed in either 2D or 3D space.
  • Even if you overcome these hurdles, it never quite works as you expect. There’s often a margin of error.

Fortunately, SVGs provide their own matrix factoring mechanisms to translate coordinates. The first step is to create a point on the SVG using the createSVGPoint() method and pass in our screen x and y coordinates:

var svg = document.getElementById('mysvg'),
    pt = svg.createSVGPoint();

pt.x = 100;
pt.y = 200;

We can then apply a matrix transformation. That matrix is created from an inverse of the SVG’s (under-documented!) .getScreenCTM() method which maps SVG units to screen coordinates:

var svgP = pt.matrixTransform(svg.getScreenCTM().inverse());

svgP now has .x and .y properties which provide the SVG coordinate location.

We can therefore place a circle at a point clicked on an SVG canvas:

var svg = document.getElementById('mysvg'),
    NS = svg.getAttribute('xmlns');

svg.addEventListener('click', function(e) {
  var pt = svg.createSVGPoint(), svgP, circle;
  
  pt.x = e.clientX;
  pt.y = e.clientY;
  svgP = pt.matrixTransform(svg.getScreenCTM().inverse());

  circle = document.createElementNS(NS, 'circle');
  circle.setAttributeNS(null, 'cx', svgP.x);
  circle.setAttributeNS(null, 'cy', svgP.y);
  circle.setAttributeNS(null, 'r', 10);
  svg.appendChild(circle);
}, false);

The createElementNS() and setAttributeNS() methods are identical to the standard DOM createElement() and setAttribute() methods except they specify an XML namespace URI. In other words, they act on the SVG rather than the HTML. setAttributeNS() can be passed a null namespace URI because it is directly manipulating an SVG element.

DOM to Transformed SVG Element Coordinates

There’s a further complication. What if we click on an SVG element which has been transformed in some way? It could be scaled, rotated or skewed which would affect our resulting SVG coordinate. For example, this <g> layer is 4x larger than the standard unit so coordinates will be one quarter those of the containing SVG:

<g id="local" transform="scale(4)">
  <rect x="50" y="50" width="100" height="100" />
</g>

The resulting rectangle appears to be 400 units in size at position 200, 200.

Fortunately, the .getScreenCTM() can be used on any SVG element and the resulting matrix considers all transformations. We can therefore create a simple svgPoint translation function:

var svg = document.getElementById('mysvg'),
    local = svg.getElementById('local');

console.log( svgPoint(svg, 10, 10) ); // returns x, y
console.log( svgPoint(local, 10, 10) ); // = x/4, y/4

// translate page to SVG co-ordinate
function svgPoint(element, x, y) {
  var pt = svg.createSVGPoint();

  pt.x = x;
  pt.y = y;

  return pt.matrixTransform(element.getScreenCTM().inverse());
}

The following demonstration works in all browsers. A circle is added to the cursor point when the grey SVG area is clicked. A circle is also added when the green box is clicked, but that layer has been scaled 4x so the circle appears four times larger.

See the Pen translate SVG co-ordinates by SitePoint (@SitePoint) on CodePen.

You could keep life simple by ensuring all SVG units map directly to page coordinates but, at some point, your client will ask for that image to be “just a few pixels larger” and your system will break. Translate between coordinate systems now and you’ll never need worry again!

  • https://google.com/+VladGURDIGA Vlad GURDIGA

    Very nice article, thank you! 🤓

    One little note on getBoundingClientRect: it seems like it only returns a DOMRect in Firefox. In Chrome, IE, Edge, and Safari it returns a ClientRect, which is similar to Firefox’s DOMRect, except it doesn’t have x and y members. 🤓

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Front-end, once a week, for free.