Redimensionner svg lorsque la fenêtre est redimensionnée dans d3.js

188

Je dessine un nuage de points avec d3.js. À l'aide de cette question:
obtenez la taille de l'écran, la page Web actuelle et la fenêtre du navigateur

J'utilise cette réponse:

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;

Je suis donc capable d'adapter mon tracé à la fenêtre de l'utilisateur comme ceci:

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

Maintenant, j'aimerais que quelque chose s'occupe de redimensionner le tracé lorsque l'utilisateur redimensionne la fenêtre.

PS: Je n'utilise pas jQuery dans mon code.

mthpvg
la source
1
Copie
ColinE

Réponses:

301

Recherchez `` SVG réactif '', il est assez simple de rendre un SVG réactif et vous n'avez plus à vous soucier des tailles.

Voici comment je l'ai fait:

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>

Remarque: tout ce qui se trouve dans l'image SVG sera mis à l'échelle avec la largeur de la fenêtre. Cela inclut la largeur du trait et les tailles de police (même celles définies avec CSS). Si cela n'est pas souhaité, il existe des solutions alternatives plus impliquées ci-dessous.

Plus d'infos / tutoriels:

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

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

cminatti
la source
8
Ceci est beaucoup plus élégant et utilise également une approche de mise à l'échelle du conteneur qui devrait être dans la boîte à outils de chaque développeur frontal (peut être utilisé pour mettre à l'échelle quoi que ce soit dans un rapport hauteur / largeur particulier). Devrait être la meilleure réponse.
kontur le
14
Juste pour ajouter à ceci: l' viewBoxattribut doit être défini sur la hauteur et la largeur appropriées de votre tracé SVG: .attr("viewBox","0 0 " + width + " " + height)widthet heightsont définis ailleurs. Autrement dit, à moins que vous ne souhaitiez que votre SVG soit coupé par la zone de visualisation. Vous pouvez également mettre à jour le padding-bottomparamètre dans votre css pour qu'il corresponde au rapport hauteur / largeur de votre élément svg. Excellente réponse cependant - très utile et fonctionne un régal. Merci!
Andrew Guy
14
Sur la base de la même idée, il serait intéressant de donner une réponse qui n'implique pas de préserver le rapport hauteur / largeur. La question était d'avoir un élément svg qui continue de couvrir toute la fenêtre lors du redimensionnement, ce qui dans la plupart des cas signifie un rapport hauteur / largeur différent.
Nicolas Le Thierry d'Ennequin
40
Juste une note concernant l'UX: pour certains cas d'utilisation, traiter votre graphique d3 basé sur SVG comme un actif avec un rapport hauteur / largeur fixe n'est pas idéal. Les graphiques qui affichent du texte, ou sont dynamiques, ou en général ressemblent plus à une interface utilisateur appropriée qu'à un actif, ont plus à gagner en effectuant un nouveau rendu avec des dimensions mises à jour. Par exemple, viewBox force les polices d'axe et les graduations à augmenter / réduire, ce qui peut sembler gênant par rapport au texte html. En revanche, un nouveau rendu permet au graphique de garder son étiquetage d'une taille cohérente, en plus il offre la possibilité d'ajouter ou de supprimer des graduations.
Karim Hernandez
1
Pourquoi top: 10px;? Cela me semble incorrect et fournit un décalage. Mettre à 0px fonctionne très bien.
TimZaman
44

Utilisez 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
la source
11
Ce n'est pas une bonne réponse car cela implique l'attente d'événements DOM ... Une bien meilleure solution consiste à n'utiliser que d3 et css
alem0lars
1
@ alem0lars curieusement, la version css / d3 n'a pas fonctionné pour moi et celle-ci a fonctionné ...
Christian
Je sais qu'il apparaît dans le jsFiddle , mais il est assez déroutant de voir w, e, gcomming de nulle part et x, en ycours d' initialisation sans mots - clés dans votre réponse.
Leogout
33

MISE À JOUR utilisez simplement la nouvelle méthode de @cminatti


ancienne réponse à des fins historiques

IMO, il est préférable d'utiliser select () et on () car de cette façon, vous pouvez avoir plusieurs gestionnaires d'événements de redimensionnement ... ne soyez pas trop fou

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
la source
Je ne peux pas être moins d'accord. La méthode de cminatti fonctionnera sur tous les fichiers d3js que nous traitons. Cette méthode de transformation avec un redimensionnement par graphique est excessive. En outre, il doit être réécrit pour chaque graphique possible que nous pourrions imaginer. La méthode mentionnée ci-dessus fonctionne pour tout. J'ai essayé 4 recettes, y compris le type de rechargement que vous avez mentionné et aucune d'elles ne fonctionne pour des parcelles à plusieurs zones telles que le type utilisé dans le cubisme.
Eamonn Kenny
1
@EamonnKenny Je ne peux pas être plus d'accord avec vous. L'autre réponse dans 7 mois est supérieure :)
slf
Pour cette méthode: "d3.select (window) .on" Je n'ai pas trouvé "d3.select (window) .off" ou "d3.select (window) .unbind"
sea-kg
1
Cette réponse n'est en aucun cas inférieure. Leur réponse mettra à l'échelle tous les aspects du graphique (y compris les graduations, les axes, les lignes et les annotations de texte) qui peuvent sembler gênants, dans certaines tailles d'écran, mauvais dans d'autres et illisibles dans des tailles extrêmes. Je trouve préférable de redimensionner en utilisant cette méthode (ou, pour une solution plus moderne, @Angu Agarwal).
Rúnar Berg
12

C'est un peu moche si le code de redimensionnement est presque aussi long que le code de construction du graphique en premier lieu. Donc, au lieu de redimensionner chaque élément du graphique existant, pourquoi ne pas simplement le recharger? Voici comment cela a fonctionné pour moi:

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 ligne cruciale est d3.select('svg').remove();. Sinon, chaque redimensionnement ajoutera un autre élément SVG sous le précédent.

Raik
la source
Cela va absolument tuer les performances si vous redessinez l'élément entier à chaque événement de redimensionnement de fenêtre
sdemurjian
2
Mais c'est correct: lors du rendu, vous pouvez déterminer si vous devez avoir un axe en médaillon, masquer une légende, faire d'autres choses en fonction du contenu disponible. En utilisant une solution purement CSS / viewbox, il ne fait que donner à la visualisation un aspect écrasé. Bon pour les images, mauvais pour les données.
Chris Knoll
8

Dans les mises en page forcées, le simple fait de définir les attributs 'height' et 'width' ne fonctionnera pas pour recentrer / déplacer le tracé dans le conteneur svg. Cependant, il existe une réponse très simple qui fonctionne pour les dispositions de force trouvées ici . En résumé:

Utilisez les mêmes (tous) événements que vous aimez.

window.on('resize', resize);

Alors en supposant que vous avez des variables svg et 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 cette façon, vous ne restaurez pas entièrement le graphique, nous définissons les attributs et d3 recalcule les choses si nécessaire. Cela fonctionne au moins lorsque vous utilisez un point de gravité. Je ne sais pas si c'est une condition préalable à cette solution. Quelqu'un peut-il confirmer ou nier?

Bravo, g

gavs
la source
Cela a parfaitement fonctionné sur ma disposition de force qui a environ 100 connexions. Merci.
tribe84 du
1

Pour ceux qui utilisent des graphes dirigés par la force dans D3 v4 / v5, la sizeméthode n'existe plus. Quelque chose comme ce qui suit a fonctionné pour moi (basé sur ce problème 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
la source
1

Si vous voulez la logique BIND pour redimensionner l' événement, de nos jours , vous pouvez commencer à utiliser l' API de navigateur ResizeObserver pour la zone de délimitation d'un SVGElement.
Cela gérera également le cas où le conteneur est redimensionné en raison du changement de taille des éléments à proximité.
Il existe un polyfill pour une prise en charge plus large du navigateur.

Voici comment cela peut fonctionner dans le composant UI:

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