Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

d3 synchronizing 2 separate zoom behaviors

Tags:

d3.js

d3fc

I have the following d3/d3fc chart

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

The chart has a zoom behavior for the main area and a separate zoom behavior for the y-axis. The y-axis can be dragged to rescale.

The problem I'm having trouble solving is that after dragging the y-axis to rescale and then subsequently panning the chart, there is a "jump" in the chart.

Obviously the 2 zoom behaviors have a disconnect and need to be synchronized but I'm racking my brain trying to fix this.

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

The desired behavior is for zooming, panning, and/or rescaling the axis to always apply the transformation from wherever the previous action finished, without any "jumps".

like image 761
parliament Avatar asked Apr 07 '20 01:04

parliament


2 Answers

There are a couple of issues with your code, one which is easy to solve, and one which is not ...

Firstly, the d3-zoom works by storing a transform on the selected DOM element(s) - you can see this via the __zoom property. When the user interacts with the DOM element, this transform is updated and events emitted. Therefore, if you have to different zoom behaviours both of which are controlling the pan / zoom of a single element, you need to keep these transforms synchronised.

You can copy the transform as follows:

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

However, this will also cause zoom events to be fired from the target behaviour also.

An alternative is to copy directly to the 'stashed' transform property:

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

However, there is a bigger problem with what you are trying to achieve. The d3-zoom transform is stored as 3 components of a transformation matrix:

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

As a result, the zoom can only represent a symmetrical scaling together with a translation. Your asymmetrical zoom as a applied to the x-axis cannot be faithfully represented by this transform and re-applied to the plot-area.

like image 149
ColinE Avatar answered Oct 18 '22 23:10

ColinE


This is an upcoming feature, as already noted by @ColinE. The original code is always doing a "temporal zoom" that is un-synced from the transform matrix.

The best workaround is to tweak the xExtent range so that the graph believes that there are additional candles on the sides. This can be achieved by adding pads to the sides. The accessors, instead of being,

[d => d.date]

becomes,

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

Sidenote: Note that there is a pad function that should do that but for some reason it works only once and never updates again that's why it is added as an accessors.

Sidenote 2: Function addDays added as a prototype (not the best thing to do) just for simplicity.

Now the zoom event modifies our X zoom factor, xZoom,

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

It is important to read the differential directly from wheelDelta. This is where the unsupported feature is: We can't read from t.x as it will change even if you drag the Y axis.

Finally, recalculate chart.xDomain(xExtent(data.series)); so that the new extent is available.

See the working demo without the jump here: https://codepen.io/adelriosantiago/pen/QWjwRXa?editors=0011

Fixed: Zoom reversing, improved behaviour on trackpad.

Technically you could also tweak yExtent by adding extra d.high and d.low's. Or even both xExtent and yExtent to avoid using the transform matrix at all.

like image 6
adelriosantiago Avatar answered Oct 18 '22 22:10

adelriosantiago