Quelle est la meilleure façon de rendre une mise en page de visualisation d3.js réactive?

224

Supposons que j'ai un script d'histogramme qui crée un graphique svg 960 500. Comment puis-je rendre cela réactif ainsi de suite redimensionner les largeurs et hauteurs graphiques sont dynamiques?

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

Un exemple d'histogramme complet est: https://gist.github.com/993912

Matt Alcock
la source
5
J'ai trouvé la méthode dans cette réponse plus facile: stackoverflow.com/a/11948988/511203
Mike Gorski
la manière la plus simple que j'ai trouvée est de faire la largeur et la hauteur svg: 100% et d'appliquer le dimensionnement sur le conteneur DOM (comme div) stackoverflow.com/questions/47076163/…
mtx

Réponses:

316

Il existe une autre façon de faire cela qui ne nécessite pas de redessiner le graphique, et cela implique de modifier les attributs viewBox et preserveAspectRatio sur l' <svg>élément:

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

Mise à jour le 24/11/15 : la plupart des navigateurs modernes peuvent déduire le rapport d'aspect des éléments SVG à partir du viewBox, vous n'aurez donc pas besoin de maintenir la taille du graphique à jour. Si vous devez prendre en charge des navigateurs plus anciens, vous pouvez redimensionner votre élément lorsque la fenêtre se redimensionne comme suit:

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

Et le contenu svg sera mis à l'échelle automatiquement. Vous pouvez voir un exemple de ceci (avec quelques modifications) ici : redimensionnez simplement la fenêtre ou le volet en bas à droite pour voir comment il réagit.

Shawn Allen
la source
1
J'aime beaucoup cette approche, 2 questions. 1 La Viewbox fonctionne-t-elle sur plusieurs navigateurs? 2. Dans votre exemple, les cercles sont redimensionnés mais ne tiennent pas dans la fenêtre. Quelque chose ne va pas avec la boîte de vue svg ou le rapport d'aspect.
Matt Alcock
4
J'adore cette approche, je ne suis pas sûr que ce qui précède fonctionne correctement. Comme Matt le dit, les cercles se redimensionnent mais la distance entre la gauche du graphique et le premier cercle ne se redimensionne pas au même rythme que les cercles. J'ai essayé de jouer un peu sur jsfiddle mais je n'ai pas pu le faire fonctionner comme prévu en utilisant Google Chrome. Avec quels navigateurs viewBox et reserveAspectRatio sont-ils compatibles?
Chris Withers
9
En fait, définir simplement viewBox et conserverAspectRatio sur un SVG avec une hauteur de largeur définie à 100% du parent, et le parent étant une grille d'amorçage / flex fonctionne pour moi sans gérer l'événement de redimensionnement
Deepu
2
Confirmer que Deepu est correct. Fonctionne avec Bootstrap / flex grid. Merci @Deepu.
Mike O'Connor
3
Remarque: dans plusieurs tutoriels d3 1.) dans le html la largeur et la hauteur svg sont définies et 2.) la visualisation est effectuée via un onload appel de fonction. Si la largeur et la hauteur svg sont définies dans le html réel et que vous utilisez la technique ci-dessus, l'image ne sera pas évolutive.
SumNeuron
34

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

Le code 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;
}

Plus d'informations / tutoriels:

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

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

cminatti
la source
1
Dans mon cas, padding-bottom: 100% fera que mon conteneur aura un rembourrage supplémentaire en bas, donc je l'ai changé à 50% pour le supprimer. Utilisé votre code et réussi à rendre le svg réactif, merci.
V-SHY
20

J'ai codé un petit résumé pour résoudre ce problème.

Le modèle de solution général est le suivant:

  1. Répartissez le script en fonctions de calcul et de dessin.
  2. Assurez-vous que la fonction de dessin dessine de manière dynamique et dépend des variables de largeur et de hauteur de visualisation (la meilleure façon de le faire est d'utiliser l'api d3.scale)
  3. Lier / chaîner le dessin à un élément de référence dans le balisage. (J'ai utilisé jquery pour cela, alors je l'ai importé).
  4. N'oubliez pas de le supprimer s'il est déjà dessiné. Obtenez les dimensions de l'élément référencé à l'aide de jquery.
  5. Lier / chaîner la fonction de dessin à la fonction de redimensionnement de la fenêtre. Introduisez un anti-rebond (timeout) sur cette chaîne pour vous assurer que nous ne redessinerons qu'après un timeout.

J'ai également ajouté le script d3.js minifié pour la vitesse. L'essentiel est ici: https://gist.github.com/2414111

code retour de référence jquery:

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

Code anti-rebond:

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

Prendre plaisir!

Matt Alcock
la source
Cela ne semble pas fonctionner; si je redimensionne la fenêtre du fichier html inclus dans l'essentiel corrigé, l'histogramme est toujours coupé à mesure que la fenêtre devient plus petite ...
Chris Withers
1
SVG est un format vectoriel. Vous pouvez définir n'importe quelle taille de zone de visualisation virtuelle, puis la mettre à l'échelle aux dimensions réelles. Pas besoin d'utiliser JS.
phil pirozhkov
4

Sans utiliser ViewBox

Voici un exemple de solution qui ne repose pas sur l'utilisation d'un viewBox:

La clé est de mettre à jour la gamme des échelles qui sont utilisées pour placer les données.

Tout d'abord, calculez votre rapport hauteur / largeur d'origine:

var ratio = width / height;

Puis, sur chaque Redimensionner, mettez à jour le rangede xet y:

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

Notez que la hauteur est basée sur la largeur et le rapport hauteur / largeur afin que vos proportions d'origine soient conservées.

Enfin, "redessinez" le graphique - mettez à jour tout attribut qui dépend de l'une des échelles 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); });
}

Notez qu'en redimensionnant le, rectsvous pouvez utiliser la limite supérieure du rangede y, plutôt que d'utiliser explicitement la hauteur:

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

Paul Murray
la source
3

Si vous utilisez d3.js à c3.js, la solution au problème de réactivité est assez simple:

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

où le code HTML généré ressemble à:

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

La réponse de Shawn Allen était excellente. Mais vous ne voudrez peut-être pas le faire à chaque fois. Si vous l'hébergez sur vida.io , vous obtenez automatiquement une réponse pour votre visualisation svg.

Vous pouvez obtenir un iframe réactif avec ce simple code d'intégration:

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

Divulgation: Je construis cette fonction à vida.io .

Phuoc Do
la source
2

Dans le cas où vous utilisez un wrapper d3 comme plottable.js , sachez que la solution la plus simple pourrait être d' ajouter un écouteur d'événements , puis d'appeler une fonction de rafraîchissement ( redrawdans plottable.js). Dans le cas de plottable.js, cela fonctionnera parfaitement (cette approche est mal documentée):

    window.addEventListener("resize", function() {
      table.redraw();
    });
Riley Steele Parsons
la source
Il serait probablement préférable de neutraliser cette fonction pour éviter de déclencher le redessin aussi vite que possible
Ian
1

Au cas où les gens visiteraient encore cette question - voici ce qui a fonctionné pour moi:

  • Mettez l'iframe dans un div et utilisez css pour ajouter un remplissage de, disons, 40% à ce div (le pourcentage en fonction du rapport d'aspect que vous voulez). Réglez ensuite la largeur et la hauteur de l'iframe lui-même à 100%.

  • Dans le document html contenant le graphique à charger dans l'iframe, définissez la largeur sur la largeur du div auquel le svg est ajouté (ou sur la largeur du corps) et définissez la hauteur sur la largeur * rapport d'aspect.

  • Écrivez une fonction qui recharge le contenu de l'iframe lors du redimensionnement de la fenêtre, afin d'adapter la taille du graphique lorsque les gens tournent leur téléphone.

Exemple ici sur mon site Web: http://dirkmjk.nl/en/2016/05/embedding-d3js-charts-responsive-website

MISE À JOUR 30 déc.2016

L'approche que j'ai décrite ci-dessus présente certains inconvénients, surtout qu'elle ne tient pas compte de la hauteur des titres et des légendes qui ne font pas partie du svg créé par D3. Depuis, je suis tombé sur ce que je pense être une meilleure approche:

  • Réglez la largeur du graphique D3 sur la largeur de la division à laquelle il est attaché et utilisez le rapport d'aspect pour définir sa hauteur en conséquence;
  • Demandez à la page intégrée d'envoyer sa hauteur et son URL à la page parente à l'aide du postMessage de HTML5;
  • Sur la page parent, utilisez l'URL pour identifier l'iframe correspondante (utile si vous avez plusieurs iframe sur votre page) et mettez à jour sa hauteur à la hauteur de la page incorporée.

Exemple ici sur mon site Web: http://dirkmjk.nl/en/2016/12/embedding-d3js-charts-responsive-website-better-solution

dirkmjk
la source
0

L'un des principes de base de la jointure de données D3 est qu'elle est idempotente. En d'autres termes, si vous évaluez à plusieurs reprises une jointure de données avec les mêmes données, la sortie rendue est la même. Par conséquent, tant que vous restituez votre graphique correctement, en prenant soin de vos sélections d'entrée, de mise à jour et de sortie - tout ce que vous avez à faire lorsque la taille change, est de restituer le graphique dans son intégralité.

Il y a quelques autres choses que vous devez faire, l'une consiste à annuler le rebond du gestionnaire de redimensionnement de la fenêtre afin de le limiter. De plus, au lieu de coder en dur les largeurs / hauteurs, cela devrait être obtenu en mesurant l'élément contenant.

Comme alternative, voici votre graphique rendu à l'aide de d3fc , qui est un ensemble de composants D3 qui gèrent correctement les jointures de données. Il a également un graphique cartésien qui le mesure contenant un élément, ce qui facilite la création de graphiques «réactifs»:

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

Vous pouvez le voir en action dans ce codepen:

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

où vous pouvez redimensionner la fenêtre et vérifier que le graphique est correctement restitué.

Veuillez noter qu'en tant que divulgation complète, je suis l'un des mainteneurs de d3fc.

ColinE
la source
Merci d'avoir gardé vos réponses à jour! J'ai vu les mises à jour récentes de certains de vos messages, et j'apprécie vraiment que les gens revoient leur propre contenu! Cependant, comme mise en garde, cette révision ne répond plus entièrement à la question, car vous éditez dans D3 v4 , alors que OP fait clairement référence à v3 , ce qui pourrait dérouter les futurs lecteurs. Personnellement, pour mes propres réponses, j'ai tendance à ajouter une section v4 tout en conservant la section v3 d' origine . Je pense que la v4 vaut bien sa propre réponse en ajoutant une référence mutuelle aux deux articles. Mais c'est juste mes deux cents ...
altocumulus
C'est vraiment un bon point! Il est si facile de se laisser emporter et de se concentrer uniquement sur l'avenir et les nouveautés. À en juger par les dernières questions d3, je pense que beaucoup de gens utilisent encore la v3. Il est également frustrant de constater à quel point certaines différences sont subtiles! Je revisiterai mes modifications :-)
ColinE
0

J'éviterais de redimensionner / cocher des solutions comme la peste, car elles sont inefficaces et peuvent causer des problèmes dans votre application (par exemple, une info-bulle recalcule la position à laquelle elle devrait apparaître lors du redimensionnement de la fenêtre, puis un instant plus tard, votre graphique se redimensionne également et la page se remet à jour). mises en page et maintenant votre info-bulle est à nouveau erronée).

Vous pouvez simuler ce comportement dans certains anciens navigateurs qui ne le prennent pas correctement en charge, comme IE11, en utilisant un <canvas>élément qui conserve son aspect.

Étant donné 960x540 qui est un aspect 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>
Dominique
la source
0

Beaucoup de réponses complexes ici.

Fondamentalement, tout ce que vous devez faire est d'abandonner les attributs widthet heighten faveur de l' viewBoxattribut:

width = 500;
height = 500;

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

Si vous avez des marges, vous pouvez simplement les ajouter dans la largeur / hauteur, puis les ajouter gpar la suite et les transformer comme vous le feriez normalement.

Jared Wilber
la source
-1

Vous pouvez également utiliser bootstrap 3 pour adapter la taille d'une visualisation. Par exemple, nous pouvons configurer le code HTML comme:

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

J'ai mis en place une hauteur fixe en raison de mes besoins, mais vous pouvez également laisser la taille auto. Le "col-sm-6 col-md-4" rend le div réactif pour différents appareils. Vous pouvez en savoir plus sur http://getbootstrap.com/css/#grid-example-basic

Nous pouvons accéder au graphique à l'aide de la vue mois id .

Je n'entrerai pas dans les détails du code d3, je n'entrerai que la partie nécessaire à l'adaptation aux différentes tailles d'écran.

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

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

La largeur est définie en obtenant la largeur du div avec la vue mensuelle id.

La hauteur dans mon cas ne doit pas inclure toute la zone. J'ai également du texte au-dessus de la barre, je dois donc également calculer cette zone. C'est pourquoi j'ai identifié la zone du texte avec l'id responsivetext. Pour calculer la hauteur autorisée de la barre, j'ai soustrait la hauteur du texte de la hauteur de la div.

Cela vous permet d'avoir une barre qui adoptera toutes les différentes tailles d'écran / div. Ce n'est peut-être pas la meilleure façon de le faire, mais cela fonctionne sûrement pour les besoins de mon projet.

arlindmusliu
la source
1
Salut, j'ai essayé, mais le contenu svg n'est pas redimensionné, comme le calendrier ou les graphiques.