d3 sincronizando 2 comportamientos de zoom separados

11

Tengo el siguiente gráfico d3 / d3fc

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

El gráfico tiene un comportamiento de zoom para el área principal y un comportamiento de zoom separado para el eje y. El eje y se puede arrastrar para reescalar.

El problema que tengo problemas para resolver es que después de arrastrar el eje y para reescalar y luego desplazar el gráfico, hay un "salto" en el gráfico.

Obviamente, los 2 comportamientos de zoom tienen una desconexión y deben sincronizarse, pero estoy atormentando mi cerebro tratando de solucionar esto.

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

El comportamiento deseado es hacer zoom, paneo y / o reescalar el eje para aplicar siempre la transformación desde donde haya terminado la acción anterior, sin ningún "salto".

parlamento
fuente

Respuestas:

10

De acuerdo, he tenido otra oportunidad en esto: como se mencionó en mi respuesta anterior , el mayor problema que debe superar es que el zoom d3 solo permite el escalado simétrico. Esto es algo que ha sido ampliamente discutido , y creo que Mike Bostock está abordando esto en el próximo lanzamiento.

Entonces, para superar el problema, debe usar el comportamiento de zoom múltiple. He creado un gráfico que tiene tres, uno para cada eje y otro para el área de trazado. Los comportamientos de zoom X e Y se utilizan para escalar los ejes. Siempre que los comportamientos de zoom X e Y provocan un evento de zoom, sus valores de traducción se copian en el área de trazado. Del mismo modo, cuando se produce una traducción en el área de trazado, los componentes x e y se copian en los comportamientos de los ejes respectivos.

Escalar en el área de trazado es un poco más complicado ya que necesitamos mantener la relación de aspecto. Para lograr esto, almaceno la transformación de zoom anterior y uso el delta de escala para calcular una escala adecuada para aplicar a los comportamientos de zoom X e Y.

Para mayor comodidad, he incluido todo esto en un componente gráfico:


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

Aquí hay una pluma con el ejemplo de trabajo:

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

ColinE
fuente
Colin, muchas gracias por darle otra oportunidad. Abrí su codepen y noté que no funciona como se esperaba. En mi codepen, arrastrar el eje Y vuelve a escalar el gráfico (este es el comportamiento deseado). En su codepen, arrastrar el eje simplemente desplaza el gráfico.
Parlamento
1
He logrado crear uno que le permite arrastrar tanto en la escala y como en el área de trazado codepen.io/colineberhardt-the-bashful/pen/mdeJyrK , pero hacer zoom en el área de trazado es todo un desafío
ColinE
5

Hay un par de problemas con su código, uno que es fácil de resolver y otro que no ...

En primer lugar, el zoom d3 funciona almacenando una transformación en los elementos DOM seleccionados; puede ver esto a través de la __zoompropiedad. Cuando el usuario interactúa con el elemento DOM, esta transformación se actualiza y se emiten eventos. Por lo tanto, si tiene diferentes comportamientos de zoom que controlan la panorámica / zoom de un solo elemento, debe mantener estas transformaciones sincronizadas.

Puede copiar la transformación de la siguiente manera:

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

Sin embargo, esto también hará que se disparen eventos de zoom desde el comportamiento del objetivo también.

Una alternativa es copiar directamente a la propiedad de transformación 'escondida':

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

Sin embargo, hay un problema mayor con lo que está tratando de lograr. La transformación d3-zoom se almacena como 3 componentes de una matriz de transformación:

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

Como resultado, el zoom solo puede representar una escala simétrica junto con una traducción. Su zoom asimétrico como aplicado al eje x no puede ser fielmente representado por esta transformación y reaplicado al área de trazado.

ColinE
fuente
Gracias Colin, deseo que todo esto tenga sentido para mí, pero no entiendo cuál es la solución. Agregaré una recompensa de 500 a esta pregunta tan pronto como pueda y espero que alguien pueda ayudar a arreglar mi codepen.
Parlamento
No se preocupe, no es un problema fácil de solucionar, pero una recompensa podría atraer algunas ofertas de ayuda.
ColinE
2

Esta es una característica próxima , como ya señaló @ColinE. El código original siempre está haciendo un "zoom temporal" que no está sincronizado desde la matriz de transformación.

La mejor solución es ajustar el xExtentrango para que el gráfico crea que hay velas adicionales en los lados. Esto se puede lograr agregando almohadillas a los lados. El accessors, en lugar de ser,

[d => d.date]

se convierte,

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

Nota al margen: Tenga en cuenta que hay una padfunción que debería hacer eso, pero por alguna razón funciona solo una vez y nunca se actualiza nuevamente, por eso se agrega como un accessors.

Sidenote 2: Función addDaysagregada como prototipo (no es lo mejor que se puede hacer) solo por simplicidad.

Ahora el evento de zoom modifica nuestro factor de zoom X xZoom,

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

Es importante leer el diferencial directamente desdewheelDelta . Aquí es donde está la característica no compatible: no podemos leer, t.xya que cambiará incluso si arrastra el eje Y.

Finalmente, vuelva a calcular chart.xDomain(xExtent(data.series));para que la nueva extensión esté disponible.

Vea la demostración de trabajo sin el salto aquí: https://codepen.io/adelriosantiago/pen/QWjwRXa?editors=0011

Corregido: inversión de zoom, comportamiento mejorado en el panel táctil.

Técnicamente, también podría ajustar yExtentagregando extra d.highy d.low's. O incluso ambos, xExtenty yExtentevitar usar la matriz de transformación.

adelriosantiago
fuente
Gracias. Lamentablemente, el comportamiento del zoom aquí está realmente desordenado. El zoom se siente muy desigual y, en algunos casos, incluso invierte la dirección varias veces (usando el trackpad macbook). El comportamiento del zoom debe permanecer igual que el lápiz original.
Parlamento
He actualizado el bolígrafo con algunas mejoras, particularmente el zoom inverso.
adelriosantiago
1
Voy a otorgarle la recompensa por su esfuerzo y estoy comenzando una segunda recompensa porque todavía necesito una respuesta con un mejor zoom / panorámica. Desafortunadamente, este método basado en cambios delta todavía es desigual y no es uniforme cuando se hace zoom y cuando se desplaza demasiado rápido. Sospecho que probablemente pueda ajustar el factor sin cesar y aún así no obtener un comportamiento fluido. Estoy buscando una respuesta que no cambie el comportamiento de acercamiento / desplazamiento del lápiz original.
Parlamento
1
También he ajustado el lápiz original para hacer zoom solo a lo largo del eje X. Ese es el comportamiento merecido y ahora me doy cuenta de que puede complicar la respuesta.
Parlamento