Redimensione svg quando a janela for redimensionada em d3.js

183

Estou desenhando um gráfico de dispersão com d3.js. Com a ajuda desta pergunta:
Obtenha o tamanho da tela, página da web atual e janela do navegador

Estou usando esta resposta:

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;

Então, eu consigo encaixar minha plotagem na janela do usuário assim:

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

Agora, eu gostaria que algo resolvesse redimensionar o gráfico quando o usuário redimensionar a janela.

PS: Não estou usando jQuery no meu código.

mthpvg
fonte

Respostas:

295

Procure por 'SVG responsivo'. É muito simples fazer um SVG responsivo e você não precisa mais se preocupar com tamanhos.

Aqui está como eu fiz isso:

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>

Nota: Tudo na imagem SVG será dimensionado com a largura da janela. Isso inclui a largura do traçado e os tamanhos das fontes (mesmo as definidas com CSS). Se isso não for desejado, há mais soluções alternativas envolvidas abaixo.

Mais informações / tutoriais:

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

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

cminatti
fonte
8
Isso é muito mais elegante e também usa uma abordagem de dimensionamento de contêiner que deve estar no kit de ferramentas de todos os desenvolvedores de front-end (pode ser usado no dimensionamento de qualquer coisa em uma proporção específica). Deve ser a melhor resposta.
kontur
13
Apenas para adicionar isso: O viewBoxatributo deve ser definido para a altura e largura relevantes do seu gráfico SVG: .attr("viewBox","0 0 " + width + " " + height) onde widthe heightsão definidos em outro lugar. Ou seja, a menos que você queira que seu SVG seja cortado pela caixa de exibição. Você também pode atualizar o padding-bottomparâmetro em seu css para corresponder à proporção do seu elemento svg. Excelente resposta embora - muito útil, e funciona um tratamento. Obrigado!
Andrew Guy
13
Com base na mesma idéia, seria interessante dar uma resposta que não envolva a preservação da proporção. A pergunta era sobre ter um elemento svg que fica cobrindo a janela inteira no redimensionamento, o que na maioria dos casos significa uma proporção diferente.
Nicolas Le Thierry d'Ennequin /
37
Apenas uma observação sobre o UX: para alguns casos de uso, tratar o gráfico d3 baseado em SVG como um ativo com uma proporção fixa não é o ideal. Gráficos que exibem texto, ou são dinâmicos ou, em geral, parecem mais uma interface do usuário adequada do que um ativo, têm mais a ganhar ao renderizar novamente com dimensões atualizadas. Por exemplo, o viewBox força as fontes e os marcadores do eixo a escalar para cima / baixo, potencialmente parecendo estranhos em comparação com o texto html. Por outro lado, uma nova renderização permite que o gráfico mantenha a rotulação de um tamanho consistente, além de oferecer a oportunidade de adicionar ou remover marcações.
Karim Hernandez
1
Por que top: 10px;? Parece incorreto para mim e fornece deslocamento. Colocar em 0px funciona bem.
TimZaman
43

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
fonte
11
Esta não é uma boa resposta, uma vez que envolve a licitação para eventos DOM ... Uma solução muito melhor é usando apenas d3 e CSS
alem0lars
@ alem0lars curiosamente, a versão css / d3 não funcionou para mim e este fez ...
Christian
32

UPDATE basta usar a nova maneira de @cminatti


resposta antiga para fins históricos

Na IMO, é melhor usar select () e on (), pois dessa forma você pode ter vários manipuladores de eventos de redimensionamento ... apenas não fique muito louco

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
fonte
Não posso concordar menos. O método cminatti funcionará em todos os arquivos d3js com os quais lidamos. Esse método de transformação com um redimensionamento por gráfico é um exagero. Além disso, ele precisa ser reescrito para todos os gráficos possíveis que possamos pensar. O método mencionado acima funciona para tudo. Eu tentei 4 receitas, incluindo o tipo de recarga que você mencionou e nenhuma delas funciona para vários gráficos de área, como o tipo usado no cubismo.
Eamonn Kenny
1
@EamonnKenny Não posso concordar mais com você. A outra resposta 7 meses no futuro é superior :)
SLF
Para este método: "d3.select (window) .on" Não foi possível encontrar "d3.select (window) .off" ou "d3.select (window) .unbind"
sea-kg
Esta resposta não é de forma alguma inferior. A resposta deles escalará todos os aspectos do gráfico (incluindo marcações, eixos, linhas e anotações de texto) que podem parecer estranhos, em alguns tamanhos de tela, ruins em outros e ilegíveis em tamanhos extremos. Acho melhor redimensionar usando esse método (ou, para soluções mais modernas, @Angu Agarwal).
Rúnar Berg
12

É meio feio se o código de redimensionamento for quase do tamanho do código para criar o gráfico em primeiro lugar. Então, em vez de redimensionar todos os elementos do gráfico existente, por que não simplesmente recarregá-lo? Aqui está como funcionou para mim:

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...
}

A linha crucial é d3.select('svg').remove();. Caso contrário, cada redimensionamento adicionará outro elemento SVG abaixo do anterior.

Raik
fonte
Isso com certeza vai matar o desempenho se você está redesenhando o elemento inteiro em cada evento janela de redimensionamento
sdemurjian
2
Mas isso está correto: À medida que você renderiza, você pode determinar se deve ter um eixo inserido, ocultar uma legenda, fazer outras coisas com base no conteúdo disponível. Usando uma solução puramente CSS / viewbox, tudo o que faz é fazer com que a visualização pareça esmagada. Bom para imagens, ruim para dados.
Chris Knoll
8

Nos layouts de força, simplesmente definir os atributos 'height' e 'width' não funcionará para re-centralizar / mover o gráfico para o contêiner svg. No entanto, há uma resposta muito simples que funciona para os Layouts de força encontrados aqui . Em suma:

Use o mesmo (qualquer) evento que desejar.

window.on('resize', resize);

Supondo que você tenha variáveis ​​svg & force:

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

Dessa forma, você não renderiza novamente o gráfico completamente, definimos os atributos e d3 recalcula as coisas conforme necessário. Isso pelo menos funciona quando você usa um ponto de gravidade. Não tenho certeza se esse é um pré-requisito para esta solução. Alguém pode confirmar ou negar ?

Saúde, g

gavs
fonte
Isso funcionou perfeitamente no meu layout de força, que tem cerca de 100 conexões. Obrigado.
precisa
1

Para aqueles que usam gráficos direcionados por força no D3 v4 / v5, o sizemétodo não existe mais. Algo como o seguinte funcionou para mim (com base nesta questão do github ):

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
fonte
1

Se você deseja vincular a lógica personalizada ao redimensionamento de eventos, hoje em dia você pode começar a usar a API do navegador ResizeObserver para a caixa delimitadora de um SVGElement.
Isso também resolverá o caso quando o contêiner for redimensionado devido à alteração do tamanho dos elementos próximos.
Há um polyfill para suporte mais amplo ao navegador.

É assim que pode funcionar no componente de interface do usuário:

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/[email protected]/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()
Anbu Agarwal
fonte