How to Perfectly Fit an SVG’s ViewBox to its Contents Using JavaScript

nas5w

Nick Scialli (he/him)

Posted on January 19, 2021

How to Perfectly Fit an SVG’s ViewBox to its Contents Using JavaScript

Using the SVG tag on websites is handy, but its interface can be different than we're used to. In this post, we're going to see how to fit the viewport of an SVG to its contents every time.


Please give this post a πŸ’“, πŸ¦„, and πŸ”– if you want more SVG/visualization posts!


The Problem

In some cases, we might have an SVG with some arbitrary shapes or paths in it. Those shapes and paths may have specified dimentsions that don't always fit your viewport.

Let's consider the following SVG. We have two paths that I have taken from a map of the United States. One path is for the state of Maryland and the other is for the state of New York.

<svg style="height: 300px;">
  <path
    d="m 822.9,269.3 0,-1.7 h -.8 l 0,1.8 z m 11.8,-3.9 1.2,-2.2 .1,-2.5 -.6,-.6 -.7,.9 -.2,2.1 -.8,1.4 -.3,1.1 -4.6,1.6 -.7,.8 -1.3,.2 -.4,.9 -1.3,.6 -.3,-2.5 .4,-.7 -.8,-.5 .2,-1.5 -1.6,1 v -2 l 1.2,-.3 -1.9,-.4 -.7,-.8 .4,-1.3 -.8,-.6 -.7,1.6 .5,.8 -.7,.6 -1.1,.5 -2,-1 -.2,-1.2 -1,-1.1 -1.4,-1.7 1.5,-.8 -1,-.6 v -.9 l .6,-1 1.7,-.3 -1.4,-.6 -.1,-.7 -1.3,-.1 -.4,1.1 -.6,.3 .1,-3.4 1,-1 .8,.7 .1,-1.6 -1,-.9 -.9,1.1 -1,1.4 -.6,-1 .2,-2.4 .9,-1 .9,.9 1.2,-.7 -.4,-1.7 -1,1 -.9,-2.1 -.2,-1.7 1.1,-2.4 1.1,-1.4 1.4,-.2 -.5,-.8 .5,-.6 -.3,-.7 .2,-2.1 -1.5,.4 -.8,1.1 1,1.3 -2.6,3.6 -.9,-.4 -.7,.9 -.6,2.2 -1.8,.5 1.3,.6 1.3,1.3 -.2,.7 .9,1.2 -1.1,1 .5,.3 -.5,1.3 v 2.1 l -.5,1.3 .9,1.1 .7,3.4 1.3,1.4 1.6,1.4 .4,2.8 1.6,2 .4,1.4 v 1 h -.7 l -1.5,-1.2 -.4,.2 -1.2,-.2 -1.7,-1.4 -1.4,-.3 -1,.5 -1.2,-.3 -.4,.2 -1.7,-.8 -1,-1 -1,-1.3 -.6,-.2 -.8,.7 -1.6,1.3 -1.1,-.8 -.4,-2.3 .8,-2.1 -.3,-.5 .3,-.4 -.7,-1 1,-.1 1,-.9 .4,-1.8 1.7,-2.6 -2.6,-1.8 -1,1.7 -.6,-.6 h -1 l -.6,-.1 -.4,-.4 .1,-.5 -1.7,-.6 -.8,.3 -1.2,-.1 -.7,-.7 -.5,-.2 -.2,-.7 .6,-.8 v -.9 l -1.2,-.2 -1,-.9 -.9,.1 -1.6,-.3 -.9,-.4 .2,-1.6 -1,-.5 -.2,-.7 h -.7 l -.8,-1.2 .2,-1 -2.6,.4 -2.2,-1.6 -1.4,.3 -.9,1.4 h -1.3 l -1.7,2.9 -3.3,.4 -1.9,-1 -2.6,3.8 -2.2,-.3 -3.1,3.9 -.9,1.6 -1.8,1.6 -1.7,-11.4 60.5,-11.8 7.6,27.1 10.9,-2.3 0,5.3 -.1,3.1 -1,1.8 z m -13.4,-1.8 -1.3,.9 .8,1.8 1.7,.8 -.4,-1.6 z"
  ></path>
  <path
    d="m 872.9,181.6 -1.3,.1 -.5,1 z m -30.6,22.7 .7,.6 1.3,-.3 1.1,.3 .9,-1.3 h 1.9 l 2.4,-.9 5.1,-2.1 -.5,-.5 -1.9,.8 -2,.9 .2,-.8 2.6,-1.1 .8,-1 1.2,.1 4.1,-2.3 v .7 l -4.2,3 4.5,-2.8 1.7,-2.2 1.5,-.1 4.5,-3.1 3.2,-3.1 3,-2.3 1,-1.2 -1.7,-.1 -1,1.2 -.2,.7 -.9,.7 -.8,-1.1 -1.7,1 -.1,.9 -.9,-.2 .5,-.9 -1.2,-.7 -.6,.9 .9,.3 .2,.5 -.3,.5 -1.4,2.6 h -1.9 l .9,-1.8 .9,-.6 .3,-1.7 1.4,-1.6 .9,-.8 1.5,-.7 -1.2,-.2 -.7,.9 h -.7 l -1.1,.8 -.2,1 -2.2,2.1 -.4,.9 -1.4,.9 -7.7,1.9 .2,.9 -.9,.7 -2,.3 -1,-.6 -.2,1.1 -1.1,-.4 .1,1 -1.2,-.1 -1.2,.5 -.2,1.1 h -1 l .2,1 h -.7 l .2,1 -1.8,.4 -1.5,2.3 z m -.8,-.4 -1.6,.4 v 1 l -.7,1.6 .6,.7 2.4,-2.3 -.1,-.9 z m -10.1,-95.2 -.6,1.9 1.4,.9 -.4,1.5 .5,3.2 2.2,2.3 -.4,2.2 .6,2 -.4,1 -.3,3.8 3.1,6.7 -.8,1.8 .9,2.2 .9,-1.6 1.9,1.5 3,14.2 -.5,2 1.1,1 -.5,15 .7,1 2.8,16.3 1.8,1.5 -3.5,3.4 1.7,2.2 -1.3,3.3 -1.5,1.7 -1.5,2.3 -.2,-.7 .4,-5.9 -14.6,-4.9 -1.6,-1.1 -1.9,.3 -3,-2.2 -3,-5.8 h -2 l -.4,-1.5 -1.7,-1.1 -70.5,13.9 -.8,-6 4.3,-3.9 .6,-1.7 3.9,-2.5 .6,-2.4 2.3,-2 .8,-1.1 -1.7,-3.3 -1.7,-.5 -1.8,-3 -.2,-3.2 7.6,-3.9 8.2,-1.6 h 4.4 l 3.2,1.6 .9,-.1 1.8,-1.6 3.4,-.7 h 3 l 2.6,-1.3 2.5,-2.6 2.4,-3.1 1.9,-.4 1.1,-.5 .4,-3.2 -1.4,-2.7 -1.2,-.7 2,-1.3 -.1,-1.8 h -1.5 l -2.3,-1.4 -.1,-3.1 6.2,-6.1 .7,-2.4 3.7,-6.3 5.9,-6.4 2.1,-1.7 2.5,.1 20.6,-5.2 z"
  ></path>
</svg>
Enter fullscreen mode Exit fullscreen mode

What we might expect to see is this:

new york and maryland

But if we were to fire up our web browser and navigate to this page, we wouldn't see a thing. Why is that? Well, it's because these paths were taken from a full map of the U.S., so the origin (0, 0) point of the coordinate system being used is at the top-left of the entire countryβ€”next to Alaska.

This is no good as want our SVG to fit our two states perfectly.

Our Options

We have a couple reasonable options:

  1. Transform each path such that it fits our current SVG view
  2. Change our SVG "viewBox" to fit our paths

I personally like the second option and will go through that one in this post!

First, an Abstaction and a Tiny Bit of Math

Let's back off the states example for a moment and just use a couple arbitrary paths in an SVG:

a diamond and a circle on a canvas

We can see that our SVG is 100x100, but our shapes actually only start at about x = 40, y = 30 and end somewhew around x = 80, y = 90. What we'd love is for our SVG to zoom in on that area, so our view looks something like this:

zoomed in on diamond and circle

How can we do this? It turns out the svg HTML element has a handy zoomBox attribute that lets us pass the desired origin's x and y values along with the desired width and height.

In the case of our shapes, we have the following:

box drawn around shapes

We can see that our origin is at x=40, y=30 and then we have to do a little math for our width and height:

width = xMax - xMin = 80 - 40 = 40
height = yMax - yMin = 90 - 30 = 60
Enter fullscreen mode Exit fullscreen mode

Therefore, we might specify the following viewBox for our SVG.

<svg viewBox="40 30 40 60">
  <!-- Shapes here -->
</svg>
Enter fullscreen mode Exit fullscreen mode

And this works! Note that, by default, the SVG will center the objects in its viewBox rather than distorting them.

That Seems Tedious

Yes, it would be super tedious to do this math any time we want to use SVGs, and seems nearly impossible if we dynamic SVG children or many children.

Luckily, we're programmers! We enjoy automating things, so let's automate the process we just did in our heads.

Automatically Finding the Boundary

The way I like to automatically find the boundary is to iterate through all children of the svg. I use a native svg method called getBBox() that will return the x, y, width, and height of an element. Then, just some simple comparison: if the x is lower than all the other x values we have seen so far, it's the new xMin. Same with y. To get xMax and yMax, we do a similar operation, except we have to make sure we're looking at x + width and y + height for each element since x and y only give us the top-left point of the element and we want the bottom-right.

Here's how this code might look:

const svg = document.querySelector('svg');

const { xMin, xMax, yMin, yMax } = [...svg.children].reduce((acc, el) => {
  const { x, y, width, height } = el.getBBox();
  if (!acc.xMin || x < acc.xMin) acc.xMin = x;
  if (!acc.xMax || x + width > acc.xMax) acc.xMax = x + width;
  if (!acc.yMin || y < acc.yMin) acc.yMin = y;
  if (!acc.yMax || y + height > acc.yMax) acc.yMax = y + height;
  return acc;
}, {});
Enter fullscreen mode Exit fullscreen mode

We use document.querySelector('svg') to grab the only SVG on our page (but make sure to use an ID or class if you need a different selector!). Next, I use [...svg.children] to (a) get all of the child elements of our svg and (b) use the spread operator (...) to convert the HTMLCollection to an array of elements. Have an array enables me to use the reduce method, which takes a callback function and an initial accumulator (an empty object {}).

Within the reduce callback, I use the getBBox() method on each element to get the x, y, width, and height. Using the aformentioned logic, we conditionally overwrite the xMin, xMax, yMin, and yMax, properties on our accumulator.

Our one final step is to set he viewBox attribute of the parent svg. Remember, the viewBox is set to x y width height, so we have to convert our xMax and yMax to width and height, respectively!

We can do this with the math we discussed in our simplified example:

width = xMax - xMin
height = yMax - yMin
Enter fullscreen mode Exit fullscreen mode

Let's put it all together here:

const svg = document.querySelector('svg');

const { xMin, xMax, yMin, yMax } = [...svg.children].reduce((acc, el) => {
  const { x, y, width, height } = el.getBBox();
  if (!acc.xMin || x < acc.xMin) acc.xMin = x;
  if (!acc.xMax || x + width > acc.xMax) acc.xMax = x + width;
  if (!acc.yMin || y < acc.yMin) acc.yMin = y;
  if (!acc.yMax || y + height > acc.yMax) acc.yMax = y + height;
  return acc;
}, {});

const viewbox = `${xMin} ${yMin} ${xMax - xMin} ${yMax - yMin}`;

svg.setAttribute('viewBox', viewbox);
Enter fullscreen mode Exit fullscreen mode

And we can see it in action with our state SVGs. In fact, we touted the flexibility of this solution, so it better be able to accommodate an additional state! Let's add North Carolina for good measure.

Conclusion

Thanks for playing with shapes, states, and even a little math with me today. Hopefully you learned something new today and how have a handy utility to fit items in your SVGs.


Please give this post a πŸ’“, πŸ¦„, and πŸ”– if you learned a little something about SVGs!

πŸ’– πŸ’ͺ πŸ™… 🚩
nas5w
Nick Scialli (he/him)

Posted on January 19, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related