¿Cuál es la mejor manera de hacer que un diseño de visualización d3.js responda?

224

Supongamos que tengo un script de histograma que crea un gráfico de 960 500 svg. ¿Cómo hago que esto responda para cambiar el tamaño de los anchos gráficos y las alturas dinámicas?

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

El ejemplo completo de histograma es: https://gist.github.com/993912

Matt Alcock
fuente
55
Encontré que el método en esta respuesta es más fácil: stackoverflow.com/a/11948988/511203
Mike Gorski
la forma más simple que encontré es hacer un ancho y una altura de svg: 100% y aplicar el tamaño en el contenedor DOM (como div) stackoverflow.com/questions/47076163/…
mtx

Respuestas:

316

Hay otra forma de hacer esto que no requiere volver a dibujar el gráfico, e implica modificar los atributos viewBox y preserveAspectRatio en el <svg>elemento:

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

Actualización 24/11/15 : la mayoría de los navegadores modernos pueden inferir la relación de aspecto de los elementos SVG a partir de viewBox, por lo que es posible que no necesite mantener actualizado el tamaño del gráfico. Si necesita admitir navegadores antiguos, puede cambiar el tamaño de su elemento cuando la ventana cambie de tamaño de la siguiente manera:

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

Y el contenido de svg se escalará automáticamente. Puede ver un ejemplo funcional de esto (con algunas modificaciones) aquí : simplemente cambie el tamaño de la ventana o el panel inferior derecho para ver cómo reacciona.

Shawn Allen
fuente
1
Realmente me gusta este enfoque shawn, 2 preguntas. 1 ¿funciona el viewbox cross browser? 2. En su ejemplo, los círculos cambian de tamaño pero no caben en la ventana. Algo va mal con el cuadro de vista de svg o la relación de aspecto.
Matt Alcock
44
Me encanta este enfoque, no estoy seguro de que lo anterior funcione correctamente. Como dice Matt, los círculos cambian de tamaño, pero la distancia desde la izquierda del gráfico hasta el primer círculo no cambia de tamaño al mismo ritmo que los círculos. Traté de tocar un poco en jsfiddle pero no pude hacerlo funcionar como se esperaba usando Google Chrome. ¿Con qué navegadores son compatibles viewBox y preserveAspectRatio?
Chris Withers
99
En realidad, configurar solo viewBox y preserveAspectRatio en un SVG con una altura de ancho establecida al 100% del padre, y el padre que es una cuadrícula bootstrap / flex funciona para mí sin manejar el evento de cambio de tamaño
Deepu
2
Confirmando que Deepu es correcto. Funciona con Bootstrap / rejilla flexible. Gracias @Deepu.
Mike O'Connor
3
Nota: en varios tutoriales de d3 1.) en el html se establecen el ancho y la altura del svg y 2.) la visualización se realiza mediante una onload llamada a la función. Si el ancho y la altura del svg se establecen en el html real y utiliza la técnica anterior, la imagen no será escalable.
SumNeuron 01 de
34

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

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

Más información / tutoriales:

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

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

cminatti
fuente
1
En mi caso, relleno inferior: el 100% hará que mi contenedor tenga relleno adicional en la parte inferior, por lo tanto, lo cambié al 50% para eliminarlo. Usó su código y logró que el svg responda, gracias.
V-SHY
20

He codificado una pequeña idea para resolver esto.

El patrón de solución general es este:

  1. Divide el script en funciones de cálculo y dibujo.
  2. Asegúrese de que la función de dibujo dibuja dinámicamente y se basa en las variables de ancho y alto de visualización (La mejor manera de hacerlo es usar la API d3.scale)
  3. Vincula / encadena el dibujo a un elemento de referencia en el marcado. (Usé jquery para esto, así que lo importé).
  4. Recuerde eliminarlo si ya está dibujado. Obtenga las dimensiones del elemento referenciado utilizando jquery.
  5. Vincula / encadena la función de dibujo a la función de cambio de tamaño de la ventana. Introduzca un rebote (tiempo de espera) en esta cadena para garantizar que solo volvamos a dibujar después de un tiempo de espera.

También agregué el script d3.js minimizado para la velocidad. La esencia está aquí: https://gist.github.com/2414111

código de referencia de jquery:

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

Código de rebote:

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

¡Disfrutar!

Matt Alcock
fuente
Esto no parece funcionar; si cambio el tamaño de la ventana del archivo html incluido en la esencia corregida, el histograma todavía se corta a medida que la ventana se vuelve más pequeña ...
Chris Withers
1
SVG es un formato vectorial. Puede establecer cualquier tamaño de cuadro de vista virtual y luego escalarlo a dimensiones reales. No es necesario usar JS.
phil pirozhkov
4

Sin usar ViewBox

Aquí hay un ejemplo de una solución que no depende del uso de viewBox:

La clave está en actualizar el rango de las escalas que se utilizan para colocar datos.

Primero, calcule su relación de aspecto original:

var ratio = width / height;

Luego, en cada cambio de tamaño, actualice el rangede xy y:

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

Tenga en cuenta que la altura se basa en el ancho y la relación de aspecto, por lo que se mantienen sus proporciones originales.

Finalmente, "vuelva a dibujar" el gráfico: actualice cualquier atributo que dependa de cualquiera de las escalas xo 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); });
}

Tenga en cuenta que al cambiar el tamaño rectspuede usar el límite superior de rangeof y, en lugar de usar explícitamente la altura:

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

Paul Murray
fuente
3

Si está utilizando d3.js a través de c3.js, la solución al problema de la capacidad de respuesta es bastante sencilla:

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

donde se ve el HTML generado:

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

La respuesta de Shawn Allen fue genial. Pero es posible que no desee hacer esto cada vez. Si lo aloja en vida.io , obtiene respuesta automática para su visualización de svg.

Puede obtener un iframe receptivo con este código de inserción simple:

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

Divulgación: construyo esta función en vida.io .

Phuoc Do
fuente
2

En el caso de que esté utilizando un contenedor d3 como plottable.js , tenga en cuenta que la solución más fácil podría ser agregar un detector de eventos y luego llamar a una función de redibujo ( redrawen plottable.js). En el caso de plottable.js, esto funcionará excelentemente (este enfoque está mal documentado):

    window.addEventListener("resize", function() {
      table.redraw();
    });
Riley Steele Parsons
fuente
Probablemente sería mejor eliminar la función de esta función para evitar disparar el redibujo lo más rápido posible
Ian
1

En caso de que la gente siga visitando esta pregunta, esto es lo que funcionó para mí:

  • Encierre el iframe en un div y use css para agregar un relleno de, por ejemplo, 40% a ese div (el porcentaje depende de la relación de aspecto que desee). Luego, establezca el ancho y la altura del iframe en 100%.

  • En el documento html que contiene el gráfico que se cargará en el iframe, establezca el ancho al ancho del div al que se adjunta el svg (o al ancho del cuerpo) y establezca la relación de aspecto alto a ancho *.

  • Escriba una función que recargue el contenido del iframe al cambiar el tamaño de la ventana, para adaptar el tamaño del gráfico cuando las personas rotan su teléfono.

Ejemplo aquí en mi sitio web: http://dirkmjk.nl/en/2016/05/embedding-d3js-charts-responsive-website

ACTUALIZACIÓN 30 dic 2016

El enfoque que describí anteriormente tiene algunos inconvenientes, especialmente que no tiene en cuenta la altura de ningún título y subtítulos que no sean parte del svg creado por D3. Desde entonces me he encontrado con lo que creo que es un mejor enfoque:

  • Establezca el ancho del gráfico D3 al ancho del div al que está conectado y use la relación de aspecto para establecer su altura en consecuencia;
  • Haga que la página incrustada envíe su altura y url a la página principal utilizando postMessage de HTML5;
  • En la página principal, use la URL para identificar el iframe correspondiente (útil si tiene más de un iframe en su página) y actualice su altura a la altura de la página incrustada.

Ejemplo aquí en mi sitio web: http://dirkmjk.nl/en/2016/12/embedding-d3js-charts-responsive-website-better-solution

dirkmjk
fuente
0

Uno de los principios básicos de la unión de datos D3 es que es idempotente. En otras palabras, si evalúa repetidamente una unión de datos con los mismos datos, la salida representada es la misma. Por lo tanto, siempre que renderice su gráfico correctamente, tenga cuidado con sus selecciones de entrada, actualización y salida; todo lo que tiene que hacer cuando cambia el tamaño es volver a representar el gráfico en su totalidad.

Hay un par de otras cosas que debe hacer, una es retirar el controlador de cambio de tamaño de la ventana para acelerarlo. Además, en lugar de codificar anchos / alturas rígidas, esto debería lograrse midiendo el elemento contenedor.

Como alternativa, aquí está su gráfico representado con d3fc , que es un conjunto de componentes D3 que manejan correctamente las uniones de datos. También tiene un gráfico cartesiano que mide que contiene un elemento, lo que facilita la creación de gráficos 'receptivos':

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

Puedes verlo en acción en este codepen:

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

donde puede cambiar el tamaño de la ventana y verificar que el gráfico se vuelva a representar correctamente.

Tenga en cuenta que, como divulgación completa, soy uno de los mantenedores de d3fc.

ColinE
fuente
¡Gracias por mantener sus respuestas actualizadas! He visto las actualizaciones recientes de algunas de sus publicaciones, ¡y realmente aprecio que las personas revisen su propio contenido! Sin embargo, como advertencia, esta revisión ya no se ajusta completamente a la pregunta, porque edita en D3 v4 , mientras que OP se refiere claramente a v3 , lo que podría confundir a los futuros lectores. Personalmente, para mis propias respuestas, tiendo a agregar una sección v4 mientras mantengo la sección v3 original . Creo que la v4 puede valer su propia respuesta agregando una referencia mutua a ambas publicaciones. Pero esos son solo mis dos centavos ...
altocumulus
Ese es un muy buen punto! Es muy fácil dejarse llevar y concentrarse en el futuro y las novedades. A juzgar por las preguntas más recientes de d3, creo que mucha gente todavía usa v3. ¡También es frustrante lo sutiles que son algunas de las diferencias!
Revisaré
0

Evitaría cambiar el tamaño / marcar soluciones como la plaga, ya que son ineficientes y pueden causar problemas en su aplicación (por ejemplo, una información sobre herramientas vuelve a calcular la posición en la que debería aparecer en el cambio de tamaño de la ventana, luego, un momento después, su gráfico cambia de tamaño y la página vuelve a cambiar). diseños y ahora su información sobre herramientas está mal otra vez).

Puede simular este comportamiento en algunos navegadores más antiguos que no lo soportan correctamente como IE11 también usando un <canvas>elemento que mantiene su aspecto.

Dado 960x540, que es un 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
fuente
0

Muchas respuestas complejas aquí.

Básicamente, todo lo que necesita hacer es deshacerse de los atributos widthy heighten favor del viewBoxatributo:

width = 500;
height = 500;

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

Si tiene márgenes, puede agregarlos allí en el ancho / alto y luego agregarlos a gcontinuación y transformarlos como lo haría normalmente.

Jared Wilber
fuente
-1

También puede usar bootstrap 3 para adaptar el tamaño de una visualización. Por ejemplo, podemos configurar el 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>

He configurado una altura fija debido a mis necesidades, pero también puede dejar el tamaño automático. El "col-sm-6 col-md-4" hace que el div responda a diferentes dispositivos. Puede obtener más información en http://getbootstrap.com/css/#grid-example-basic

Podemos acceder al gráfico con la ayuda de la vista de id de mes .

No entraré en muchos detalles sobre el código d3, solo ingresaré la parte que se necesita para adaptarse a diferentes tamaños de pantalla.

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

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

El ancho se establece obteniendo el ancho del div con la vista id de mes.

La altura en mi caso no debe incluir toda el área. También tengo un texto sobre la barra, así que necesito calcular esa área también. Es por eso que identifiqué el área del texto con el id responsivetext. Para calcular la altura permitida de la barra, resta la altura del texto de la altura del div.

Esto le permite tener una barra que adoptará todos los diferentes tamaños de pantalla / div. Puede que no sea la mejor manera de hacerlo, pero seguramente funciona para las necesidades de mi proyecto.

arlindmusliu
fuente
1
Hola, probé esto, pero el contenido de svg no cambia de tamaño, como el calendario o los gráficos.