Let's consider an analogy using d3-drag. A drag is essentially a zoom with a fixed scale/pan only (I seem to recall a discussion about removing d3-drag altogether given the overlap with d3-zoom).
Take following example:
const svg = d3.select("svg");
const datum = {x: 100, y:100}
const drag = d3.drag()
.on("drag", function(event,d) {
d3.select(this)
.attr("cx",d.x = event.x)
.attr("cy",d.y = event.y);
})
svg.append("circle")
.datum(datum)
.attr("cx",d=>d.x)
.attr("cy",d=>d.y)
.attr("r", 10)
.call(drag);
circle { cursor: pointer; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<svg width=500 height=300></svg>
Drags will be relative to the datum which in essence stores the "drag state". The data and the element, while bound, are still separate: we have to explicitly update the element's visual representation after the data is updated.
Now let's move the circle without updating the drag state:
const svg = d3.select("svg");
const datum = {x: 100, y:100}
const drag = d3.drag()
.on("drag", function(event,d) {
d3.select(this)
.attr("cx",d.x = event.x)
.attr("cy",d.y = event.y);
})
svg.append("circle")
.datum(datum)
.attr("cx",d=>d.x)
.attr("cy",d=>d.y)
.attr("r", 10)
.call(drag);
d3.select("button").on("click", function() {
d3.select("circle")
.attr("cx", 300)
.attr("cy", 50)
})
circle { cursor: pointer; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<button>Move</button>
<svg width=500 height=300></svg>
If we move the circle with the button we reposition the circle without updating the datum: this results in a jump afterwards when dragging: the drag is relative to the datum, but the feature is no longer positioned according to the datum.
The above snippet positions the circle without updating the drag state, much the same as when you use:
svg.transition().duration(500).attr("transform", d3.zoomIdentity);
You are changing the transform without updating the zoom state.
The only real difference here between the two is that the zoom state is not stored on the datum - it is stored elsewhere on the element. Something that is fairly easily fixed with d3-drag: just update the datum when positioning the circle:
const svg = d3.select("svg");
const datum = {x: 100, y:100}
const drag = d3.drag()
.on("drag", function(event,d) {
d3.select(this)
.attr("cx",d.x = event.x)
.attr("cy",d.y = event.y);
})
svg.append("circle")
.datum(datum)
.attr("cx",d=>d.x)
.attr("cy",d=>d.y)
.attr("r", 10)
.call(drag);
d3.select("button").on("click", function() {
d3.select("circle")
.attr("cx", d=>d.x=300)
.attr("cy", d=>d.y=50)
})
circle { cursor: pointer; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<button>Move</button>
<svg width=500 height=300></svg>
With d3-zoom the zoom state is not as accessible. The zoom behavior updates zoom state during zoom events, so we need to trigger a zoom event to update the zoom state. As the zoom event is usually used to update the visual representation of the elements, we also ensure agreement between zoom state and visual representation.
Now you almost had the right approach in the code that is commented out:
svg.attr("transform", d3.zoomIdentity);
Except you need to trigger a zoom event on the same element you called the zoom behavior originally. Like a drag, a zoom behavior can be called on multiple elements: it is just a behavior and doesn't track any state itself:
const svg = d3.select("svg");
const data = [{x: 100, y:100},{x:200,y:100}]
const zoom = d3.zoom()
.on("zoom", function(event,d) {
d3.select(this).select("circle")
.attr("transform",event.transform)
})
svg.selectAll(null)
.data(data)
.enter()
.append("g")
.call(zoom)
.append("circle")
.attr("cx",d=>d.x)
.attr("cy",d=>d.y)
.attr("r", 10)
circle { cursor: pointer; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<svg width=500 height=300></svg>
The above snippet uses the same zoom behavior twice, once for each circle (zoom interaction is only with circles themselves, not empty space). Ideally this shows that the behavior is independent of the zoom state. As noted above, the zoom state is stored on the element itself, if the behavior tracked zoom state, we couldn't use it on multiple elements.
So if we use:
svg.call(zoom)
Followed by:
svg2.call(zoom.transform,someTransform)
The zoom state stored on svg won't be affected/updated when we apply the zoom transform on svg2 - resulting in a jump when we resume zooming on svg.
The reason the result looks ok at first when using zoom.transform is because the zoom event updates the visual representation of the the target elements here (what you call the zoom on and what you modify in the event handler are of course independent):
const svg = d3.select("svg");
const data = [{x: 100, y:100},{x:200,y:100}]
const zoom = d3.zoom()
.on("zoom", function(event,d) {
d3.select(d.target).select("circle")
.attr("fill", d3.scaleLinear()
.range(["orange","lightgreen","steelblue"])
.domain([-250,0,250])(event.transform.x))
.attr("r",d3.scaleLinear()
.range([50,10,50])
.domain([-125,0,125])(event.transform.y))
})
svg.selectAll(null)
.data(data)
.enter()
.append("g")
.call(zoom)
.append("circle")
.attr("cx",d=>d.x)
.attr("cy",d=>d.y)
.attr("r", 20)
.each(function(d,i,nodes) {
d.target = nodes[+!i].parentNode;
})
circle { cursor: pointer; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<svg width=500 height=300></svg>
Again - zoom interaction only on the circles, drag/pan a circle to interact with the other one
svg.call(zoom.transform,d3.zoomIdentity)doesn't work for you in the reset function? - Andrew Reidsvg_orig:svg_orig.call(zoom.transform,d3.zoomIdentity). (d3-zoom stores the zoom state on the element it is called). - Andrew Reid