d3 menyinkronkan 2 perilaku zoom terpisah

11

Saya memiliki bagan d3 / d3fc berikut

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

Grafik memiliki perilaku zoom untuk area utama dan perilaku zoom terpisah untuk sumbu y. Sumbu y dapat diseret untuk mengubah skala.

Masalah yang saya alami pemecahannya adalah setelah menyeret sumbu y untuk mengubah skala dan kemudian menggeser grafik, ada "lompatan" pada grafik.

Jelas perilaku 2 pembesaran memiliki putuskan dan perlu disinkronkan tapi saya memutar otak saya mencoba untuk memperbaikinya.

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

Perilaku yang diinginkan adalah untuk memperbesar, menggeser, dan / atau mengubah ukuran sumbu untuk selalu menerapkan transformasi dari mana pun tindakan sebelumnya selesai, tanpa "lompatan".

parlemen
sumber

Jawaban:

10

OK, jadi saya sudah mencoba lagi - seperti yang disebutkan dalam jawaban saya sebelumnya , masalah terbesar yang perlu Anda atasi adalah bahwa d3-zoom hanya memungkinkan penskalaan simetris. Ini adalah sesuatu yang telah dibahas secara luas , dan saya percaya Mike Bostock sedang membahas ini di rilis berikutnya.

Jadi, untuk mengatasi masalah ini, Anda perlu menggunakan beberapa perilaku zoom. Saya telah membuat bagan yang memiliki tiga, satu untuk setiap sumbu dan satu untuk area plot. Perilaku pembesaran X & Y digunakan untuk menskalakan sumbu. Setiap kali acara zoom dinaikkan oleh perilaku zoom X & Y, nilai terjemahannya disalin ke area plot. Demikian juga, ketika terjemahan terjadi pada area plot, komponen x & y disalin ke perilaku sumbu masing-masing.

Penskalaan pada area plot sedikit lebih rumit karena kita perlu mempertahankan aspek rasio. Untuk mencapai ini, saya menyimpan transformasi zoom sebelumnya dan menggunakan skala delta untuk menentukan skala yang sesuai untuk diterapkan pada perilaku zoom X & Y.

Untuk kenyamanan, saya telah membungkus semua ini menjadi komponen bagan:


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

Ini pulpen dengan contoh kerja:

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

ColinE
sumber
Colin, terima kasih banyak sudah mencoba ini lagi. Saya membuka codepen Anda dan memperhatikan bahwa itu tidak berfungsi seperti yang diharapkan. Dalam codepen saya, menyeret sumbu Y skala ulang grafik (ini adalah perilaku yang diinginkan). Di codepen Anda, menyeret sumbu cukup buka grafik.
parlemen
1
Saya telah berhasil membuat satu yang memungkinkan Anda untuk menyeret skala y dan area plot codepen.io/colineberhardt-the-bashful/pen/mdeJyrK - tetapi memperbesar area plot cukup sulit
ColinE
5

Ada beberapa masalah dengan kode Anda, yang mudah dipecahkan, dan yang tidak ...

Pertama, zoom d3 berfungsi dengan menyimpan transformasi pada elemen DOM yang dipilih - Anda dapat melihatnya melalui __zoomproperti. Saat pengguna berinteraksi dengan elemen DOM, transformasi ini diperbarui dan acara dipancarkan. Oleh karena itu, jika Anda harus memiliki perilaku zoom yang berbeda, yang keduanya mengendalikan pan / zoom elemen tunggal, Anda harus menjaga agar transformasi ini tetap tersinkronisasi.

Anda dapat menyalin transformasi sebagai berikut:

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

Namun, ini juga akan menyebabkan acara zoom dipecat dari perilaku target juga.

Alternatifnya adalah menyalin langsung ke properti transformasi 'simpanan':

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

Namun, ada masalah yang lebih besar dengan apa yang ingin Anda capai. Transformasi d3-zoom disimpan sebagai 3 komponen dari matriks transformasi:

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

Akibatnya, zoom hanya dapat mewakili penskalaan simetris bersama dengan terjemahan. Zoom asimetris Anda sebagai diterapkan pada sumbu x tidak dapat dengan setia diwakili oleh transformasi ini dan diterapkan kembali ke area plot.

ColinE
sumber
Terima kasih Colin, saya berharap semua ini masuk akal bagi saya tetapi saya tidak mengerti apa solusinya. Saya akan menambahkan 500 karunia untuk pertanyaan ini sesegera mungkin dan semoga seseorang dapat membantu memperbaiki codepen saya.
parlemen
Jangan khawatir, bukan masalah mudah untuk diperbaiki, tetapi karunia mungkin menarik beberapa tawaran bantuan.
ColinE
2

Ini adalah fitur yang akan datang , seperti yang sudah dicatat oleh @ColinE. Kode asli selalu melakukan "temporal zoom" yang tidak disinkronkan dari matriks transformasi.

Solusi terbaik adalah mengubah xExtentrentang sehingga grafik percaya bahwa ada lilin tambahan di samping. Ini dapat dicapai dengan menambahkan pembalut ke samping. The accessors, bukannya menjadi,

[d => d.date]

menjadi,

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

Sidenote: Perhatikan bahwa ada padfungsi yang harus melakukan itu tetapi karena beberapa alasan ia berfungsi hanya sekali dan tidak pernah memperbarui lagi itu sebabnya ditambahkan sebagai accessors.

Sidenote 2: Fungsi addDaysditambahkan sebagai prototipe (bukan hal terbaik untuk dilakukan) hanya untuk kesederhanaan.

Sekarang acara zoom memodifikasi faktor zoom X kami xZoom,,

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

Penting untuk membaca diferensial langsung dariwheelDelta . Di sinilah fitur yang tidak didukung: Kami tidak dapat membaca dari t.xkarena akan berubah bahkan jika Anda menyeret sumbu Y.

Akhirnya, hitung ulang chart.xDomain(xExtent(data.series));sehingga tingkat baru tersedia.

Lihat demo kerja tanpa lompatan di sini: https://codepen.io/adelriosantiago/pen/QWjwRXa?editors=0011

Tetap: Pembalikan zoom, peningkatan perilaku pada trackpad.

Secara teknis Anda juga dapat mengubah yExtentdengan menambahkan ekstra d.highdan d.low. Atau bahkan keduanya xExtentdan yExtentuntuk menghindari menggunakan matriks transformasi sama sekali.

adelriosantiago
sumber
Terima kasih. Sayangnya perilaku zoom di sini benar-benar kacau. Zoom terasa sangat tersentak-sentak dan dalam beberapa kasus bahkan membalik arah beberapa kali (menggunakan macbook trackpad). Perilaku zoom harus tetap sama dengan pena asli.
parlemen
Saya telah memperbarui pena dengan beberapa perbaikan, khususnya pembalikan pembesaran.
adelriosantiago
1
Saya akan memberi Anda hadiah untuk usaha Anda dan saya memulai hadiah kedua karena saya masih membutuhkan jawaban dengan zoom / pan yang lebih baik. Sayangnya metode ini berdasarkan perubahan delta masih tersentak-sentak dan tidak mulus saat zooming, dan ketika panning panci terlalu cepat. Saya menduga bahwa Anda mungkin dapat menyesuaikan faktor tanpa henti dan masih belum mendapatkan perilaku yang lancar. Saya mencari jawaban yang tidak mengubah perilaku pembesaran / panning dari pena asli.
parlemen
1
Saya juga menyesuaikan pena asli untuk memperbesar hanya di sepanjang sumbu X. Itu adalah perilaku yang pantas dan saya sekarang sadar bahwa itu mungkin memperumit jawabannya.
parlemen