d3 sincronizzando 2 comportamenti zoom separati


11

Ho il seguente grafico d3 / d3fc

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

Il grafico presenta un comportamento di zoom per l'area principale e un comportamento di zoom separato per l'asse y. L'asse y può essere trascinato per ridimensionare.

Il problema che sto riscontrando nella risoluzione dei problemi è che dopo aver trascinato l'asse y per ridimensionare e successivamente eseguire una panoramica del grafico, c'è un "salto" nel grafico.

Ovviamente i 2 comportamenti di zoom hanno una disconnessione e devono essere sincronizzati ma sto scervellando il mio cervello cercando di risolvere questo problema.

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

Il comportamento desiderato è lo zoom, il pan e / o il riscalamento dell'asse per applicare sempre la trasformazione da qualsiasi punto dell'azione precedente, senza "salti".

Risposte:


10

OK, quindi ho fatto un altro tentativo - come menzionato nella mia precedente risposta , il problema più grande che devi superare è che lo zoom d3 consente solo il ridimensionamento simmetrico. Questo è qualcosa che è stato ampiamente discusso , e credo che Mike Bostock si occuperà di questo nella prossima versione.

Quindi, al fine di superare il problema, è necessario utilizzare il comportamento di zoom multiplo. Ho creato un grafico con tre, uno per ciascun asse e uno per l'area del grafico. I comportamenti di zoom X e Y vengono utilizzati per ridimensionare gli assi. Ogni volta che un comportamento di zoom viene generato dai comportamenti di zoom X & Y, i loro valori di traduzione vengono copiati nell'area della trama. Allo stesso modo, quando si verifica una traslazione nell'area del tracciato, i componenti x & y vengono copiati nei rispettivi comportamenti degli assi.

Il ridimensionamento nell'area della trama è un po 'più complicato in quanto dobbiamo mantenere le proporzioni. Per raggiungere questo obiettivo, memorizzo la precedente trasformazione dello zoom e utilizzo il delta di scala per elaborare una scala adatta da applicare ai comportamenti di zoom X & Y.

Per comodità, ho racchiuso tutto questo in un componente grafico:


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

Ecco una penna con l'esempio funzionante:

https://codepen.io/colineberhardt-the-bashful/pen/qBOEEGJ


Colin, grazie mille per averci dato un'altra possibilità. Ho aperto il tuo codice e ho notato che non funziona come previsto. Nel mio codice, trascinare l'asse Y ridimensiona il grafico (questo è il comportamento desiderato). Nel tuo codice, trascinando l'asse si esegue semplicemente una panoramica del grafico.
parlamento

1
Sono riuscito a crearne uno che ti consente di trascinare sia la scala y che l'area della trama codepen.io/colineberhardt-the-bashful/pen/mdeJyrK - ma lo zoom nell'area della trama è una vera sfida
ColinE

5

Ci sono un paio di problemi con il tuo codice, uno che è facile da risolvere e uno che non è ...

Innanzitutto, lo zoom d3 funziona memorizzando una trasformazione sugli elementi DOM selezionati: puoi vederlo tramite la __zoomproprietà. Quando l'utente interagisce con l'elemento DOM, questa trasformazione viene aggiornata e gli eventi emessi. Pertanto, se si devono adottare comportamenti di zoom diversi, entrambi i quali controllano il pan / zoom di un singolo elemento, è necessario mantenere sincronizzate queste trasformazioni.

È possibile copiare la trasformazione come segue:

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

Tuttavia, ciò causerà anche l'attivazione di eventi di zoom anche dal comportamento target.

Un'alternativa è copiare direttamente nella proprietà di trasformazione "stashed":

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

Tuttavia, c'è un problema più grande con ciò che stai cercando di ottenere. La trasformazione zoom d3 è memorizzata come 3 componenti di una matrice di trasformazione:

https://github.com/d3/d3-zoom#zoomTransform

Di conseguenza, lo zoom può rappresentare solo un ridimensionamento simmetrico insieme a una traduzione. Lo zoom asimmetrico applicato all'asse x non può essere fedelmente rappresentato da questa trasformazione e riapplicato all'area della trama.


Grazie Colin, vorrei che tutto ciò avesse senso per me, ma non capisco quale sia la soluzione. Aggiungerò una ricompensa di 500 a questa domanda non appena posso e spero che qualcuno possa aiutarmi a sistemare il mio codice.
Parlamento,

Nessun problema, non è un problema facile da risolvere, ma una taglia potrebbe attirare alcune offerte di aiuto.
ColinE

2

Questa è una funzionalità imminente , come già notato da @ColinE. Il codice originale esegue sempre uno "zoom temporale" non sincronizzato dalla matrice di trasformazione.

La soluzione migliore è modificare la xExtentgamma in modo che il grafico ritenga che ci siano candele aggiuntive sui lati. Ciò può essere ottenuto aggiungendo cuscinetti ai lati. Il accessors, invece di essere,

[d => d.date]

diventa,

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

Sidenote: Nota che esiste una padfunzione che dovrebbe farlo, ma per qualche motivo funziona solo una volta e non si aggiorna mai più, ecco perché viene aggiunto come accessors.

Sidenote 2: Funzione addDaysaggiunta come prototipo (non la cosa migliore da fare) solo per semplicità.

Ora la modifica di eventi zoom nostro fattore di zoom X, xZoom,

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

È importante leggere il differenziale direttamente dawheelDelta . Ecco dove si trova la funzione non supportata: non possiamo leggere t.xpoiché cambierà anche se trascini l'asse Y.

Infine, ricalcola chart.xDomain(xExtent(data.series)); modo che la nuova estensione sia disponibile.

Guarda la demo funzionante senza il salto qui: https://codepen.io/adelriosantiago/pen/QWjwRXa?editors=0011

Risolto: inversione dello zoom, comportamento migliorato sul trackpad.

Tecnicamente si potrebbe anche modificare yExtentcon l'aggiunta di più d.highe d.lows'. O anche entrambi xExtente yExtentper non usare affatto la matrice di trasformazione.


Grazie. Purtroppo il comportamento dello zoom qui è davvero incasinato. Lo zoom sembra molto a scatti e in alcuni casi inverte anche la direzione più volte (usando il trackpad del macbook). Il comportamento dello zoom deve rimanere lo stesso della penna originale.
parlamento

Ho aggiornato la penna con alcuni miglioramenti, in particolare lo zoom di retromarcia.
adelriosantiago,

1
Ti assegnerò la generosità per i tuoi sforzi e inizierò una seconda ricompensa perché ho ancora bisogno di una risposta con zoom / panoramica migliori. Sfortunatamente questo metodo basato sui cambiamenti delta è ancora a scatti e non è fluido quando si esegue lo zoom e quando si esegue la panoramica è troppo veloce. Ho il sospetto che tu possa probabilmente regolare il fattore all'infinito e comunque non avere un comportamento regolare. Sto cercando una risposta che non cambi il comportamento di zoom / panoramica della penna originale.
parlamento il

1
Ho anche regolato la penna originale per zoomare solo lungo l'asse X. Questo è il comportamento meritato e ora mi rendo conto che potrebbe complicare la risposta.
parlamento il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.