Qual é a melhor maneira de tornar um layout de visualização d3.js. responsivo?

224

Suponha que eu tenho um script de histograma que cria um gráfico de 960 500 svg. Como faço para que isso seja responsivo, o redimensionamento das larguras e alturas do gráfico é dinâmico?

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

O exemplo completo do histograma gist é: https://gist.github.com/993912

Matt Alcock
fonte
5
Eu achei o método nesta resposta mais fácil: stackoverflow.com/a/11948988/511203
Mike Gorski
a maneira mais simples que encontrei é tornar a largura e a altura do svg: 100% e aplicar o dimensionamento no contêiner do DOM (como div) stackoverflow.com/questions/47076163/…
mtx

Respostas:

316

Há outra maneira de fazer isso que não requer redesenho do gráfico e envolve a modificação dos atributos viewBox e preserveAspectRatio no <svg>elemento:

<svg id="chart" width="960" height="500"
  viewBox="0 0 960 500"
  preserveAspectRatio="xMidYMid meet">
</svg>

Atualização 24/11/15 : os navegadores mais modernos podem inferir a proporção dos elementos SVG a partir de viewBox, portanto, talvez você não precise manter o tamanho do gráfico atualizado. Se você precisar dar suporte a navegadores antigos, poderá redimensionar seu elemento quando a janela for redimensionada da seguinte forma:

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

E o conteúdo do svg será redimensionado automaticamente. Você pode ver um exemplo prático disso (com algumas modificações) aqui : basta redimensionar a janela ou o painel inferior direito para ver como ele reage.

Shawn Allen
fonte
1
Eu realmente gosto dessa abordagem shawn, 2 perguntas. 1 a caixa de visualização funciona em vários navegadores? 2. No seu exemplo, os círculos são redimensionados, mas não cabem na janela. Há algo de errado com a caixa de exibição svg ou a proporção.
Matt Alcock 25/05
4
Adore essa abordagem, não sei se o que foi descrito acima funciona corretamente. Como Matt diz, os círculos são redimensionados, mas a distância da esquerda do gráfico até o primeiro círculo não é redimensionada na mesma proporção que os círculos. Tentei mexer bastante no jsfiddle, mas não consegui fazê-lo funcionar como esperado usando o Google Chrome. Com quais navegadores o viewBox e o preserveAspectRatio são compatíveis?
22812 Chris Corners
9
Na verdade, definir apenas viewBox e preserveAspectRatio para um SVG com largura de altura definida como 100% do pai, e o pai sendo uma grade de bootstrap / flex funciona para mim sem manipular o evento de redimensionamento
Deepu
2
Confirmando que o Deepu está correto. Funciona com Bootstrap / grade flexível. Obrigado @Deepu.
Mike O'Connor
3
Nota: em vários tutoriais do d3 1.) no html, a largura e a altura do svg são definidas e 2.) a visualização é feita através de uma onload chamada de função. Se a largura e a altura do svg estiverem definidas no html real e você usar a técnica acima, a imagem não será escalável.
SumNeuron 01/04
34

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")
   .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); 

O código CSS:

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

Mais informações / tutoriais:

http://demosthenes.info/blog/744/Make-SVG-Responsive

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

cminatti
fonte
1
No meu caso, preenchimento inferior: 100% fará com que meu contêiner tenha preenchimento extra na parte inferior; portanto, mudei para 50% para removê-lo. Utilizou seu código e tornou o svg responsivo, obrigado.
V-SHY
20

Eu codifiquei uma pequena essência para resolver isso.

O padrão geral da solução é este:

  1. Divida o script em funções de computação e desenho.
  2. Verifique se a função de desenho é desenhada dinamicamente e é direcionada pelas variáveis ​​de largura e altura da visualização (a melhor maneira de fazer isso é usar a API d3.scale)
  3. Vincule / encadeie o desenho a um elemento de referência na marcação. (Eu usei o jquery para isso, então o importei).
  4. Lembre-se de removê-lo se já estiver desenhado. Obtenha as dimensões do elemento referenciado usando jquery.
  5. Vincular / encadear a função de desenho à função de redimensionamento da janela. Introduzir um debounce (timeout) a essa cadeia para garantir que apenas redesenhamos após um timeout.

Também adicionei o script d3.js. minificado para velocidade. A essência está aqui: https://gist.github.com/2414111

código de referência de referência jquery:

$(reference).empty()
var width = $(reference).width();

Código de debounce:

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

Aproveitar!

Matt Alcock
fonte
Isso não parece funcionar; se eu redimensionar a janela do arquivo html incluídos na essência corrigido, o histograma ainda fica cortada como a janela torna-se menor ...
Chris Withers
1
SVG é um formato vetorial. Você pode definir qualquer tamanho de caixa de visualização virtual e depois dimensioná-lo para dimensões reais. Não há necessidade de usar JS.
22613 philipirozhkov
4

Sem usar o ViewBox

Aqui está um exemplo de uma solução que não depende do uso de um viewBox:

A chave está na atualização do intervalo das escalas que são usadas para colocar dados.

Primeiro, calcule sua proporção original:

var ratio = width / height;

Em cada redimensionamento, atualize o rangede xe y:

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

Observe que a altura é baseada na largura e na proporção, para que suas proporções originais sejam mantidas.

Por fim, "redesenhe" o gráfico - atualize qualquer atributo que dependa de uma das escalas xou y:

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

Observe que, ao redimensionar o tamanho, rectsvocê pode usar o limite superior rangede y, em vez de usar explicitamente a altura:

.attr("y", function(d) { return y.range()[1] - y(d.y); })

Paul Murray
fonte
3

Se você estiver usando o d3.js até o c3.js, a solução para o problema de responsividade é bastante direta:

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

onde o HTML gerado se parece:

<div id="chart">
    <svg>...</svg>
</div>
vanna
fonte
2

A resposta de Shawn Allen foi ótima. Mas você pode não querer fazer isso sempre. Se você o hospedar no vida.io , você será responsivo automaticamente pela sua visualização svg.

Você pode obter um iframe responsivo com este código de incorporação simples:

<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/

Divulgação: eu construir esse recurso a vida.io .

Phuoc Do
fonte
2

No caso de você estar usando um wrapper d3 como o plottable.js , saiba que a solução mais fácil pode ser adicionar um ouvinte de evento e, em seguida, chamar uma função de redesenho ( redrawem plottable.js). No caso do plottable.js, isso funcionará de maneira excelente (essa abordagem está mal documentada):

    window.addEventListener("resize", function() {
      table.redraw();
    });
Riley Steele Parsons
fonte
Provavelmente seria melhor para Debounce esta função para evitar disparar o redesenho mais rápido possível
Ian
1

Caso as pessoas ainda estejam visitando esta pergunta - eis o que funcionou para mim:

  • Coloque o iframe em uma div e use css para adicionar um preenchimento de, digamos, 40% a essa div (a porcentagem dependendo da proporção que você deseja). Em seguida, defina a largura e a altura do próprio iframe como 100%.

  • No documento html que contém o gráfico a ser carregado no iframe, defina a largura como a largura da div à qual o svg é anexado (ou à largura do corpo) e defina a proporção da largura para a largura *.

  • Escreva uma função que recarrega o conteúdo do iframe no redimensionamento da janela, para adaptar o tamanho do gráfico quando as pessoas rodam o telefone.

Exemplo aqui no meu site: http://dirkmjk.nl/en/2016/05/embedding-d3js-charts-responsive-website

ATUALIZAÇÃO 30 de dezembro de 2016

A abordagem que descrevi acima tem algumas desvantagens, principalmente porque ela não leva em consideração a altura de nenhum título e legenda que não fazem parte do svg criado pelo D3. Desde então, me deparei com o que considero uma abordagem melhor:

  • Defina a largura do gráfico D3 para a largura da div à qual está anexado e use a proporção para definir sua altura de acordo;
  • Faça com que a página incorporada envie sua altura e URL para a página pai usando postMessage do HTML5;
  • Na página pai, use o URL para identificar o iframe correspondente (útil se você tiver mais de um iframe na página) e atualize sua altura para a altura da página incorporada.

Exemplo aqui no meu site: http://dirkmjk.nl/en/2016/12/embedding-d3js-charts-responsive-website-better-solution

dirkmjk
fonte
0

Um dos princípios básicos da junção de dados D3 é que ela é idempotente. Em outras palavras, se você avaliar repetidamente uma junção de dados com os mesmos dados, a saída renderizada será a mesma. Portanto, desde que você renderize seu gráfico corretamente, tomando cuidado com as seleções de entrada, atualização e saída - tudo o que você precisa fazer quando o tamanho muda, é renderizar novamente o gráfico na sua totalidade.

Há algumas outras coisas que você deve fazer, uma delas é devolver o manipulador de redimensionamento da janela para acelerá-la. Além disso, em vez de codificar larguras / alturas, isso deve ser alcançado medindo o elemento que o contém.

Como alternativa, aqui está seu gráfico renderizado usando d3fc , que é um conjunto de componentes D3 que manipulam corretamente junções de dados. Ele também possui um gráfico cartesiano que o mede contendo o elemento, facilitando a criação de gráficos 'responsivos':

// create some test data
var data = d3.range(50).map(function(d) {
  return {
    x: d / 4,
    y: Math.sin(d / 4),
    z: Math.cos(d / 4) * 0.7
  };
});

var yExtent = fc.extentLinear()
  .accessors([
    function(d) { return d.y; },
    function(d) { return d.z; }
  ])
  .pad([0.4, 0.4])
  .padUnit('domain');

var xExtent = fc.extentLinear()
  .accessors([function(d) { return d.x; }]);

// create a chart
var chart = fc.chartSvgCartesian(
    d3.scaleLinear(),
    d3.scaleLinear())
  .yDomain(yExtent(data))
  .yLabel('Sine / Cosine')
  .yOrient('left')
  .xDomain(xExtent(data))
  .xLabel('Value')
  .chartLabel('Sine/Cosine Line/Area Chart');

// create a pair of series and some gridlines
var sinLine = fc.seriesSvgLine()
  .crossValue(function(d) { return d.x; })
  .mainValue(function(d) { return d.y; })
  .decorate(function(selection) {
    selection.enter()
      .style('stroke', 'purple');
  });

var cosLine = fc.seriesSvgArea()
  .crossValue(function(d) { return d.x; })
  .mainValue(function(d) { return d.z; })
  .decorate(function(selection) {
    selection.enter()
      .style('fill', 'lightgreen')
      .style('fill-opacity', 0.5);
  });

var gridlines = fc.annotationSvgGridline();

// combine using a multi-series
var multi = fc.seriesSvgMulti()
  .series([gridlines, sinLine, cosLine]);

chart.plotArea(multi);

// render
d3.select('#simple-chart')
  .datum(data)
  .call(chart);

Você pode vê-lo em ação neste codepen:

https://codepen.io/ColinEberhardt/pen/dOBvOy

onde você pode redimensionar a janela e verificar se o gráfico foi corretamente renderizado novamente.

Observe que, como uma divulgação completa, sou um dos mantenedores do d3fc.

ColinE
fonte
Obrigado por manter suas respostas atualizadas! Vi as atualizações recentes de algumas de suas postagens e realmente aprecio as pessoas que revisitam seu próprio conteúdo! No entanto, como uma palavra de cautela, esta revisão não se encaixa mais na questão, porque você edita no D3 v4 , enquanto OP se refere claramente à v3 , o que pode confundir futuros leitores. Pessoalmente, para minhas próprias respostas, costumo adicionar uma seção v4 enquanto ainda mantenho a seção v3 original . Eu acho que a v4 pode valer sua própria resposta, adicionando uma referência mútua aos dois posts. Mas isso é apenas meus dois centavos ...
altocumulus
Esse é realmente um bom ponto! É tão fácil se deixar levar e se concentrar apenas no futuro e nas novidades. A julgar pelas perguntas mais recentes do d3, acho que muitas pessoas ainda estão usando a v3. Também é frustrante o quão sutis são algumas das diferenças! Vou revisitar minhas edições :-)
ColinE 27/12
0

Eu evitaria o redimensionamento / tick de soluções como a praga, uma vez que são ineficientes e podem causar problemas no seu aplicativo (por exemplo, uma dica de ferramenta recalcula a posição em que ela deve aparecer no redimensionamento da janela; em seguida, um momento depois, o gráfico também é redimensionado e a página é redimensionada. layouts e agora sua dica está incorreta novamente).

Você pode simular esse comportamento em alguns navegadores antigos que também não o suportam adequadamente, como o IE11, usando um <canvas>elemento que mantém seu aspecto.

Dado 960x540, que é um aspecto de 16: 9:

<div style="position: relative">
  <canvas width="16" height="9" style="width: 100%"></canvas>
  <svg viewBox="0 0 960 540" preserveAspectRatio="xMidYMid meet" style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; -webkit-tap-highlight-color: transparent;">
  </svg>
</div>
Dominic
fonte
0

Muitas respostas complexas aqui.

Basicamente, tudo que você precisa fazer é abandonar os atributos widthe heightem favor do viewBoxatributo:

width = 500;
height = 500;

const svg = d3
  .select("#chart")
  .append("svg")
  .attr("viewBox", `0 0 ${width} ${height}`)

Se você tiver margens, basta adicioná-las à largura / altura e depois anexá- glas posteriormente e transformá-las como faria normalmente.

Jared Wilber
fonte
-1

Você também pode usar o bootstrap 3 para adaptar o tamanho de uma visualização. Por exemplo, podemos configurar o código HTML como:

<div class="container>
<div class="row">

<div class='col-sm-6 col-md-4' id="month-view" style="height:345px;">
<div id ="responsivetext">Something to write</div>
</div>

</div>
</div>

Configurei uma altura fixa devido às minhas necessidades, mas você também pode deixar o tamanho automático. O "col-sm-6 col-md-4" torna o div responsivo a diferentes dispositivos. Você pode saber mais em http://getbootstrap.com/css/#grid-example-basic

Podemos acessar o gráfico com a ajuda da id month-view .

Não entrarei em muitos detalhes sobre o código d3, apenas inserirei a parte necessária para adaptar a diferentes tamanhos de tela.

var width = document.getElementById('month-view').offsetWidth;

var height = document.getElementById('month-view').offsetHeight - document.getElementById('responsivetext2').offsetHeight;

A largura é definida obtendo-se a largura da div com o id month-view.

A altura no meu caso não deve incluir toda a área. Também tenho algum texto acima da barra, por isso preciso calcular essa área também. Por isso identifiquei a área do texto com o id responsivetext. Para calcular a altura permitida da barra, subtraí a altura do texto da altura da div.

Isso permite que você tenha uma barra que adotará todos os diferentes tamanhos de tela / div. Pode não ser a melhor maneira de fazê-lo, mas certamente funciona para as necessidades do meu projeto.

arlindmusliu
fonte
1
Oi, eu tentei isso, mas o conteúdo svg não é redimensionado, como o calendário ou os gráficos.