Quelle est la meilleure façon de faire un d3.mise en page de visualisation js responsive?
Supposons que j'ai un script d'histogramme qui construit un graphique svg 960 500. Comment puis-je rendre cela réactif afin de redimensionner les largeurs et les 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>
Exemple complet histogramme gist est: https://gist.github.com/993912
11 réponses
Il y a une autre façon de le faire qui ne nécessite pas de redessiner le graphique, et cela implique de modifier les attributs viewBox et preserveAspectRatio sur l'élément <svg>
:
<svg id="chart" width="960" height="500"
viewBox="0 0 960 500"
preserveAspectRatio="xMidYMid meet">
</svg>
Mise à jour 11/24/15: la plupart des navigateurs modernes peuvent déduire le rapport d'aspect des éléments SVG du viewBox
, vous n'avez donc pas besoin de garder la taille du graphique à jour. Si vous avez besoin de prendre en charge les navigateurs plus anciens, vous pouvez redimensionner votre élément lorsque la fenêtre se redimensionne comme alors:
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: il suffit de redimensionner la fenêtre ou le volet inférieur droit pour voir comment il réagit.
Recherchez 'responsive SVG' 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'infos / tutoriels:
Http://demosthenes.info/blog/744/Make-SVG-Responsive
Http://soqr.fr/testsvg/embed-svg-liquid-layout-responsive-web-design.php
J'ai codé un petit essentiel pour résoudre ce problème.
Le modèle de solution générale est le suivant:
- découper le script en fonctions de calcul et de dessin.
- Assurez-vous que la fonction de dessin dessine dynamiquement et variables de largeur et de hauteur de visualisation (la meilleure façon de le faire est pour utiliser le d3.api d'échelle)
- lier / enchaîner le dessin à une référence de l'élément dans les balises. (J'ai utilisé jquery pour cela, donc importé).
- n'oubliez pas de l'enlever si c'est déjà dessiné. Obtenir les dimensions de l'élément référencé utilisant jquery.
- lier / enchaîner la fonction de tirage à la fenêtre de la fonction de redimensionnement. Introduire un debounce (timeout) à cette chaîne pour s'assurer que nous ne redessinons après un délai d'attente.
J'ai également ajouté le d3 minifié.script js pour la vitesse. L'essentiel est ici: https://gist.github.com/2414111
Code de référence Jquery:
$(reference).empty()
var width = $(reference).width();
Code 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);
Profitez!
Sans Utiliser ViewBox
Voici un exemple d'une solution qui ne repose pas sur l'utilisation d'un viewBox
:
La clé consiste à mettre à jour la plage des échelles qui sont utilisées pour placer des données.
Tout d'abord, calculez votre rapport d'aspect d'origine:
var ratio = width / height;
, Puis, sur chaque redimensionner, mettre à jour le range
de x
et 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 d'aspect, de sorte que vos proportions d'origine sont exploité.
Enfin, "redessiner" le graphique-mettre à jour tout attribut qui dépend de l'une des échelles x
ou 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 que dans le redimensionnement du rects
Vous pouvez utiliser la limite supérieure du range
de y
, plutôt que d'utiliser explicitement la hauteur:
.attr("y", function(d) { return y.range()[1] - y(d.y); })
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 ratio = width / height;
var x = d3.scale.ordinal()
.domain(histogram.map(function(d) {
return d.x;
}))
var y = d3.scale.linear()
.domain([0, d3.max(histogram, function(d) {
return d.y;
})])
var svg = d3.select("body").append("svg")
.attr("width", "100%")
.attr("height", height);
var rects = svg.selectAll("rect").data(histogram);
rects.enter().append("rect");
function redraw() {
rects.attr("width", x.rangeBand())
.attr("x", function(d) {
return x(d.x);
})
// .attr("y", function(d) { return height - y(d.y); })
.attr("y", function(d) {
return y.range()[1] - y(d.y);
})
.attr("height", function(d) {
return y(d.y);
});
}
function resize() {
x.rangeRoundBands([0, window.innerWidth]);
y.range([0, window.innerWidth / ratio]);
svg.attr("height", window.innerHeight);
}
d3.select(window).on('resize', function() {
resize();
redraw();
})
resize();
redraw();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Si vous utilisez d3.js par c3.js la solution au problème de la 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>
La réponse de Shawn Allen était géniale. Mais vous ne voudrez peut-être pas le faire à chaque fois. Si vous l'hébergez sur vida.io , vous obtenez une réponse automatique pour votre visualisation svg.
Vous pouvez obtenir un iframe réactif avec ce code d'intégration 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/
Divulgation: je construis cette fonctionnalité à vida.io.
Dans le cas où vous utilisez un wrapper d3 comme plottable.js , sachez que la solution la plus simple peut être d'ajouter un écouteur d'événement, puis d'appeler une fonction redraw (redraw
dans plottable.js). Dans le cas de plottable.js cela fonctionnera parfaitement (cette approche est mal documentée):
window.addEventListener("resize", function() {
table.redraw();
});
L'un des principes de base de la D3 data-join 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 Entrée, mise à jour et 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 devriez faire, l'une est de-bounce le gestionnaire de redimensionnement de la fenêtre dans l'ordre à l'étrangle. En outre, plutôt que des largeurs / hauteurs de codage en dur, cela devrait être réalisé en mesurant l'élément contenant.
Comme alternative, voici votre graphique rendu en utilisant 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 rendu.
Veuillez noter, comme une divulgation complète, je suis l'un des responsables de d3fc.
J'éviterais les solutions de redimensionnement/coche comme la peste car elles sont inefficaces et peuvent causer des problèmes dans votre application (par exemple, une infobulle calcule à nouveau la position qu'elle devrait apparaître sur le redimensionnement de la fenêtre, puis un moment plus tard votre graphique redimensionne aussi et la page re-mise en page et maintenant votre infobulle est
Vous pouvez simuler ce comportement dans certains navigateurs plus anciens qui ne le supportent pas correctement comme IE11 en utilisant un élément <canvas>
qui maintient 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>
Dans le cas où les gens visitent encore cette question-voici ce qui a fonctionné pour moi:
Enfermez 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). Ensuite, définissez 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 width sur la largeur du div auquel le svg est ajouté (ou sur la largeur du corps) et définissez height largeur * rapport d'aspect.
Ecrire une fonction qui recharge le contenu iframe lors du redimensionnement de la fenêtre, de manière à adapter la taille du graphique lorsque les gens font pivoter 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 quelques inconvénients, d'autant plus qu'elle ne prend en compte la hauteur d'aucun titre et légendes qui ne font pas partie du svg créé par D3. J'ai depuis rencontré ce que je pense être une meilleure approche:
- Définissez la largeur du graphique D3 sur la largeur du div auquel 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 parent en utilisant postMessage de HTML5;
- sur la page parent, utilisez l'url pour identifier l'iframe correspondant (utile si vous avez plus d'un iframe sur votre page) et mettez à jour sa hauteur à la hauteur de la page intégrée.
Exemple ici sur mon site: http://dirkmjk.nl/en/2016/12/embedding-d3js-charts-responsive-website-better-solution
Vous pouvez aussi utiliser bootstrap 3 pour s'adapter à la taille d'une visualisation. Par exemple, nous pouvons configurer le code HTML comme suit:
<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 l'id Vue mensuelle.
Je Je n'entrerai pas dans beaucoup de détails sur le code d3, Je n'entrerai que la partie nécessaire pour s'adapter à 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 de la div avec l'ID month-view.
La hauteur dans mon cas ne devrait pas inclure toute la zone. J'ai aussi du texte au-dessus de la barre, donc j'ai aussi besoin de 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 à partir 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 répond sûrement aux besoins de mon projet.