OK, j'ai donc essayé à nouveau - comme mentionné dans ma réponse précédente , le plus gros problème que vous devez surmonter est que le zoom d3 ne permet qu'une mise à l'échelle symétrique. C'est quelque chose qui a été largement discuté , et je pense que Mike Bostock en parlera dans la prochaine version.
Donc, pour surmonter le problème, vous devez utiliser plusieurs comportements de zoom. J'ai créé un graphique qui en a trois, un pour chaque axe et un pour la zone de traçage. Les comportements de zoom X & Y sont utilisés pour mettre à l'échelle les axes. Chaque fois qu'un événement de zoom est déclenché par les comportements de zoom X & Y, leurs valeurs de translation sont copiées dans la zone de traçage. De même, lorsqu'une translation se produit sur la zone de traçage, les composants x & y sont copiés dans les comportements d'axe respectifs.
La mise à l'échelle sur la zone de traçage est un peu plus compliquée car nous devons maintenir le rapport hauteur / largeur. Pour ce faire, je stocke la transformation de zoom précédente et j'utilise le delta d'échelle pour déterminer une échelle appropriée à appliquer aux comportements de zoom X & Y.
Pour plus de commodité, j'ai regroupé tout cela dans un composant graphique:
const interactiveChart = (xScale, yScale) => {
const zoom = d3.zoom();
const xZoom = d3.zoom();
const yZoom = d3.zoom();
const chart = fc.chartCartesian(xScale, yScale).decorate(sel => {
const plotAreaNode = sel.select(".plot-area").node();
const xAxisNode = sel.select(".x-axis").node();
const yAxisNode = sel.select(".y-axis").node();
const applyTransform = () => {
// apply the zoom transform from the x-scale
xScale.domain(
d3
.zoomTransform(xAxisNode)
.rescaleX(xScaleOriginal)
.domain()
);
// apply the zoom transform from the y-scale
yScale.domain(
d3
.zoomTransform(yAxisNode)
.rescaleY(yScaleOriginal)
.domain()
);
sel.node().requestRedraw();
};
zoom.on("zoom", () => {
// compute how much the user has zoomed since the last event
const factor = (plotAreaNode.__zoom.k - plotAreaNode.__zoomOld.k) / plotAreaNode.__zoomOld.k;
plotAreaNode.__zoomOld = plotAreaNode.__zoom;
// apply scale to the x & y axis, maintaining their aspect ratio
xAxisNode.__zoom.k = xAxisNode.__zoom.k * (1 + factor);
yAxisNode.__zoom.k = yAxisNode.__zoom.k * (1 + factor);
// apply transform
xAxisNode.__zoom.x = d3.zoomTransform(plotAreaNode).x;
yAxisNode.__zoom.y = d3.zoomTransform(plotAreaNode).y;
applyTransform();
});
xZoom.on("zoom", () => {
plotAreaNode.__zoom.x = d3.zoomTransform(xAxisNode).x;
applyTransform();
});
yZoom.on("zoom", () => {
plotAreaNode.__zoom.y = d3.zoomTransform(yAxisNode).y;
applyTransform();
});
sel
.enter()
.select(".plot-area")
.on("measure.range", () => {
xScaleOriginal.range([0, d3.event.detail.width]);
yScaleOriginal.range([d3.event.detail.height, 0]);
})
.call(zoom);
plotAreaNode.__zoomOld = plotAreaNode.__zoom;
// cannot use enter selection as this pulls data through
sel.selectAll(".y-axis").call(yZoom);
sel.selectAll(".x-axis").call(xZoom);
decorate(sel);
});
let xScaleOriginal = xScale.copy(),
yScaleOriginal = yScale.copy();
let decorate = () => {};
const instance = selection => chart(selection);
// property setters not show
return instance;
};
Voici un stylo avec l'exemple de travail:
https://codepen.io/colineberhardt-the-bashful/pen/qBOEEGJ
Il y a quelques problèmes avec votre code, un qui est facile à résoudre et un qui ne l'est pas ...
Tout d'abord, le zoom d3 fonctionne en stockant une transformation sur les éléments DOM sélectionnés - vous pouvez le voir via la
__zoom
propriété. Lorsque l'utilisateur interagit avec l'élément DOM, cette transformation est mise à jour et des événements sont émis. Par conséquent, si vous devez avoir des comportements de zoom différents qui contrôlent tous les deux le panoramique / zoom d'un seul élément, vous devez garder ces transformations synchronisées.Vous pouvez copier la transformation comme suit:
Cependant, cela entraînera également le déclenchement d'événements de zoom à partir du comportement cible.
Une alternative consiste à copier directement dans la propriété de transformation 'stashed':
Cependant, il y a un plus gros problème avec ce que vous essayez de réaliser. La transformation d3-zoom est stockée en 3 composants d'une matrice de transformation:
https://github.com/d3/d3-zoom#zoomTransform
Par conséquent, le zoom ne peut représenter qu'une mise à l'échelle symétrique avec une translation. Votre zoom asymétrique appliqué à l'axe des x ne peut pas être fidèlement représenté par cette transformation et appliqué de nouveau à la zone de tracé.
la source
Ceci est une fonctionnalité à venir , comme l'a déjà noté @ColinE. Le code d'origine effectue toujours un "zoom temporel" non synchronisé à partir de la matrice de transformation.
La meilleure solution consiste à modifier la
xExtent
plage de sorte que le graphique pense qu'il y a des bougies supplémentaires sur les côtés. Cela peut être réalisé en ajoutant des coussinets sur les côtés. Leaccessors
, au lieu d'être,devient,
Sidenote: Notez qu'il existe une
pad
fonction qui devrait le faire, mais pour une raison quelconque, elle ne fonctionne qu'une seule fois et ne se met jamais à jour, c'est pourquoi elle est ajoutée en tant queaccessors
.Sidenote 2: Fonction
addDays
ajoutée comme prototype (pas la meilleure chose à faire) juste pour plus de simplicité.Maintenant , l'événement de zoom modifie notre facteur de zoom X,
xZoom
,Il est important de lire directement le différentiel
wheelDelta
. C'est là que se trouve la fonctionnalité non prise en charge: nous ne pouvons pas liret.x
car elle changera même si vous faites glisser l'axe Y.Enfin, recalculez
chart.xDomain(xExtent(data.series));
afin que la nouvelle étendue soit disponible.Voir la démo de travail sans le saut ici: https://codepen.io/adelriosantiago/pen/QWjwRXa?editors=0011
Correction: inversion du zoom, comportement amélioré sur le trackpad.
Techniquement, vous pouvez également modifier
yExtent
en ajoutant des «d.high
etd.low
» supplémentaires . Ou même les deuxxExtent
etyExtent
pour éviter d'utiliser la matrice de transformation.la source