What's the best way to make a d3.js visualisation layout responsive?
Assume I have a histogram script that builds a 960 500 svg graphic. How do I make this responsive so on resize the graphic widths and heights are dynamic?
<script>
var n = 10000, // number of trials
m = 10, // number of random variables
data = [];
// Generate an Irwin-Hall distribution.
for (var i = 0; i < n; i++) {
for (var s = 0, j = 0; j < m; j++) {
s += Math.random();
}
data.push(s);
}
var histogram = d3.layout.histogram()
(data);
var width = 960,
height = 500;
var x = d3.scale.ordinal()
.domain(histogram.map(function(d) { return d.x; }))
.rangeRoundBands([0, width]);
var y = d3.scale.linear()
.domain([0, d3.max(histogram.map(function(d) { return d.y; }))])
.range([0, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.selectAll("rect")
.data(histogram)
.enter().append("rect")
.attr("width", x.rangeBand())
.attr("x", function(d) { return x(d.x); })
.attr("y", function(d) { return height - y(d.y); })
.attr("height", function(d) { return y(d.y); });
svg.append("line")
.attr("x1", 0)
.attr("x2", width)
.attr("y1", height)
.attr("y2", height);
</script>
Full example histogram gist is: https://gist.github.com/993912
There's another way to do this that doesn't require redrawing the graph, and it involves modifying the viewBox and preserveAspectRatio attributes on the <svg>
element:
<svg id="chart" viewBox="0 0 960 500"
preserveAspectRatio="xMidYMid meet">
</svg>
Update 11/24/15: most modern browsers can infer the aspect ratio of SVG elements from the viewBox
, so you may not need to keep the chart's size up to date. If you need to support older browsers, you can resize your element when the window resizes like so:
var aspect = width / height,
chart = d3.select('#chart');
d3.select(window)
.on("resize", function() {
var targetWidth = chart.node().getBoundingClientRect().width;
chart.attr("width", targetWidth);
chart.attr("height", targetWidth / aspect);
});
And the svg contents will be scaled automatically. You can see a working example of this (with some modifications) here: just resize the window or the bottom right pane to see how it reacts.
Look for 'responsive SVG' it is pretty simple to make a SVG responsive and you don't have to worry about sizes any more.
Here is how I did it:
d3.select("div#chartId")
.append("div")
.classed("svg-container", true) //container class to make it responsive
.append("svg")
//responsive SVG needs these 2 attributes and no width and height attr
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "0 0 600 400")
//class to make it responsive
.classed("svg-content-responsive", true);
The CSS code:
.svg-container {
display: inline-block;
position: relative;
width: 100%;
padding-bottom: 100%; /* aspect ratio */
vertical-align: top;
overflow: hidden;
}
.svg-content-responsive {
display: inline-block;
position: absolute;
top: 10px;
left: 0;
}
More info / tutorials:
http://demosthenes.info/blog/744/Make-SVG-Responsive
http://soqr.fr/testsvg/embed-svg-liquid-layout-responsive-web-design.php
I've coded up a small gist to solve this.
The general solution pattern is this:
- Breakout the script into computation and drawing functions.
- Ensure the drawing function draws dynamically and is driven of visualisation width and height variables (The best way to do this is to use the d3.scale api)
- Bind/chain the drawing to a reference element in the markup. (I used jquery for this, so imported it).
- Remember to remove it if it's already drawn. Get the dimensions from the referenced element using jquery.
- Bind/chain the draw function to the window resize function. Introduce a debounce (timeout) to this chain to ensure we only redraw after a timeout.
I also added the minified d3.js script for speed. The gist is here: https://gist.github.com/2414111
jquery reference back code:
$(reference).empty()
var width = $(reference).width();
Debounce code:
var debounce = function(fn, timeout)
{
var timeoutID = -1;
return function() {
if (timeoutID > -1) {
window.clearTimeout(timeoutID);
}
timeoutID = window.setTimeout(fn, timeout);
}
};
var debounced_draw = debounce(function() {
draw_histogram(div_name, pos_data, neg_data);
}, 125);
$(window).resize(debounced_draw);
Enjoy!