d3 / d3-zoom

Pan and zoom SVG, HTML or Canvas using mouse or touch input.
https://d3js.org/d3-zoom
ISC License
507 stars 143 forks source link

Ability to set X & Y transform scale #48

Closed joshuahiggins closed 4 years ago

joshuahiggins commented 8 years ago

With zoom.x() and zoom.y() removed, there doesn't appear to be an elegant way to zoom a single axis while leaving the opposite axis's domain untouched.

I'd like to propose the idea of extending the transform methods to support 1 or 2 scales, replacing the single k value with kx and ky values and extending the transform.scale method to accept an optional second parameter. By default, when called with a single parameter, kx and ky would be assigned with the same scale, resulting in the same functionality as today. However, users would have more control over their transforms through the added ability to compress and expand axises as needed.

I'm happy to work on this functionality but wanted to get a feel for it's perceived viability before I dove in.

mbostock commented 8 years ago

Here’s an example of one-dimensional zooming:

https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172

If you want to zoom the x-scale, use transform.rescaleX. If you want to zoom the y-scale, use transform.rescaleY.

joshuahiggins commented 8 years ago

That doesn't solve the problem for all cases, just for that specific use case where you only need to work off of a single zoom scale. What about the case where X and Y need to be transformed at different scales? Take this example: http://bl.ocks.org/jgbos/9752277

The above block is essentially a hack to accommodate dual scales in v3. They essentially wipe the scale clean by reapplying the zoom behavior, thus effectively setting the scale back to 1. Not a great solution, as it prevents you from using the provided zoom extents, however it was an okay workaround.

However, in v4, the transform is a bit more engrained, which feels like a huge step forward until you realize you're still limited to a single scale and now working around that limitation is far more difficult. rescale, invert, apply, and scale all operate under the assumption that you will never need to control k and that the transform.k is the scale you want to use for all scaling. For example, switching from X axis to XY axis zooming will result in a reset of the X axis to the same scale as Y, which is not the desired functionality.

There doesn't appear to be a feasible method of managing individual kx and ky scales that doesn't require writing your own implementation of the transform state and methods that accept kx and ky. Correct me if I'm wrong?

In my opinion, it would be very beneficial and likely a low risk feature to extend the transform to 4 values, x, y, kx, ky. The provided methods can be updated as follows:

Proposed changes:

d3.zoomTransform(node)

Return a slight different, but still two dimensional matrix: kx 0 tx 0 ky ty 0 0 1

transform.scale(kx [, ky = kx])

Returns a transform whose scale kx₁ is equal to kx₀ × kx, and whose scale ky₁ is equal to ky₀ × ky, where kx₀ is this transform’s x scale and ky₀ is this transform’s y scale.

transform.apply(point)

Returns the transformation of the specified point which is a two-element array of numbers [x, y]. The returned point is equal to [x × kx + tx, y × ky + ty].

transform.applyX(x)

Returns the transformation of the specified x-coordinate, x × kx + tx.

transform.applyY(y)

Returns the transformation of the specified x-coordinate, x × ky + ty.

transform.invert(point)

Returns the inverse transformation of the specified point which is a two-element array of numbers [x, y]. The returned point is equal to [(x - tx) / kx, (y - ty) / ky].

transform.invertX(x)

Returns the inverse transformation of the specified x-coordinate, (x - tx) / kx.

transform.invertY(y)

Returns the inverse transformation of the specified y-coordinate, (y - ty) / ky.

transform.toString()

Returns a string representing the SVG transform corresponding to this transform. Implemented as:

function toString() {
  return "translate(" + this.x + "," + this.y + ") scale(" + this.kx + "," + this.ky + ")";
}

d3.zoomIdentity

The identity transform, where kx = ky = 1, tx = ty = 0.

mbostock commented 8 years ago

Okay, I see what you mean now. And yes the zoom behavior is limited to uniform scaling at the moment; you can ignore one of those dimensions in how you apply the transform, but the zoom behavior internally represent the transform with a single k for both x and y.

It feels a bit weird to me to introduce a feature that is only accessible programmatically—I can’t think of a natural way to allow independent zooming of x and y in the current model (outside of multitouch and even then I don’t think you would want it enabled by default). Also I expect this would have ramifications on how zoom transitions are calculated.

(Somewhat related, but supporting rotation would be nice in the future, too.)

I guess I am not sure whether this makes more sense as a feature on the zoom behavior or another class of behavior. And adding this as a feature to the existing zoom behavior would likely not be backwards compatible, so maybe a new class makes more sense?

joshuahiggins commented 8 years ago

It feels a bit weird to me to introduce a feature that is only accessible programmatically—I can’t think of a natural way to allow independent zooming of x and y in the current model (outside of multitouch and even then I don’t think you would want it enabled by default).

I agree to an extent. Axis specific zooming has it's use cases, even if there's not a common pattern for it. I don't see an issue with providing the programmatic controls to allow people to build functionality that is outside of the box, considering that feels like the whole purpose of D3 in the first place.

Also I expect this would have ramifications on how zoom transitions are calculated.

That's possible. I haven't found any issues with zooming in the first pass I took, but I haven't tried transitions yet. I don't think it'll be an issue though, personally.

You can view the proposed changes in action here: http://bl.ocks.org/joshuahiggins/2512b0790c0feb2afec70c372530edbd

As well as a diff against the prod build of D3 here: https://www.diffchecker.com/hgtigw7g

A couple notes on the code:

  1. It's an afternoon monkey patch against the distributable code, so it clearly needs some love.
  2. It addresses all instances of scaling in the d3-zoom package that I could find.
  3. To expedite the patch, I updated Transform(k, x, y) to Transform(k, x, y, ky)... Obviously would need to address that before sending a proper PR.
  4. I added a zoom.scaleLock([x, y]) method which accepts an array of 2 booleans or a function that returns an array of 2 booleans. This is the magic sauce to making the proposed changes worthwhile to developers. This function adds in a check at the very start of a zoom event that allows the user to essentially stop each axis individually before an event even fires. You can see that in action in the Block.
  5. The changes in the patch are backwards compatible, as ky is simply an option parameter that defaults to k.

Somewhat related, but supporting rotation would be nice in the future, too.

Agreed. In that case, it might be good for Transform to become a helper function for a more robust Matrix function that takes all 6 parameters.

joshuahiggins commented 8 years ago

Oh and now that I've posted it, I realize my hover events are garbage and sometimes don't trigger the proper mouseover events to unlock the axis for scaling. Just hover out and hover back in if it gives you trouble, hah.

jfsiii commented 8 years ago

I recently ran into this issue while working on a chart that had brushes on the X & Y axes.

Using uniform scaling: brush and zoom - no scale y2

Using independent scaling: brush and zoom - scale y

I'm not sure of the solution, but I think the use case is reasonable.

joshuahiggins commented 8 years ago

I have D3 v4 working with a forked build of the d3-zoom library which allows for dual scaling. Been using it for a few weeks without any issues and no apparent conflicts with core functionality of D3. I haven't had time to open a PR yet, but I'm hoping to get one submitted in the next week or two.

jfsiii commented 8 years ago

(Somewhat related, but supporting rotation would be nice in the future, too.)

Agreed. Any thoughts on the skew, matrix, or the *3d properties? Is there any interest in a d3-transform?

I don't want to hijack this thread, so I'll stop here, but I want to mention I made a related library. It uses matrices internally, and allows you to create/update transforms using .translate(x,y,z), .rotate(x,y,z), etc. That particular lib is made to match a specific interface, but I'm glad to supply any code or tests 1 2

jfsiii commented 8 years ago

Here's the monkey-patch I apply. It's definitely "just get it done" quality, but ...

Object.assign(Transform.prototype, {
  invertY(y) {
    const scaleY = this.ky || this.k;
    return (y - this.y) / scaleY;
  },
  scaleY(k) {
    this.ky = k;
    return this;
  },
  toString() {
    const translate = `translate(${this.x},${this.y})`;
    const scale = `scale(${this.k},${this.ky || this.k})`;
    return `${translate} ${scale}`;
  }
});

Then I use it like const transform = (new Transform(scaleX, xPos, yPos)).scaleY(scaleY). Again, not ideal, but posting in case it helps anyone.

crapthings commented 8 years ago

i've the same issue.

https://github.com/d3/d3/issues/2970

timfish commented 7 years ago

I'd like to be able to set a different scaleExtent for each of x and y too. This would make it easier to disable zoom on a single axis.

jfsiii commented 7 years ago

I just learned about https://github.com/trinary/d3-transform/

There's also a PR to update it for V4 https://github.com/trinary/d3-transform/pull/19

etiennecrb commented 7 years ago

Hi, I'm working on this subject too because I need user to zoom out only on a single axis once scaling on the other would not respect translate extent constraints. Moreover, in my use case, I sometimes need to zoom with a different "scale factor" on x and y.

As @joshuahiggins did, I rewrite the Transform object which now has 4 values (x, y, kx, ky). Thus, it is possible to constrain scales so that translate extent constraints are respected, even on zoom out. This is done on the zoom object each time zoom.extent() or zoom.translateExtent() is called with this function:

function constrainScaleExtent() {
  kx0 = Math.max(kx0, (extent()[1][0] - extent()[0][0]) / (x1 - x0));
  ky0 = Math.max(ky0, (extent()[1][1] - extent()[0][1]) / (y1 - y0));
}

Moreover, it adds the zoom.scaleRatio() method in which you can provide a scale factor for x and y axis. It has an impact on event listeners (touch events currently not supported).

Demo Diff Github repo

I'm not sure about the consequences of what I'm doing. Except touch support and zoom transition, it seems to work great for my use case. Any advice?

etiennecrb commented 7 years ago

I released an independent d3 plugin: d3-xyzoom.

mbostock commented 7 years ago

Perhaps as d3-zoom 2.0 we should generalize the matrix to match CSS 2D matrix transformations:

a c tx b d ty 0 0 1

This corresponds to matrix(a b c d tx ty). Then d3-zoom could express both independent scaling of x and y, as well as rotation and skew. We could still define the default user interaction to be limited to translate and symmetric scale, but have options to allow rotation or other more general transformations, as well as being exposed programmatically.

HamsterHuey commented 7 years ago

Great discussion and glad to see that this is being considered for a future release. I just wanted to pipe in with another use case:

I have been struggling for a couple of days to implement a line chart with a button/checkbox that lets you switch between panning/scroll-zoom mode and a 2-D rectangular brush based zooming a-la MATLAB/Matplotlib. Due to the lack of support for independent zoom scales for X and Y, you are forced to use a separate path to implement the 2D brush zoom (Mike's block for Brush and Zoom II was invalauable), while the panning/scroll-zooming can be implemented in the tradition manner using d3.zoom. This led to 2 related issues:

1) Having duplication of code since each path requires very similar code for updating plot elements/scales/axes

2) It is tricky to update the zoom.transform when zooming with the 2-D brush. I believe that this was possible in d3 v3, but not in the new version? The only way to update it seems to be to use something like selection.call(zoom.transform, newTransform); which also fires off a bunch of events that I don't really want it to do due to having performed the rectangular 2D selection based zooming externally.

Perhaps this is easily accomplished within D3 v4 (I only just dipped my feet into the world of D3 a couple of weeks back), but I couldn't achieve this without a bit of a hacky approach that works but results in unnecessary overhead of the update operation on DOM elements being performed twice (once by the 2D brush method and once by the selection.call method to update the zoom transform so that subsequent pan/scroll-zoom operations are aware of the changes due to the 2D brush based zooming).

DaWaD3 commented 7 years ago

I use a zoom event for each axis and one for the global chart. Each zoom works fine but in combination the chart jumps.

See https://jsfiddle.net/DaWa/vn0rxd5g/

I think it has something to do with that problem. I am happy about any help.

Root-Core commented 7 years ago

Hi guys, any news about this issue? We really need to achieve an zoom on a single axis together with an "global" multi axis zoom.

Just like @DaWaD3's example shows.

@mbostock could you please consider declaring this issue as bug, as this works on d3v3? I'm confident that this is a regression to some point.

houfeng0923 commented 7 years ago

@Root-Core I had same trouble about this issue too .. it had work easily in d3 v3 . One change about zoom in v4 is that transform being held in selection . so i try to modify/reset the __zoom prop in selection . look at this demo : https://jsfiddle.net/1g68fggd/2/ but i not think it 's a nice way . @mbostock , hope your advice .

hi, @joshuahiggins , i can't find your version. what about your fork/pr now ?

Root-Core commented 7 years ago

If you want to apply the transformation of two scales to an given html-element, this should work with a bit of work.

But the problem here is, that we acquire the data dynamically and draw it scaled, but do not apply the events transform to the svg element. The data can be "infinitely" long, gets prefiltered to have one value per pixel and might be "live". This is why we need to restore the behavior from v3. ;)

DaWaD3 commented 7 years ago

I am quite not sure if it is the right way to do it but I think I have a solution for a multiple axis zoom.

https://jsfiddle.net/DaWa/dLmp8zk8/2/

In that example I always reset the transform for each zoom to ensure that I start zooming and do not have to pay attetion on the other zooms.

Does that make sense or am I doing something terribly wrong?

lteacher commented 7 years ago

Just started with trying to chart stuff and immediately have this issue. Basically I wanted to just say on ctrl + mousewheel zoom / stretch or shrink the Y axis and retain that scale for Y when doing regular zoom but instead it they both just spring back to the same scale.

Any plans to resolve this issue? I need the functionality and am having a hard time getting the builds to work when forking and linking locally. Since I already need to rescale both X and Y where in this case I conditionally choose to not rescale X, I don't see why they should be linked?

ghyatzo commented 7 years ago

@DaWaD3 Resetting the current transform state held in the selection seems to be the only viable solution as of now.

The only problem could be that any set scale extent would be useless since the scale would always be at 1. Moreover if you for example set a scale extent of [1, Infinity] you would be unable to zoom out, so not such a nice thing...

As a work around i fear that for applying this method to a scale extent you'd have to store the initial state of the graph and then at each redraw check to see if it's matched or not: Say compare the current axis domain with an instance of the same axis initial domain previously saved somewhere in your code.

jeffsf commented 6 years ago

I've been bitten by this one as well when trying to plot time-series data. A good visualization lets the user compress and expand the time scale to be able to see long-term trends and behavior, as well as short-term details, without changing the scaling on the data axis. If you've worked with a `scope before, think of the time-base knob that runs from microseconds per division up through seconds.

While wickedly ugly compared to the rest of my admittedly novice D3 usage, I found a marginally reasonable way to deal with this. A "hack" at best, I'll admit.

I can't say that it was either quick or easy to figure out how to do something that was so natural for me; a "simple" chart / strip recorder or digital `scope, depending on your era and experiences.

First, I observed that there are two kinds of d3.event.transform that the zoom handler gets, at least that I could generate:

When I say "zoom events" here, I mean events where the transform's scale has changed relative to its last version. As far as I can generate with a normal mouse, Apple touchpad, or Android touch-screen browser, these events do not simultaneously zoom and pan the "event point" as well. While there is change in the translation, it appears to be just to keep the same point under the cursor.

const t = d3.event.transform;

The X/time scale handles the unmodified transform, as it can both pan and zoom

updatingGraph.current.timeScale = t.rescaleX(updatingGraph.unzoomed.timeScale);

However, the data scales need to get a transform without the zoom component. If you just modify t directly by setting t.k = 1; you end up with never being able to zoom as t is a reference to the transform of whatever is seeing the events and storing the cumulative transform.

You unfortunately can't call d3.zoomIdentity.translate(t.x, t.y).rescaleY(someScale) since it isn't a full-fledged transform, only the matrix coefficients. This means creating a compatible DOM object to "back" the transform, just to be able to call .rescaleY() as well as someplace to tuck away the last transform, to know if it is a "zoom" or a "pan" event.

        updatingGraph.zoomBehavior = d3.zoom().on("zoom", updatingGraph.doZoom);

        updatingGraph.zoom_catcher = updatingGraph.active_area.append("rect")
            .attr("class", "zoom-catcher")
            .attr("id", "zoom-catcher")
            .attr("width", updatingGraph.activeAreaExtent.width)
            .attr("height", updatingGraph.activeAreaExtent.height)
            .call(updatingGraph.zoomBehavior);

        // a functional zoomTransform needs to be associated with an element

        updatingGraph.t_holder = updatingGraph.zoom_catcher.append("g")
            .attr("id", "t-holder");

        updatingGraph.lastZoomTransform =
            Object.assign({}, d3.zoomTransform(updatingGraph.zoom_catcher));

Now when I get a zoom event, I can branch on zoom vs. pan. If a pan-only event, I ignore the scale when calling .rescaleY(). For a zoom-only event, I reverse-out the translation change in Y (otherwise the data appears to shift up and down "for no reason")

        let t_tmp = d3.zoomTransform("#t-holder");  // Get a "real" zoomTransform

        if (t.k === updatingGraph.lastZoomTransform.k) {

            t_tmp = d3.zoomIdentity.translate(t.x, t.y);

            updatingGraph.current.tempScale = t_tmp.rescaleY(updatingGraph.unzoomed.tempScale);
            updatingGraph.current.humidityScale = t_tmp.rescaleY(updatingGraph.unzoomed.humidityScale)

        } else {

            t.y = updatingGraph.lastZoomTransform.y

        }

        updatingGraph.lastZoomTransform = Object.assign({}, t);

Of course, this all falls apart with any device that can create simultaneous (combined) zoom and pan

ohenrik commented 6 years ago

So I’m about to go crazy trying to solve this... There is definitely a use case for this.

I need to be able to zoom the time axis (x-axis) while controlling zoom on the price axis (y-axis) separately. Lastly when i drag the window i want to be able to drag both the x and the y axis.

Are there any plans to add support for kx and ky? @DaWaD3 solution is only for a spesific use case.

jfsiii commented 6 years ago

@ohenrik Would https://github.com/d3/d3-zoom/issues/48#issuecomment-243957022 help? You can see it in action in https://github.com/d3/d3-zoom/issues/48#issuecomment-243953357

ohenrik commented 6 years ago

Thank you for responding :) @jfsiii, It looks close to what i want, however how do i make this? Do you have any example code?

This is another example that is close to what i want: https://jsfiddle.net/o8vaecyn/35/ However i need to be able to be able to zoom the y-axis by scrolling over the Y axis. In the example above Y is forever unzoomable.

ohenrik commented 6 years ago

I tried implementing the code from https://github.com/d3/d3-zoom/issues/48#issuecomment-243957022 however Transform is not defined. What object is this? Do i get it from d3?

ohenrik commented 6 years ago

@jfsiii i managed to implement the code from https://github.com/d3/d3-zoom/issues/48#issuecomment-243957022. And it does work for panning. However... it does not work for zooming. when i zoom the y scale seems not to be taken into account. I have inspected the Transform code, however i cannot completely understand it. Got any tips on how to modify this so that zoom is taken care of?

jfsiii commented 6 years ago

@ohenrik You can see both panning and zooming working in the GIF, but perhaps you're using different methods than I did or in a different way.

Here's an overview:

This code updates Transform from https://github.com/d3/d3-zoom/blob/master/src/transform.js

Object.assign(Transform.prototype, {
  invertY(y) {
    const scaleY = this.ky || this.k;
    return (y - this.y) / scaleY;
  },
  scaleY(k) {
    this.ky = k;
    return this;
  },
  toString() {
    const translate = `translate(${this.x},${this.y})`;
    const scale = `scale(${this.k},${this.ky || this.k})`;
    return `${translate} ${scale}`;
  }
});

The key issue is that there is only one scale value (this.k), so I added a scaleY method which sets this.ky and updated invertY and toString to use this.ky if it's present or fall back to the original/uniform scale this.k

Perhaps you didn't call scaleY to set this.ky? Perhaps other methods need to be altered to use this.ky || this.k?

Unfortunately, I don't have access to that code any longer so I cannot show you how it's used.

Let's not spam subscribers with any more notifications iterating on this. At this point, I think it's better suited for Stack Overflow.

Good luck!

Abhijeet-Das commented 6 years ago

Hello All,

I am wondering what could be an appropriate way to zoom in matrix scatter plot. For example , in this https://bl.ocks.org/Fil/6d9de24b31cb870fed2e6178a120b17d we can see both x and y axis have different scales.

Could anyone please suggest what can be ideal approach to apply zooming feature in this visualization or share any example which has zoom in matrix scatter plot d3 V4. Thanks in advance

aldanor commented 6 years ago

Late to join the party, been bit by the same horrible problem. Wondering if any of you guys have come up to any of the cleaner or more stable solutions than described above?

(since it doesn’t look it’s making it in d3-zoom prod anytime soon)

mbostock commented 6 years ago

I haven’t had any time to work on this feature, but I’d be willing to review an implementation of https://github.com/d3/d3-zoom/issues/48#issuecomment-264541988 if anyone is willing to work on it.

10lojzo commented 5 years ago

I made a hack-like workaround to achieve zooming only one axis and pan both x and y axes. I am not completely sure it addresses the exact problem of this thread, as long as panning is involved in my case. I would appreciate some comment with better(proper) solution or some final word if this feature is going to be implemented or not.

https://jsfiddle.net/xpr364uo/

What is going on in short:

In transform function I check the property d3.event.sourceEvent.type and decide if it is zooming or panning. If it is panning, I store the delta y corresponding to panning by comparing with t.y in last stored transform object. Then I am able to tell, which portion of the t.y in new transform is produced by all pannings in previous transforms.

Thanks

aldanor commented 5 years ago

Here's a working example using d3-xyzoom if anyone's interested: https://codepen.io/aldanor/pen/WabmoL

kaltal commented 5 years ago

Here's a working example using d3-xyzoom if anyone's interested: https://codepen.io/aldanor/pen/WabmoL

aldanor, How could I get the complete code for your working copy? It looks pretty clean. Thanks

aldanor commented 5 years ago

@kaltal the linked codepen contains a complete self-contained example.

akhilkurnool commented 5 years ago

I released an independent d3 plugin: d3-xyzoom.

How do I apply this plugin to d3 v4. I'm using react 16.4 and create-react-app 2.0. Thanks

mbostock commented 5 years ago

I am currently working on a new release for this library, but I would like to add support for multitouch rotation (as is now commonly supported by vector sloppy maps, for instance), along with easier support for one-dimensional zooming. I plan to revisit this after d3-zoom 2.0 is released.

klausz commented 5 years ago

Here's a working example using d3-xyzoom if anyone's interested: https://codepen.io/aldanor/pen/WabmoL

I liked it. But there are two libraries added.

Neeraj-swarnkar commented 4 years ago

I am facing some similar type of issue - https://stackoverflow.com/questions/60412324/in-my-d3-chart-appended-rect-is-not-coming-properly can you guide me

adelriosantiago commented 4 years ago

The best workaround is to simply create faux data entries. This allows to simulate asymmetric X, Y zoom and pan. See my answer here for more info: https://stackoverflow.com/a/61164185/1996066

parliament718 commented 4 years ago

Have been struggling with this implementation related to this problem for several weeks. https://stackoverflow.com/questions/61071276/d3-synchronizing-2-separate-zoom-behaviors

Open bounty, any help appreciated.

Fil commented 4 years ago

I've made this example of a double zoom chart; somewhat arbitrarily, the features are:

I tried to write it in a way that I hope is somewhat readable, and can be adapted to various needs.

Let me know if there are things that are unclear, and don't hesitate to send suggestions. https://observablehq.com/d/0b496b3c9cf2144e

Fil commented 4 years ago

The solution / example notebook is published https://observablehq.com/@d3/x-y-zoom

joshuahiggins commented 4 years ago

This looks slick. Going to poke around the code later, but from a quick glance over it it looks like an elegant solution to the problem. Appreciate it!

cybae0804 commented 3 years ago

The solution / example notebook is published https://observablehq.com/@d3/x-y-zoom

For those of you trying to make use of this example, I'd recommend changing

const point = e.sourceEvent ? d3.pointer(e) : [width / 2, height / 2];

to

const point = e.sourceEvent ? d3.pointer(e, e.sourceEvent.target) : [width / 2, height / 2];

wheelEvent from zooming works correctly, and d3.pointer(e) correctly uses the svg element from currentTarget. However mouseEvent from dragging returns the pageX and pageY coordinate because it uses the window as the currentTarget. I'm not sure if this is a chrome specific behavior, but basically always passing the svg element solves this.

You can see this behavior by dragging the graph above vertically, and if it's near the bottom x axis, it will not drag. However as you drag upwards toward the top of the page, it'll start dragging properly.

Fil commented 3 years ago

Thanks! I've fixed this issue (using this instead of e.sourceEvent.target, but it's the same thing), and made it work with touch events too by switching from d3.pointer to d3.pointers.

martinblostein commented 3 years ago

I appreciate the example posted by @Fil (https://observablehq.com/@d3/x-y-zoom), but that seems to me to be a clever workaround rather than a resolution to this issue and so I don't agree that this issue should be closed.

There are some quirks about that solution:

  1. It only works on pure translation and pure scaling events. Touch users can do both at the same time by pinching. Using d3-zoom the normal way fully supports this out of the box.
  2. It is not straightforward to combine that solution with other programmatic control of the zoom/pan.
  3. The user must track previous zoom state themselves.
  4. There's redundancy -- each d3 zoom instances tracks separate 2D transformations, but only half of each one is used. I think this makes the code harder to understand and maintain.

Again, it is a nice workaround, but I think a resolution to this issue would mean supporting independent X/Y transforms within the existing d3 zoom API, not a workaround that tracks the transformations manually and combines multiple 2 dimensional d3 zoom instances.

richard-biarri commented 3 years ago

An additional issue with the workaroud example above is that it is not possible to apply translationExtent to the zoom behaviour: they are not scaled along the axes correctly.

Edit: ah, translation extents do work as long as you assert them on zoomX and zoomY!