d3 synchronisation de 2 comportements de zoom distincts

11

J'ai le graphique d3 / d3fc suivant

https://codepen.io/parliament718/pen/BaNQPXx

Le graphique a un comportement de zoom pour la zone principale et un comportement de zoom distinct pour l'axe des y. L'axe des y peut être déplacé pour être mis à l'échelle.

Le problème que j'ai du mal à résoudre est qu'après avoir fait glisser l'axe des y pour redimensionner et ensuite faire un panoramique sur le graphique, il y a un "saut" dans le graphique.

Évidemment, les 2 comportements de zoom ont une déconnexion et doivent être synchronisés, mais j'essaye de résoudre ce problème.

const mainZoom = zoom()
    .on('zoom', () => {
       xScale.domain(t.rescaleX(x2).domain());
       yScale.domain(t.rescaleY(y2).domain());
    });

const yAxisZoom = zoom()
    .on('zoom', () => {
        const t = event.transform;
        yScale.domain(t.rescaleY(y2).domain());
        render();
    });

const yAxisDrag = drag()
    .on('drag', (args) => {
        const factor = Math.pow(2, -event.dy * 0.01);
        plotArea.call(yAxisZoom.scaleBy, factor);
    });

Le comportement souhaité est le zoom, le panoramique et / ou le redimensionnement de l'axe pour toujours appliquer la transformation de l'endroit où l'action précédente s'est terminée, sans aucun "saut".

parlement
la source

Réponses:

10

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

ColinE
la source
Colin, merci beaucoup d'avoir donné un autre coup de pouce. J'ai ouvert votre codepen et j'ai remarqué qu'il ne fonctionne pas comme prévu. Dans mon codepen, faire glisser l'axe Y redimensionne le graphique (c'est le comportement souhaité). Dans votre codepen, faire glisser l'axe déplace simplement le graphique.
parlement
1
J'ai réussi à en créer un qui vous permet de faire glisser à la fois l'échelle y et la zone de tracé codepen.io/colineberhardt-the-bashful/pen/mdeJyrK - mais zoomer dans la zone de tracé est tout un défi
ColinE
5

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 __zoomproprié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:

selection.call(zoom.transform, d3.event.transform);

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

selection.node().__zoom = d3.event.transform;

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

ColinE
la source
Merci Colin, je souhaite que tout cela ait un sens pour moi, mais je ne comprends pas quelle est la solution. J'ajouterai une prime de 500 à cette question dès que je le pourrai et j'espère que quelqu'un pourra m'aider à réparer mon codepen.
parlement le
Pas de soucis, pas un problème facile à résoudre, mais une prime pourrait attirer des offres d'aide.
ColinE
2

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 xExtentplage 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. Le accessors, au lieu d'être,

[d => d.date]

devient,

[
  () => new Date(taken[0].date.addDays(-xZoom)), // Left pad
  d => d.date,
  () => new Date(taken[taken.length - 1].date.addDays(xZoom)) // Right pad
]

Sidenote: Notez qu'il existe une padfonction 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 que accessors.

Sidenote 2: Fonction addDaysajouté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,

zoomFactor = Math.sign(d3.event.sourceEvent.wheelDelta) * -5;
if (zoomFactor) xZoom += zoomFactor;

Il est important de lire directement le différentielwheelDelta . C'est là que se trouve la fonctionnalité non prise en charge: nous ne pouvons pas lire t.xcar 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 yExtenten ajoutant des « d.highet d.low» supplémentaires . Ou même les deux xExtentet yExtentpour éviter d'utiliser la matrice de transformation.

adelriosantiago
la source
Je vous remercie. Malheureusement, le comportement du zoom ici est vraiment foiré. Le zoom est très saccadé et dans certains cas, il inverse même plusieurs fois la direction (en utilisant le trackpad macbook). Le comportement du zoom doit rester le même que le stylet d'origine.
parlement
J'ai mis à jour le stylet avec quelques améliorations, en particulier le zoom inversé.
adelriosantiago
1
Je vais vous attribuer la prime pour votre effort et je commence une deuxième prime parce que j'ai encore besoin d'une réponse avec un meilleur zoom / panoramique. Malheureusement, cette méthode basée sur les changements de delta est toujours saccadée et pas fluide lors du zoom, et lors du panoramique, elle se déplace trop rapidement. Je soupçonne que vous pouvez probablement ajuster le facteur à l'infini et ne pas obtenir un comportement fluide. Je cherche une réponse qui ne change pas le comportement de zoom / panoramique du stylet d'origine.
parlement
1
J'ai également ajusté le stylet d'origine pour zoomer uniquement le long de l'axe X. C'est le comportement mérité et je me rends compte maintenant que cela peut compliquer la réponse.
parlement