What’s the best way to make a d3.js visualisation layout responsive?

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?

Full example histogram gist is:
https://gist.github.com/993912

Solutions/Answers:

Solution 1:

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" width="960" height="500"
  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.

Solution 2:

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

Solution 3:

I've coded up a small gist to solve this.

The general solution pattern is this:

  1. Breakout the script into computation and drawing functions.
  2. 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)
  3. Bind/chain the drawing to a reference
    element in the markup. (I used jquery for this, so imported it).
  4. Remember to remove it if it's already drawn. Get the dimensions from
    the referenced element using jquery.
  5. 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!

Solution 4:

Without Using ViewBox

Here is an example of a solution that does not rely on using a viewBox:

The key is in updating the range of the scales which are used to place data.

First, calculate your original aspect ratio:

var ratio = width / height;

Then, on each resize, update the range of x and y:

function resize() {
  x.rangeRoundBands([0, window.innerWidth]);
  y.range([0, window.innerWidth / ratio]);
  svg.attr("height", window.innerHeight);
}

Note that the height is based on the width and the aspect ratio, so that your original proportions are maintained.

Finally, "redraw" the chart – update any attribute that depends on either of the x or y scales:

function redraw() {
    rects.attr("width", x.rangeBand())
      .attr("x", function(d) { return x(d.x); })
      .attr("y", function(d) { return y.range()[1] - y(d.y); })
      .attr("height", function(d) { return y(d.y); });
}

Note that in re-sizing the rects you can use the upper-bound of the range of y, rather than explicitly using the height:

.attr("y", function(d) { return y.range()[1] - y(d.y); })
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 ratio = width / height;

var x = d3.scale.ordinal()
  .domain(histogram.map(function(d) {
    return d.x;
  }))

var y = d3.scale.linear()
  .domain([0, d3.max(histogram, function(d) {
    return d.y;
  })])

var svg = d3.select("body").append("svg")
  .attr("width", "100%")
  .attr("height", height);

var rects = svg.selectAll("rect").data(histogram);
rects.enter().append("rect");

function redraw() {
  rects.attr("width", x.rangeBand())
    .attr("x", function(d) {
      return x(d.x);
    })
    // .attr("y", function(d) { return height - y(d.y); })
    .attr("y", function(d) {
      return y.range()[1] - y(d.y);
    })
    .attr("height", function(d) {
      return y(d.y);
    });
}

function resize() {
  x.rangeRoundBands([0, window.innerWidth]);
  y.range([0, window.innerWidth / ratio]);
  svg.attr("height", window.innerHeight);
}

d3.select(window).on('resize', function() {
  resize();
  redraw();
})

resize();
redraw();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Solution 5:

If you are using d3.js through c3.js the solution to the responsiveness issue is quite straightforward :

var chart = c3.generate({bindTo:"#chart",...});
chart.resize($("#chart").width(),$("#chart").height());

where the generated HTML looks like :

<div id="chart">
    <svg>...</svg>
</div>

Solution 6:

Shawn Allen's answer was great. But you may not want to do this every single time. If you host it on vida.io, you get automatic responsive for your svg visualization.

You can get responsive iframe with this simple embed code:

<div id="vida-embed">
<iframe src="http://embed.vida.io/documents/9Pst6wmB83BgRZXgx" width="auto" height="525" seamless frameBorder="0" scrolling="no"></iframe>
</div>

#vida-embed iframe {
  position: absolute;
  top:0;
  left: 0;
  width: 100%;
  height: 100%;
}

http://jsfiddle.net/dnprock/npxp3v9d/1/

Disclosure: I build this feature at vida.io.

Related:  Turning live() into on() in jQuery