206

I'm drawing a scatterplot with d3.js. With the help of this question :
Get the size of the screen, current web page and browser window

I'm using this answer :

var w = window,
    d = document,
    e = d.documentElement,
    g = d.getElementsByTagName('body')[0],
    x = w.innerWidth || e.clientWidth || g.clientWidth,
    y = w.innerHeight|| e.clientHeight|| g.clientHeight;

So I'm able to fit my plot to the user's window like this :

var svg = d3.select("body").append("svg")
        .attr("width", x)
        .attr("height", y)
        .append("g");

Now I'd like that something takes care of resizing the plot when the user resize the window.

PS : I'm not using jQuery in my code.

Community
  • 1
  • 1
mthpvg
  • 3,789
  • 3
  • 26
  • 34
  • 1
    Possible duplicate of [Whats the best way to make a d3.js visualisation layout responsive?](http://stackoverflow.com/questions/9400615/whats-the-best-way-to-make-a-d3-js-visualisation-layout-responsive) – ColinE Jun 26 '16 at 18:01

7 Answers7

332

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")
   // Container class to make it responsive.
   .classed("svg-container", true) 
   .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)
   // Fill with a rectangle for visualization.
   .append("rect")
   .classed("rect", true)
   .attr("width", 600)
   .attr("height", 400);
.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;
}

svg .rect {
  fill: gold;
  stroke: steelblue;
  stroke-width: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

<div id="chartId"></div>

Note: Everything in the SVG image will scale with the window width. This includes stroke width and font sizes (even those set with CSS). If this is not desired, there are more involved alternate solutions below.

More info / tutorials:

http://thenewcode.com/744/Make-SVG-Responsive

http://soqr.fr/testsvg/embed-svg-liquid-layout-responsive-web-design.php

Rúnar Berg
  • 4,229
  • 1
  • 22
  • 38
cminatti
  • 4,596
  • 2
  • 19
  • 8
  • 8
    This is much more elegant and also uses a container scaling approach that should be in every front-end developer's toolkit (can be used on scaling anything in a particular aspect ratio). Should be the top answer. – kontur Jun 02 '15 at 09:36
  • 14
    Just to add to this: The `viewBox` attribute should be set to the relevant height and width of your SVG plot: `.attr("viewBox","0 0 " + width + " " + height)` where `width` and `height` are defined elsewhere. That is, unless you want your SVG clipped by the view box. You might also want to update the `padding-bottom` parameter in your css to match the aspect ratio of your svg element. Excellent response though - very helpful, and works a treat. Thanks! – Andrew Guy Jun 18 '15 at 04:24
  • I find this way easier than redrawing the SVG every time. You lose some of the fine tuning you may want on a window resize event, but this is so much easier to implement and maintain. – aboutaaron Jan 22 '16 at 03:16
  • 17
    Based on the same idea, it would be interesting to give an answer that doesn't involve preserving the aspect ratio. The question was about having an svg element that keeps covering the full window on resize, which in most cases means a different aspect ratio. – Nicolas Le Thierry d'Ennequin Feb 12 '16 at 13:32
  • This is amazing, all the other websites such as: http://getbootstrap.com/getting-started/ http://jsfiddle.net/shawnbot/BJLe6/ http://eyeseast.github.io/visible-data/2013/08/28/responsive-charts-with-d3/ contain rubbish solutions that can't possibly work. (mainly due to the fact that they are trying to work on scaled versions of the width/height). In all the above mentioned websites the canvas changes dynamically but the area plots never change (thats with no mention of static pixel sizes in the CSS). – Eamonn Kenny Mar 15 '16 at 11:08
  • HI @cminatti. When i am trying to use the code above my ticks are not getting displayed . Can you explain how to adjust the viewbox and Aspect Ratio to fit it in correctly. Following is the jsfiddle in which it works but the plots become really small when i integrate this in a React Page. https://jsfiddle.net/adityap16/11edxrnq/1/ – Aditya Patel Apr 05 '16 at 12:25
  • 45
    Just a note regarding UX: For some use cases, treating your SVG-based d3 chart like an asset with a fixed aspect ratio isn't ideal. Charts which display text, or are dynamic, or in general feel more like a proper User Interface than an asset, have more to gain by re-rendering with updated dimensions. For example, viewBox forces axis fonts and ticks to scale up/down, potentially looking awkward compared to html text. In contrast, a re-render enables the chart to keep it's labeling a consistent size, plus it provides an opportunity to add or remove ticks. – Karim Hernandez Apr 30 '16 at 09:54
  • 1
    Why `top: 10px;`? Seems incorrect for me and provides offset. Putting to 0px works fine. – TimZaman Aug 23 '16 at 13:26
  • Not sure why the `padding-bottom:100%` is there. It created a mega margin for me down under. I changed it to 50% and now it's perfect. Thanks. – ow3n Dec 06 '16 at 11:45
  • 1
    How could this code be combined with zooming and dezooming (at svg dimensions fixed) with mousewheel, please? – Myoch Jan 04 '17 at 11:33
  • Cannot do this, if the container is hidden. I made a D3JS graph in a Bootstrap tab that is not active when the page loads. I get this error, `Uncaught DOMException: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The source width is 0.`. – notalentgeek Apr 18 '17 at 08:14
  • Can you explain where the 3 different sections of your code go in terms of an actual working example. Suppose I have an .html file containing the main body tags as well as a script tag to a file containing my .js -> I html file running my 1 js file. I have in my .js file all the d3j3 stuff. Do the style guides go in the html above the body, and the svg inside my .js file? The reason I ask is that I did just that but my previously working script now doesn't display anything – brian_ds Aug 06 '19 at 20:55
  • @Karim Hernandez makes a good point here, but I would avoid re-rendering in a multi line graph because it will be incredibly glitchy. To counter this I Scaled and translated my main graphic element by how much the window size had changed, then updated my clipped graphic width and height to the new svg width and height after window resize, and all my texts and lines maintained size. Don't go scaling everything individually because it will take ages and is unnecessary. – sean le roy Sep 05 '19 at 11:52
  • In you want the svg to be centered horizontally in the parent div then you need to use `xMidYMin` instead of `xMinYMin`. – Bemipefe Oct 29 '20 at 17:17
  • to calculate the correct padding-bottom im using this: ```.style('padding-bottom', (100 / aspect) + '%')``` on my appended div.svg-container instead of defining it on the css class. also, aspect is ```width / height``` ofc (but im including the margins as well). so in my case it is ```aspect = (width + margin.left + margin.right) / (height + margin.bottom + margin.top)```. – hawaii Dec 06 '21 at 00:10
48

Use window.onresize:

function updateWindow(){
    x = w.innerWidth || e.clientWidth || g.clientWidth;
    y = w.innerHeight|| e.clientHeight|| g.clientHeight;

    svg.attr("width", x).attr("height", y);
}
d3.select(window).on('resize.updatesvg', updateWindow);

http://jsfiddle.net/Zb85u/1/

Adam Pearce
  • 9,243
  • 2
  • 38
  • 35
  • 13
    This isn't a good answer since it involves the biding to DOM events... A much better solution is using only d3 and css – alem0lars Mar 23 '15 at 08:40
  • 2
    @alem0lars funnily enough, the css/d3 version didn't work for me and this one did... – Christian Apr 14 '18 at 11:01
  • 2
    I know it appears in the JSFiddle but it is quite confusing to see `w`, `e`, `g` comming from nowhere and `x`, `y` being initialized without any keywords in your answer. – Leogout Nov 03 '20 at 13:16
34

UPDATE just use the new way from @cminatti


old answer for historic purposes

IMO it's better to use select() and on() since that way you can have multiple resize event handlers... just don't get too crazy

d3.select(window).on('resize', resize); 

function resize() {
    // update width
    width = parseInt(d3.select('#chart').style('width'), 10);
    width = width - margin.left - margin.right;

    // resize the chart
    x.range([0, width]);
    d3.select(chart.node().parentNode)
        .style('height', (y.rangeExtent()[1] + margin.top + margin.bottom) + 'px')
        .style('width', (width + margin.left + margin.right) + 'px');

    chart.selectAll('rect.background')
        .attr('width', width);

    chart.selectAll('rect.percent')
        .attr('width', function(d) { return x(d.percent); });

    // update median ticks
    var median = d3.median(chart.selectAll('.bar').data(), 
        function(d) { return d.percent; });

    chart.selectAll('line.median')
        .attr('x1', x(median))
        .attr('x2', x(median));


    // update axes
    chart.select('.x.axis.top').call(xAxis.orient('top'));
    chart.select('.x.axis.bottom').call(xAxis.orient('bottom'));

}

http://eyeseast.github.io/visible-data/2013/08/28/responsive-charts-with-d3/

slf
  • 22,595
  • 11
  • 77
  • 101
  • I can't agree less. The method of cminatti will work on all d3js files that we deal with. This method of transforming with a resize per chart is overkill. Also, it needs to be rewritten for every possible chart that we could think up. The method mentioned above works for everything. I've tried 4 recipes including the type of reload you have mentioned and none of them work for multiple area plots such the type used in cubism. – Eamonn Kenny Mar 15 '16 at 11:11
  • 1
    @EamonnKenny I can't agree with you more. The other answer 7 months in the future is superior :) – slf Mar 15 '16 at 16:43
  • For this method: "d3.select(window).on" I could not find "d3.select(window).off" or "d3.select(window).unbind" – sea-kg Apr 27 '16 at 10:17
  • 1
    @sea-kg http://stackoverflow.com/questions/20269384/how-do-you-remove-a-handler-using-a-d3-js-selector – slf Apr 27 '16 at 16:55
  • 2
    This answer is by no means inferior. Their answer will scale every aspect of the graph (including ticks, axis, lines, and text annotations) which can look awkward, in some screen sizes, bad in others and unreadable in extreme sizes. I find it better to resize using this (or, for a more modern solutions, @Angu Agarwal) method. – Rúnar Berg Jun 07 '19 at 03:16
12

It's kind of ugly if the resizing code is almost as long as the code for building the graph in first place. So instead of resizing every element of the existing chart, why not simply reloading it? Here is how it worked for me:

function data_display(data){
   e = document.getElementById('data-div');
   var w = e.clientWidth;
   // remove old svg if any -- otherwise resizing adds a second one
   d3.select('svg').remove();
   // create canvas
   var svg = d3.select('#data-div').append('svg')
                                   .attr('height', 100)
                                   .attr('width', w);
   // now add lots of beautiful elements to your graph
   // ...
}

data_display(my_data); // call on page load

window.addEventListener('resize', function(event){
    data_display(my_data); // just call it again...
}

The crucial line is d3.select('svg').remove();. Otherwise each resizing will add another SVG element below the previous one.

Raik
  • 355
  • 2
  • 9
  • This will absolutely kill performance if you're redrawing the entire element on every window resize event – sdemurjian Aug 25 '16 at 17:01
  • 2
    But this is correct: As you render you can determine if you should have inset axis, hide a legend, do other things based on the available content. Using a purely CSS/viewbox solution, all it does is makes the visualziation look squished. Good for images, bad for data. – Chris Knoll May 26 '18 at 01:04
8

In force layouts simply setting the 'height' and 'width' attributes will not work to re-center/move the plot into the svg container. However, there's a very simple answer that works for Force Layouts found here. In summary:

Use same (any) eventing you like.

window.on('resize', resize);

Then assuming you have svg & force variables:

var svg = /* D3 Code */;
var force = /* D3 Code */;    

function resize(e){
    // get width/height with container selector (body also works)
    // or use other method of calculating desired values
    var width = $('#myselector').width(); 
    var height = $('#myselector').height(); 

    // set attrs and 'resume' force 
    svg.attr('width', width);
    svg.attr('height', height);
    force.size([width, height]).resume();
}

In this way, you don't re-render the graph entirely, we set the attributes and d3 re-calculates things as necessary. This at least works when you use a point of gravity. I'm not sure if that's a prerequisite for this solution. Can anyone confirm or deny ?

Cheers, g

gavs
  • 266
  • 3
  • 6
3

If you want to bind custom logic to resize event, nowadays you may start using ResizeObserver browser API for the bounding box of an SVGElement.
This will also handle the case when container is resized because of the nearby elements size change.
There is a polyfill for broader browser support.

This is how it may work in UI component:

function redrawGraph(container, { width, height }) {
  d3
    .select(container)
    .select('svg')
    .attr('height', height)
    .attr('width', width)
    .select('rect')
    .attr('height', height)
    .attr('width', width);
}

// Setup observer in constructor
const resizeObserver = new ResizeObserver((entries, observer) => {
  for (const entry of entries) {
    // on resize logic specific to this component
    redrawGraph(entry.target, entry.contentRect);
  }
})

// Observe the container
const container = document.querySelector('.graph-container');
resizeObserver.observe(container)
.graph-container {
  height: 75vh;
  width: 75vw;
}

.graph-container svg rect {
  fill: gold;
  stroke: steelblue;
  stroke-width: 3px;
}
<script src="https://unpkg.com/resize-observer-polyfill@1.5.1/dist/ResizeObserver.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

<figure class="graph-container">
  <svg width="100" height="100">
    <rect x="0" y="0" width="100" height="100" />
  </svg>
</figure>
// unobserve in component destroy method
this.resizeObserver.disconnect()
Rúnar Berg
  • 4,229
  • 1
  • 22
  • 38
Anbu Agarwal
  • 491
  • 2
  • 10
1

For those using force directed graphs in D3 v4/v5, the size method doesn't exist any more. Something like the following worked for me (based on this github issue):

simulation
    .force("center", d3.forceCenter(width / 2, height / 2))
    .force("x", d3.forceX(width / 2))
    .force("y", d3.forceY(height / 2))
    .alpha(0.1).restart();
Justin Lewis
  • 1,261
  • 1
  • 15
  • 33