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>
What we might expect to see is this:
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:
- Transform each path such that it fits our current SVG view
- 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:
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:
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:
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
Therefore, we might specify the following viewBox
for our SVG.
<svg viewBox="40 30 40 60">
<!-- Shapes here -->
</svg>
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;
}, {});
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
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);
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.
Top comments (0)