Making responsive SVG graphs

One of the (many) nice things about Twitter’s Bootstrap is its responsive layout. I’m using it for a new project alongside some D3, and I’d really like the graphs to resize with the grid. Unfortunately SVG elements don’t seem particularly amenable to this, and all the D3 demos I’ve seen have had a fixed width and height.

Building a responsive graph

A responsive SVG graph should behave like any other styled element

  • Expand to fill the containing element (if required)
  • Keep line widths and text the same size and prevent distortion, no matter how big the graph becomes

SVG does allow percentages as a unit, but I haven’t personally seen them used much (at all, actually) and D3 uses px for its ranges.

Luckily SVG elements can also define their own viewports through the viewBox attribute, so its contents will always fill the same portion of the viewport, no matter how big or small to the outer element becomes (or whatever other transformations are applied to it).

W3C SVG viewBox example

(A related attribute is preserveAspectRatio, which I’ve set to none as I want the graph to fill the whole space. There are other options to force uniform scaling if you want your graph to maintain a fixed aspect ratio.)

<svg viewBox="0 0 1000 1000" preserveAspectRatio="none">
<g>
<!-- graph here -->
</g>
</svg>

This demo graph resizes nicely, but the text and line width can get horribly distorted – not really what I wanted.

Preventing the distortions

Graphical SVG elements support the vector-effect attribute, and setting this to non-scaling-stroke stops the lines from being affected by any transforms that have been applied. Unfortunately there isn’t an equivalent for text, but this JavaScript snippet from Gavin Kistner can help here by undoing the damage. Just set it to run on load and hook it up to fire when the window resizes as well.

(I’ve actually changed the code slightly as it uses getTransformToElement to, um, get the transform from the containing SVG element to its contents, but the transform matrix returned by the function doesn’t take into account transforms applied to the SVG element itself. So I’ve put an SVG element (with a custom viewBox) inside the outer SVG element and referenced that in the JS instead.)

Putting it all together

Demo graph

<svg>
<svg viewBox="0 0 1000 1000" preserveAspectRatio="none">
<g>
<!-- graph here -->
</g>
</svg>
</svg>
.line {
  fill: none;
  stroke-width: 3px;
  vector-effect: non-scaling-stroke;
}
.area {
  vector-effect: non-scaling-stroke;
}
$(function () {
	"use strict";
	var resizeTracker;
	// Counteracts all transforms applied above an element.
	// Apply a translation to the element to have it remain at a local position
	var unscale = function (el) {
		var svg = el.ownerSVGElement.ownerSVGElement;
		var xf = el.scaleIndependentXForm;
		if (!xf) {
			// Keep a single transform matrix in the stack for fighting transformations
			xf = el.scaleIndependentXForm = svg.createSVGTransform();
			// Be sure to apply this transform after existing transforms (translate)
			el.transform.baseVal.appendItem(xf);
		}
		var m = svg.getTransformToElement(el.parentNode);
		m.e = m.f = 0; // Ignore (preserve) any translations done up to this point
		xf.setMatrix(m);
	};
	[].forEach.call($("text, .tick"), unscale);
	$(window).resize(function () {
		if (resizeTracker) clearTimeout(resizeTracker);
		resizeTracker = setTimeout(function () { [].forEach.call($("text, .tick"), unscale); }, 100);
	});
});

Notes

There’s also transform ref() that applies transforms to an element relative to a specified SVG element rather than its parent. This would be ideal as I could ditch the JS, but unfortunately Opera seems to be the only browser that supports it at the moment.

If you want sharper lines you can disable antialiasing with shape-rendering=optimizeSpeed or shape-rendering=crispEdges.

Two useful StackOverflow questions on this.

The demo doesn’t seem to work with Opera or IE. I may look into it at some point.

Let me know what you think

Your email address will not be published. Required fields are marked *