Cambiar el tamaño de svg cuando la ventana cambia de tamaño en d3.js

183

Estoy dibujando un diagrama de dispersión con d3.js. Con la ayuda de esta pregunta:
Obtenga el tamaño de la pantalla, la página web actual y la ventana del navegador

Estoy usando esta respuesta:

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;

Así que puedo ajustar mi trama a la ventana del usuario de esta manera:

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

Ahora me gustaría que algo se encargue de cambiar el tamaño de la trama cuando el usuario cambie el tamaño de la ventana.

PD: no estoy usando jQuery en mi código.

mthpvg
fuente

Respuestas:

295

Busque 'SVG receptivo', es bastante simple hacer que un SVG responda y ya no tiene que preocuparse por los tamaños.

Así es como lo hice:

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: Todo en la imagen SVG se escalará con el ancho de la ventana. Esto incluye el ancho del trazo y los tamaños de fuente (incluso aquellos configurados con CSS). Si esto no se desea, hay más soluciones alternativas involucradas a continuación.

Más información / tutoriales:

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

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

cminatti
fuente
8
Esto es mucho más elegante y también utiliza un enfoque de escala de contenedor que debería estar en el kit de herramientas de cada desarrollador front-end (se puede usar para escalar cualquier cosa en una relación de aspecto particular). Debería ser la mejor respuesta.
kontur
13
Solo para agregar a esto: el viewBoxatributo debe establecerse en la altura y el ancho relevantes de su diagrama SVG: .attr("viewBox","0 0 " + width + " " + height) donde widthy heightse definen en otro lugar. Es decir, a menos que desee que su cuadro de vista recorte su SVG. También es posible que desee actualizar el padding-bottomparámetro en su CSS para que coincida con la relación de aspecto de su elemento svg. Excelente respuesta, muy útil, y funciona de maravilla. ¡Gracias!
Andrew Guy
13
Basado en la misma idea, sería interesante dar una respuesta que no implique preservar la relación de aspecto. La pregunta era sobre tener un elemento svg que siga cubriendo la ventana completa al cambiar el tamaño, lo que en la mayoría de los casos significa una relación de aspecto diferente.
Nicolas Le Thierry d'Ennequin
37
Solo una nota con respecto a UX: para algunos casos de uso, tratar su gráfico d3 basado en SVG como un activo con una relación de aspecto fija no es ideal. Los gráficos que muestran texto, o son dinámicos, o en general se sienten más como una interfaz de usuario adecuada que como un activo, tienen más que ganar al volver a representar con dimensiones actualizadas. Por ejemplo, viewBox obliga a las fuentes y los ticks del eje a escalar hacia arriba / abajo, lo que puede parecer incómodo en comparación con el texto html. Por el contrario, un re-renderizado permite que el gráfico mantenga su etiquetado de un tamaño constante, además de que brinda la oportunidad de agregar o eliminar marcas.
Karim Hernández
1
¿Por qué top: 10px;? Parece incorrecto para mí y proporciona compensación. Poner a 0px funciona bien.
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
fuente
11
Esta no es una buena respuesta ya que implica la licitación de eventos DOM ... Una solución mucho mejor es usar solo d3 y css
alem0lars
@ alem0lars curiosamente, la versión css / d3 no funcionó para mí y esta sí ...
Christian
32

ACTUALIZAR simplemente use la nueva forma de @cminatti


vieja respuesta para propósitos históricos

En mi opinión, es mejor usar select () y on () ya que de esa manera puedes tener múltiples controladores de eventos de cambio de tamaño ... simplemente no te vuelvas loco

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
fuente
No puedo estar menos de acuerdo. El método de cminatti funcionará en todos los archivos d3js con los que tratamos. Este método de transformación con un cambio de tamaño por gráfico es excesivo. Además, debe reescribirse para cada gráfico posible que podamos pensar. El método mencionado anteriormente funciona para todo. He probado 4 recetas, incluido el tipo de recarga que ha mencionado y ninguna de ellas funciona para trazados de áreas múltiples, como el tipo utilizado en el cubismo.
Eamonn Kenny
1
@EamonnKenny No puedo estar más de acuerdo contigo. La otra respuesta 7 meses en el futuro es superior :)
slf
Para este método: "d3.select (window) .on" No pude encontrar "d3.select (window) .off" o "d3.select (window) .unbind"
sea-kg
Esta respuesta no es de ninguna manera inferior. Su respuesta escalará todos los aspectos del gráfico (incluidos los ticks, ejes, líneas y anotaciones de texto) que pueden parecer incómodos, en algunos tamaños de pantalla, malos en otros e ilegibles en tamaños extremos. Me parece mejor cambiar el tamaño utilizando este método (o, para soluciones más modernas, @Angu Agarwal).
Rúnar Berg
12

Es un poco feo si el código de cambio de tamaño es casi tan largo como el código para construir el gráfico en primer lugar. Entonces, en lugar de cambiar el tamaño de cada elemento del gráfico existente, ¿por qué no simplemente recargarlo? Así es como funcionó para mí:

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

La línea crucial es d3.select('svg').remove();. De lo contrario, cada cambio de tamaño agregará otro elemento SVG debajo del anterior.

Raik
fuente
Esto matará absolutamente rendimiento si estás volver a dibujar el elemento completo en cada evento de cambio de tamaño de ventana
sdemurjian
2
Pero esto es correcto: a medida que renderiza, puede determinar si debe tener un eje insertado, ocultar una leyenda, hacer otras cosas según el contenido disponible. Usando una solución puramente CSS / viewbox, todo lo que hace es hacer que la visualización se vea compacta. Bueno para las imágenes, malo para los datos.
Chris Knoll
8

En los diseños de fuerza, simplemente establecer los atributos 'alto' y 'ancho' no funcionará para volver a centrar / mover el gráfico al contenedor de svg. Sin embargo, hay una respuesta muy simple que funciona para Force Layouts que se encuentra aquí. . En resumen:

Use el mismo evento que desee.

window.on('resize', resize);

Luego, suponiendo que tenga las variables svg y 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();
}

De esta manera, no vuelve a renderizar el gráfico por completo, establecemos los atributos y d3 vuelve a calcular las cosas según sea necesario. Esto al menos funciona cuando usas un punto de gravedad. No estoy seguro de si ese es un requisito previo para esta solución. ¿Alguien puede confirmar o negar?

Saludos, g

gavs
fuente
Esto funcionó perfectamente en mi diseño de fuerza que tiene alrededor de 100 conexiones. Gracias.
Tribe84
1

Para aquellos que usan gráficos de fuerza dirigida en D3 v4 / v5, el sizemétodo ya no existe. Algo como lo siguiente funcionó para mí (basado en este problema de 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
fuente
1

Si desea vincular la lógica personalizada para cambiar el tamaño del evento, hoy en día puede comenzar a usar la API del navegador ResizeObserver para el cuadro delimitador de un elemento SVGElement.
Esto también manejará el caso cuando se cambie el tamaño del contenedor debido al cambio de tamaño de los elementos cercanos.
Hay un polyfill para un soporte de navegador más amplio.

Así es como puede funcionar en el componente de la interfaz de usuario:

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
fuente