[javascript] Resize svg when window is resized in d3.js

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.

This question is related to javascript d3.js

The answer is


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/


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/


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.


_x000D_
_x000D_
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);
_x000D_
.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;
}
_x000D_
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

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


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:

_x000D_
_x000D_
function redrawGraph(container, { width, height }) {_x000D_
  d3_x000D_
    .select(container)_x000D_
    .select('svg')_x000D_
    .attr('height', height)_x000D_
    .attr('width', width)_x000D_
    .select('rect')_x000D_
    .attr('height', height)_x000D_
    .attr('width', width);_x000D_
}_x000D_
_x000D_
// Setup observer in constructor_x000D_
const resizeObserver = new ResizeObserver((entries, observer) => {_x000D_
  for (const entry of entries) {_x000D_
    // on resize logic specific to this component_x000D_
    redrawGraph(entry.target, entry.contentRect);_x000D_
  }_x000D_
})_x000D_
_x000D_
// Observe the container_x000D_
const container = document.querySelector('.graph-container');_x000D_
resizeObserver.observe(container)
_x000D_
.graph-container {_x000D_
  height: 75vh;_x000D_
  width: 75vw;_x000D_
}_x000D_
_x000D_
.graph-container svg rect {_x000D_
  fill: gold;_x000D_
  stroke: steelblue;_x000D_
  stroke-width: 3px;_x000D_
}
_x000D_
<script src="https://unpkg.com/[email protected]/dist/ResizeObserver.js"></script>_x000D_
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>_x000D_
_x000D_
<figure class="graph-container">_x000D_
  <svg width="100" height="100">_x000D_
    <rect x="0" y="0" width="100" height="100" />_x000D_
  </svg>_x000D_
</figure>
_x000D_
_x000D_
_x000D_

// unobserve in component destroy method
this.resizeObserver.disconnect()

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


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();