diff --git a/README.md b/README.md
index a1aba131..2b332ba7 100644
--- a/README.md
+++ b/README.md
@@ -168,6 +168,18 @@ function constrain(transform, extent, translateExtent) {
The constraint function must return a [*transform*](#zoom-transforms) given the current *transform*, [viewport extent](#zoom_extent) and [translate extent](#zoom_translateExtent). The default implementation attempts to ensure that the viewport extent does not go outside the translate extent.
+# zoom.center([center]) · [Source](https://github.com/d3/d3-zoom/blob/main/src/zoom.js), [Examples](https://observablehq.com/@d3/zoom-center-212)
+
+If *center* is a function, sets the center to the specified function, and returns the zoom behavior. If *center* is an array, sets the center to a constant function that returns the specified array. If *center* is not specified, returns the current center, which defaults to the pointer’s position relative to the current DOM element:
+
+```js
+function center(event) {
+ return d3.pointer(event, this);
+}
+```
+
+The center is passed the current event (`event`) and datum `d`, with the `this` context as the current DOM element. It must return the reference position as [*x*, *y*].
+
# zoom.filter([filter]) · [Source](https://github.com/d3/d3-zoom/blob/main/src/zoom.js)
If *filter* is specified, sets the filter to the specified function and returns the zoom behavior. If *filter* is not specified, returns the current filter, which defaults to:
diff --git a/src/zoom.js b/src/zoom.js
index d5643882..fb3ff5b3 100644
--- a/src/zoom.js
+++ b/src/zoom.js
@@ -14,6 +14,10 @@ function defaultFilter(event) {
return (!event.ctrlKey || event.type === 'wheel') && !event.button;
}
+function defaultCenter(event) {
+ return pointer(event, this);
+}
+
function defaultExtent() {
var e = this;
if (e instanceof SVGElement) {
@@ -52,6 +56,7 @@ function defaultConstrain(transform, extent, translateExtent) {
export default function() {
var filter = defaultFilter,
+ center = defaultCenter,
extent = defaultExtent,
constrain = defaultConstrain,
wheelDelta = defaultWheelDelta,
@@ -243,6 +248,7 @@ export default function() {
if (g.wheel) {
if (g.mouse[0][0] !== p[0] || g.mouse[0][1] !== p[1]) {
g.mouse[1] = t.invert(g.mouse[0] = p);
+ g.mouse[2] = center.apply(this, arguments);
}
clearTimeout(g.wheel);
}
@@ -252,14 +258,14 @@ export default function() {
// Otherwise, capture the mouse point and location at the start.
else {
- g.mouse = [p, t.invert(p)];
+ g.mouse = [p, t.invert(p), center.apply(this, arguments)];
interrupt(this);
g.start();
}
noevent(event);
g.wheel = setTimeout(wheelidled, wheelDelay);
- g.zoom("mouse", constrain(translate(scale(t, k), g.mouse[0], g.mouse[1]), g.extent, translateExtent));
+ g.zoom("mouse", constrain(translate(scale(t, k), g.mouse[2], t.invert(g.mouse[2])), g.extent, translateExtent));
function wheelidled() {
g.wheel = null;
@@ -406,6 +412,10 @@ export default function() {
return arguments.length ? (touchable = typeof _ === "function" ? _ : constant(!!_), zoom) : touchable;
};
+ zoom.center = function(_) {
+ return arguments.length ? (center = typeof _ === "function" ? _ : constant([+_[0], +_[1]]), zoom) : center;
+ };
+
zoom.extent = function(_) {
return arguments.length ? (extent = typeof _ === "function" ? _ : constant([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), zoom) : extent;
};