One of my favorite no-so-secret weapons for data visualization on the web is the SVG viewBox
attribute. I've written about it before: in my textbook, I have a whole chapter on art direction with viewBox, and I also talk about it in the section on rendering SVG from JS.
Essentially, the viewBox
attribute lets you set up the... view box. SVG works on a 2d coordinate system that can (but doesn't have to) match the CSS positioning grid for an element. You can use the attribute to tell the SVG to set an offset for the rendering canvas, to zoom in or out, or to crop the image to keep an area in view while still fitting in an arbitrary aspect ratio (similar to how object-fit
and object-position
work in CSS, but more granular).
But on a recent project, I also had a chance to use viewBox
as a literal view box: for a Votebeat story on ballot readability, we generated cropped comparison images of ballots to show the relative size of specific page sections, with a little minimap to give the full context. Instead of pre-rendering the graphics, or using JavaScript to generate the thumbnails, we did it all in SVG.
Here's how it works. In our template code, we define a data structure for a crop and any number of highlighted areas (we use EJS templating, so the data structure is basically a JSON object that our template can apply):
{
title: "Pinal",
image: "./assets/pinal-crop.jpg",
height: 1776,
viewBox: [0, 0, 800, 600],
highlights: [
[32, 90, 736, 210]
]
}
The viewBox
and highlights
are both just x/y/width/height quads, the way we would define any SVG rectangle. So rendering the crop is pretty easy. Here's the EJS template:
<svg
class="main"
viewBox="<%= config.viewBox.join(" ") %>"
>
<image href="<%= config.image %>" x="0" y="0" width="800" height="<%= config.height %>" />
<% for (var [x,y,w,h] of config.highlights) { %>
<rect
class="highlight"
x="<%= x %>"
y="<%= y %>"
width="<%= w %>"
height="<%= h %>"
vector-effect="non-scaling-stroke"
/>
<% } %>
</svg>
And the resulting HTML:
<svg class="main" viewBox="0 0 800 600">
<image href="./assets/pinal-crop.jpg" x="0" y="0" width="800" height="1776"></image>
<rect class="highlight" x="32" y="90" width="736" height="210" vector-effect="non-scaling-stroke"></rect>
</svg>
(Unfortunately, GitHub doesn't render SVG inline in Markdown files. But you can drop this code into an HTMl file to try it.)
Since the viewBox
will act as the width/height of the SVG in the absence of explicit attributes or styles, this has the effect of creating a cropped image in the right aspect ratio to fit that viewBox pretty much exactly. If we wanted to tweak that, we could change the preserveAspectRatio
, but depending on its value, we'd have to deal with a possible mismatch with the minimap view.
Now, to generate the minimap, we use the same data, and pretty much the same template. The only difference is that this time, we render the view box as a rectangle, and set the viewBox
of the minimap SVG to show the whole page:
<svg viewBox="0 0 800 1600"
width="800" height="1600"
preserveAspectRatio="xMidYMid slice">
<image href="<%= config.image %>" width="800" height="<%= config.height %>" />
<rect
<% var [x, y, w, h] = config.viewBox; %>
x="<%= x %>"
y="<%= y %>"
width="<%= w %>"
height="<%= h %>"
class="locator"
vector-effect="non-scaling-stroke"
/>
<g class="highlights">
<% for (var [x,y,w,h] of config.highlights) { %>
<rect
class="highlight"
x="<%= x %>"
y="<%= y %>"
width="<%= w %>"
height="<%= h %>"
vector-effect="non-scaling-stroke"
/>
<% } %>
</g>
</svg>
Resulting in:
<svg viewBox="0 0 800 1600" width="800" height="1600" preserveAspectRatio="xMidYMid slice">
<image href="./assets/pinal-crop.jpg" width="800" height="1776"></image>
<rect x="0" y="0" width="800" height="600" class="locator" vector-effect="non-scaling-stroke"></rect>
<g class="highlights">
<rect class="highlight" x="32" y="90" width="736" height="210" vector-effect="non-scaling-stroke"></rect>
</g>
</svg>
The .locator
rectangle is our view box, visualized in gray. Since our highlights are still in the same coordinate system (we've just zoomed out from the crop by setting the viewBox
attribute to full width), we don't need to reposition them at all--they'll show up inside the locator, looking pretty much just like the cropped version does. Using CSS, we can make this SVG smaller and place it in one of the lower corners of the original image so that readers can orient themselves.
To me, this captures the tremendous power of learning SVG basics. Sure, you could do something similar in JavaScript, or draw it yourself in Illustrator or Inkscape. But leveraging viewBox
means that we can create the same effect in only a few bytes, without having to work out the positioning/scaling math ourselves. It's all just different views of the same scaled, Cartesian grid.