d3 synchronizuje 2 osobne zachowania zoomu

11

Mam następujący wykres d3 / d3fc

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

Wykres ma zachowanie powiększenia dla głównego obszaru i osobne zachowanie powiększenia dla osi y. Oś y można przeciągnąć, aby zmienić skalę.

Problem, który mam problem z rozwiązywaniem, polega na tym, że po przeciągnięciu osi y w celu przeskalowania, a następnie przeskanowaniu wykresu na wykresie pojawia się „skok”.

Oczywiście 2 zachowania zoomu są rozłączone i muszą zostać zsynchronizowane, ale staram się to naprawić.

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

Pożądanym zachowaniem jest powiększanie, przesuwanie i / lub przeskalowanie osi, aby zawsze zastosować transformację od miejsca, w którym zakończyła się poprzednia akcja, bez żadnych „skoków”.

parlament
źródło

Odpowiedzi:

10

OK, więc postanowiłem to jeszcze raz - jak wspomniano w mojej poprzedniej odpowiedzi , największym problemem, który musisz rozwiązać, jest to, że zoom d3 pozwala tylko na skalowanie symetryczne. Jest to szeroko dyskutowane i uważam, że Mike Bostock zajmuje się tym w następnym wydaniu.

Tak więc, aby rozwiązać ten problem, musisz użyć funkcji wielokrotnego powiększania. Utworzyłem wykres, który ma trzy, po jednym dla każdej osi i jeden dla obszaru wykresu. Zachowania zoomu X i Y są używane do skalowania osi. Za każdym razem, gdy zachowanie zoomu wywołuje zdarzenie zoomu, ich wartości translacji są kopiowane do obszaru wydruku. Podobnie, gdy występuje translacja w obszarze wydruku, komponenty x i y są kopiowane do odpowiednich zachowań osi.

Skalowanie w obszarze wykresu jest nieco bardziej skomplikowane, ponieważ musimy zachować proporcje. Aby to osiągnąć, przechowuję poprzednią transformację zoomu i używam delty skali, aby opracować odpowiednią skalę do zastosowania dla zachowań zoomu X i Y.

Dla wygody zawarłem to wszystko w składnik wykresu:


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

Oto długopis z działającym przykładem:

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

ColinE
źródło
Colin, wielkie dzięki za to, że dałeś mi to jeszcze raz. Otworzyłem twój codepen i zauważyłem, że nie działa zgodnie z oczekiwaniami. W mojej codepen przeciągnięcie osi Y powoduje ponowne skalowanie wykresu (jest to pożądane zachowanie). Przeciągnięcie osi w codepen po prostu przesuwa wykres.
parlament
1
Udało mi się stworzyć taki, który pozwala przeciągać zarówno w skali y, jak i w obszarze wykresu codepen.io/colineberhardt-the-bashful/pen/mdeJyrK - ale powiększenie obszaru wykresu jest sporym wyzwaniem
ColinE
5

Kod ma kilka problemów, z których jeden jest łatwy do rozwiązania, a drugi nie ...

Po pierwsze, zoom d3 działa poprzez zapisanie transformacji na wybranych elementach DOM - możesz to zobaczyć poprzez __zoomwłaściwość. Gdy użytkownik wchodzi w interakcję z elementem DOM, ta transformacja jest aktualizowana i emitowane są zdarzenia. Dlatego, jeśli musisz zmieniać zachowanie zoomu, z których oba kontrolują przesuwanie / powiększanie pojedynczego elementu, musisz synchronizować te transformacje.

Możesz skopiować transformację w następujący sposób:

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

Spowoduje to jednak również, że zdarzenia zoom będą uruchamiane również z zachowania docelowego.

Alternatywą jest skopiowanie bezpośrednio do właściwości transformacji „ukrytych”:

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

Istnieje jednak większy problem z tym, co próbujesz osiągnąć. Transformacja d3-zoom jest przechowywana jako 3 elementy macierzy transformacji:

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

W rezultacie powiększenie może reprezentować jedynie symetryczne skalowanie wraz z tłumaczeniem. Twój asymetryczny zoom zastosowany do osi X nie może być wiernie reprezentowany przez tę transformację i ponownie zastosowany do obszaru kreślenia.

ColinE
źródło
Dzięki Colin, chciałbym, żeby to wszystko miało sens, ale nie rozumiem, jakie jest rozwiązanie. Jak najszybciej dodam 500 nagród do tego pytania i mam nadzieję, że ktoś może pomóc naprawić mój codepen.
parlament
Bez obaw, niełatwy problem do rozwiązania, ale nagroda może przyciągnąć niektóre oferty pomocy.
ColinE
2

To nadchodząca funkcja , jak już zauważyła @ColinE. Oryginalny kod zawsze wykonuje „zbliżenie czasowe”, które nie jest synchronizowane z macierzą transformacji.

Najlepszym obejściem jest ulepszenie xExtentzakresu, aby wykres uznał, że po bokach znajdują się dodatkowe świece. Można to osiągnąć, dodając podkładki po bokach. accessors, Zamiast,

[d => d.date]

staje się,

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

Sidenote: Zauważ, że jest padfunkcja, która powinna to zrobić, ale z jakiegoś powodu działa tylko raz i nigdy nie aktualizuje się ponownie, dlatego została dodana jako accessors.

Sidenote 2: Funkcja addDaysdodana jako prototyp (nie najlepsza rzecz do zrobienia) tylko dla uproszczenia.

Teraz zdarzenie powiększenia modyfikuje nasz współczynnik powiększenia X xZoom,

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

Ważne jest, aby odczytać różnicę bezpośrednio zwheelDelta . Oto, gdzie znajduje się nieobsługiwana funkcja: Nie możemy odczytać, t.xponieważ zmieni się, nawet jeśli przeciągniesz oś Y.

Na koniec ponownie oblicz, chart.xDomain(xExtent(data.series));aby nowy zasięg był dostępny.

Zobacz działające demo bez skoku tutaj: https://codepen.io/adelriosantiago/pen/QWjwRXa?editors=0011

Naprawiono: Cofanie powiększenia, ulepszone zachowanie na gładziku.

Technicznie możesz również ulepszyć yExtent, dodając dodatkowe d.highi d.low. Lub nawet jedno xExtenti drugie, yExtentaby w ogóle nie używać macierzy transformacji.

adelriosantiago
źródło
Dziękuję Ci. Niestety zachowanie zoomu w tym miejscu jest naprawdę popsute. Zoom wydaje się bardzo gwałtowny, a w niektórych przypadkach nawet kilkakrotnie zmienia kierunek (za pomocą gładzika MacBooka). Zachowanie powiększenia musi pozostać takie samo jak w oryginalnym piórze.
parlament
Zaktualizowałem pióro z kilkoma ulepszeniami, w szczególności z zoomem cofania.
adelriosantiago
1
Przyznam ci nagrodę za twój wysiłek i zaczynam drugą nagrodę, ponieważ wciąż potrzebuję odpowiedzi z lepszym powiększaniem / przesuwaniem. Niestety, ta metoda oparta na zmianach delta jest nadal gwałtowna i nie jest płynna podczas powiększania, a podczas panoramowania przesuwa się zbyt szybko. Podejrzewam, że prawdopodobnie możesz bez końca regulować ten współczynnik i nadal nie uzyskać płynnego zachowania. Szukam odpowiedzi, która nie zmienia zachowania powiększania / przesuwania oryginalnego pióra.
parlament
1
Dostosowałem również oryginalne pióro, aby powiększać tylko wzdłuż osi X. To jest zasłużone zachowanie i teraz zdaję sobie sprawę, że może to skomplikować odpowiedź.
parlament