d3 Synchronisieren von 2 verschiedenen Zoomverhalten


11

Ich habe das folgende d3 / d3fc-Diagramm

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

Das Diagramm hat ein Zoomverhalten für den Hauptbereich und ein separates Zoomverhalten für die y-Achse. Die y-Achse kann zum erneuten Skalieren gezogen werden.

Das Problem, das ich nicht lösen kann, besteht darin, dass nach dem Ziehen der y-Achse zum erneuten Skalieren und anschließenden Schwenken des Diagramms ein "Sprung" im Diagramm auftritt.

Offensichtlich haben die 2-Zoom-Verhaltensweisen eine Unterbrechung und müssen synchronisiert werden, aber ich zerbreche mir den Kopf, um dies zu beheben.

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

Das gewünschte Verhalten besteht darin, die Achse zu zoomen, zu schwenken und / oder neu zu skalieren, um die Transformation immer von jedem Ort aus anzuwenden, an dem die vorherige Aktion beendet wurde, ohne "Sprünge".

Antworten:


10

OK, also habe ich es noch einmal versucht - wie in meiner vorherigen Antwort erwähnt , ist das größte Problem, das Sie überwinden müssen, dass der d3-Zoom nur eine symmetrische Skalierung zulässt. Dies wurde viel diskutiert , und ich glaube, Mike Bostock wird dies in der nächsten Version ansprechen.

Um das Problem zu beheben, müssen Sie das Verhalten des Mehrfachzooms verwenden. Ich habe ein Diagramm erstellt, das drei enthält, eines für jede Achse und eines für den Plotbereich. Das X- und Y-Zoomverhalten wird zum Skalieren der Achsen verwendet. Immer wenn ein Zoomereignis durch das X- und Y-Zoomverhalten ausgelöst wird, werden deren Übersetzungswerte in den Plotbereich kopiert. Wenn eine Übersetzung im Plotbereich erfolgt, werden die x & y-Komponenten ebenfalls in das jeweilige Achsenverhalten kopiert.

Die Skalierung des Plotbereichs ist etwas komplizierter, da das Seitenverhältnis beibehalten werden muss. Um dies zu erreichen, speichere ich die vorherige Zoomtransformation und verwende das Skalendelta, um eine geeignete Skalierung für das X- und Y-Zoomverhalten zu erarbeiten.

Der Einfachheit halber habe ich all dies in eine Diagrammkomponente zusammengefasst:


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

Hier ist ein Stift mit dem Arbeitsbeispiel:

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


Colin, vielen Dank, dass du es noch einmal versucht hast. Ich habe Ihren Codepen geöffnet und festgestellt, dass er nicht wie erwartet funktioniert. Wenn Sie in meinem Codepen die Y-Achse ziehen, wird das Diagramm neu skaliert (dies ist das gewünschte Verhalten). Wenn Sie in Ihrem Codepen die Achse ziehen, wird das Diagramm einfach verschoben.
Parlament

1
Ich habe es geschafft, eine zu erstellen, mit der Sie sowohl die y-Skala als auch den Plotbereich codepen.io/colineberhardt-the-bashful/pen/mdeJyrK ziehen können - aber das Zoomen im Plotbereich ist eine ziemliche Herausforderung
ColinE

5

Es gibt einige Probleme mit Ihrem Code, eines, das leicht zu lösen ist, und eines, das nicht ...

Erstens speichert der d3-Zoom eine Transformation für die ausgewählten DOM-Elemente. Sie können dies über die __zoomEigenschaft anzeigen. Wenn der Benutzer mit dem DOM-Element interagiert, wird diese Transformation aktualisiert und Ereignisse ausgegeben. Wenn Sie unterschiedliche Zoomverhaltensweisen haben, die beide das Schwenken / Zoomen eines einzelnen Elements steuern, müssen Sie diese Transformationen synchronisieren.

Sie können die Transformation wie folgt kopieren:

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

Dies führt jedoch auch dazu, dass Zoomereignisse auch vom Zielverhalten ausgelöst werden.

Eine Alternative besteht darin, direkt in die Transformationseigenschaft "Stashed" zu kopieren:

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

Es gibt jedoch ein größeres Problem mit dem, was Sie erreichen möchten. Die d3-Zoom-Transformation wird als 3 Komponenten einer Transformationsmatrix gespeichert:

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

Infolgedessen kann der Zoom nur eine symmetrische Skalierung zusammen mit einer Übersetzung darstellen. Ihr asymmetrischer Zoom als auf die x-Achse angewendet kann durch diese Transformation nicht originalgetreu dargestellt und erneut auf den Plotbereich angewendet werden.


Danke Colin, ich wünschte, irgendetwas davon hätte für mich Sinn gemacht, aber ich verstehe nicht, was die Lösung ist. Ich werde dieser Frage ein Kopfgeld von 500 hinzufügen, sobald ich kann, und hoffentlich kann jemand helfen, meinen Codepen zu reparieren.
Parlament

Keine Sorge, kein leicht zu behebendes Problem, aber ein Kopfgeld könnte einige Hilfsangebote anziehen.
ColinE

2

Dies ist eine bevorstehende Funktion , wie bereits von @ColinE erwähnt. Der ursprüngliche Code führt immer einen "zeitlichen Zoom" durch, der nicht mit der Transformationsmatrix synchronisiert ist.

Die beste Problemumgehung besteht darin, den xExtentBereich so anzupassen , dass in der Grafik davon ausgegangen wird, dass sich an den Seiten zusätzliche Kerzen befinden. Dies kann durch Hinzufügen von Pads an den Seiten erreicht werden. Das accessors, anstatt zu sein,

[d => d.date]

wird,

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

Nebenbemerkung: Beachten Sie, dass es eine padFunktion gibt, die dies tun sollte, aber aus irgendeinem Grund nur einmal funktioniert und nie wieder aktualisiert wird. Deshalb wird sie als hinzugefügt accessors.

Nebenbemerkung 2: Der addDaysEinfachheit halber wurde eine Funktion als Prototyp hinzugefügt (nicht das Beste).

Jetzt ändert das Zoomereignis unseren X-Zoomfaktor xZoom.

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

Es ist wichtig, das Differential direkt aus zu lesenwheelDelta . Hier ist die nicht unterstützte Funktion: Wir können nicht lesen, t.xda sie sich ändert, selbst wenn Sie die Y-Achse ziehen.

Berechnen Sie abschließend neu, chart.xDomain(xExtent(data.series));damit der neue Umfang verfügbar ist.

Die funktionierende Demo ohne Sprung finden Sie hier: https://codepen.io/adelriosantiago/pen/QWjwRXa?editors=0011

Behoben: Zoomumkehr, verbessertes Verhalten auf dem Trackpad.

Technisch können Sie auch yExtentdurch Hinzufügen von Extra d.highund d.low's optimieren . Oder sogar beides xExtentund yExtentum die Transformationsmatrix überhaupt nicht zu verwenden.


Vielen Dank. Leider ist das Zoomverhalten hier wirklich durcheinander. Der Zoom fühlt sich sehr ruckartig an und kehrt in einigen Fällen sogar mehrmals die Richtung um (mit dem MacBook Trackpad). Das Zoomverhalten muss dem des Originalstifts entsprechen.
Parlament

Ich habe den Stift mit einigen Verbesserungen aktualisiert, insbesondere dem Umkehrzoom.
Adelriosantiago

1
Ich werde dir das Kopfgeld für deine Mühe gewähren und ich beginne ein zweites Kopfgeld, weil ich noch eine Antwort mit besserem Zoomen / Schwenken brauche. Leider ist diese Methode, die auf Delta-Änderungen basiert, beim Zoomen und beim Schwenken immer noch ruckelig und nicht flüssig. Ich vermute, dass Sie den Faktor wahrscheinlich endlos anpassen können und trotzdem kein reibungsloses Verhalten erhalten. Ich suche nach einer Antwort, die das Zoom- / Schwenkverhalten des Originalstifts nicht ändert.
Parlament

1
Ich habe auch den Originalstift so eingestellt, dass er nur entlang der X-Achse zoomt. Das ist das verdiente Verhalten, und mir ist jetzt klar, dass es die Antwort erschweren kann.
Parlament
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.