Pan and zoom SVG, HTML or Canvas using mouse or touch input
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Comprehensive event system for handling zoom interactions and responding to zoom state changes.
Registers event listeners for zoom events.
/**
* Gets or sets event listeners for zoom events
* @param typenames - Event type names (space-separated)
* @param listener - Event listener function or null to remove (optional)
* @returns Current listener or ZoomBehavior for chaining
*/
on(typenames: string, listener?: EventListener | null): ZoomBehavior | EventListener;
type EventListener = (this: Element, event: ZoomEvent, d?: any) => void;Usage Examples:
import { select } from "d3-selection";
import { zoom } from "d3-zoom";
const svg = select("svg");
const zoomBehavior = zoom()
.on("start", function(event) {
console.log("Zoom started");
this.style.cursor = "grabbing";
})
.on("zoom", function(event) {
const { transform } = event;
svg.select("g").attr("transform", transform);
})
.on("end", function(event) {
console.log("Zoom ended");
this.style.cursor = "grab";
});
svg.call(zoomBehavior);
// Multiple listeners for same event
zoomBehavior
.on("zoom.update", updateVisualization)
.on("zoom.debug", logTransform);
// Remove specific listener
zoomBehavior.on("zoom.debug", null);
// Get current listener
const currentListener = zoomBehavior.on("zoom");Event object passed to zoom event listeners.
/**
* Zoom event object containing event details
*/
interface ZoomEvent {
/** The zoom behavior that dispatched this event */
readonly target: ZoomBehavior;
/** Event type: "start", "zoom", or "end" */
readonly type: "start" | "zoom" | "end";
/** Current zoom transform */
readonly transform: ZoomTransform;
/** Original DOM event that triggered the zoom */
readonly sourceEvent: Event;
}Usage Examples:
import { zoom } from "d3-zoom";
const zoomBehavior = zoom()
.on("zoom", function(event) {
const { target, type, transform, sourceEvent } = event;
console.log("Event type:", type); // "zoom"
console.log("Transform:", transform.toString());
console.log("Source event:", sourceEvent.type); // "wheel", "mousemove", etc.
console.log("Zoom behavior:", target === zoomBehavior); // true
// Apply transform
svg.select("g").attr("transform", transform);
});Different types of zoom events and their timing.
type ZoomEventType = "start" | "zoom" | "end";Fired when a zoom gesture begins.
Triggers:
Usage Examples:
import { zoom } from "d3-zoom";
const zoomBehavior = zoom()
.on("start", function(event) {
// Visual feedback
this.classList.add("zooming");
// Disable other interactions
this.style.pointerEvents = "none";
// Log interaction type
const { sourceEvent } = event;
if (sourceEvent.type === "mousedown") {
console.log("Pan started");
} else if (sourceEvent.type === "wheel") {
console.log("Zoom started");
} else if (sourceEvent.type === "touchstart") {
console.log("Touch interaction started");
}
});Fired during zoom transform changes.
Triggers:
Usage Examples:
import { zoom } from "d3-zoom";
import { select } from "d3-selection";
const svg = select("svg");
const zoomBehavior = zoom()
.on("zoom", function(event) {
const { transform, sourceEvent } = event;
// Apply transform to content
svg.select(".content").attr("transform", transform);
// Update scale-dependent elements
svg.selectAll(".scale-dependent")
.attr("stroke-width", 1 / transform.k);
// Update info display
select("#zoom-info")
.text(`Scale: ${transform.k.toFixed(2)}, X: ${transform.x.toFixed(0)}, Y: ${transform.y.toFixed(0)}`);
// Throttle expensive operations
if (sourceEvent && sourceEvent.type === "wheel") {
clearTimeout(this._updateTimeout);
this._updateTimeout = setTimeout(() => {
updateExpensiveVisualization(transform);
}, 100);
} else {
updateExpensiveVisualization(transform);
}
});Fired when a zoom gesture completes.
Triggers:
Usage Examples:
import { zoom } from "d3-zoom";
const zoomBehavior = zoom()
.on("end", function(event) {
// Remove visual feedback
this.classList.remove("zooming");
this.style.pointerEvents = "";
// Save zoom state
const { transform } = event;
localStorage.setItem("zoomState", JSON.stringify({
k: transform.k,
x: transform.x,
y: transform.y
}));
// Trigger expensive post-zoom operations
updateDataLayers(transform);
recalculateVisibleItems(transform);
});Understanding the context and data available in event handlers.
Usage Examples:
import { zoom } from "d3-zoom";
import { select, selectAll } from "d3-selection";
// Bind data to elements
const nodes = selectAll(".node")
.data(nodeData)
.call(zoom().on("zoom", function(event, d) {
// 'this' is the DOM element
// 'event' is the ZoomEvent
// 'd' is the bound data for this element
console.log("Element:", this);
console.log("Data:", d);
console.log("Transform:", event.transform);
// Apply transform with data-specific scaling
select(this)
.attr("transform", event.transform)
.select("circle")
.attr("r", d.baseRadius / event.transform.k);
}));
// Global zoom behavior
const globalZoom = zoom()
.on("zoom", function(event) {
// 'this' is the element zoom was applied to
// No bound data ('d' is undefined)
selectAll(".zoomable")
.attr("transform", event.transform);
});
select("svg").call(globalZoom);Common patterns for complex zoom event handling.
Debounced Events:
import { zoom } from "d3-zoom";
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
const zoomBehavior = zoom()
.on("zoom", function(event) {
// Immediate updates
svg.select("g").attr("transform", event.transform);
})
.on("zoom.debounced", debounce(function(event) {
// Expensive operations
updateComplexVisualization(event.transform);
}, 250));State Management:
import { zoom } from "d3-zoom";
let zoomState = {
isZooming: false,
startTransform: null,
currentTransform: null
};
const zoomBehavior = zoom()
.on("start", function(event) {
zoomState.isZooming = true;
zoomState.startTransform = event.transform;
})
.on("zoom", function(event) {
zoomState.currentTransform = event.transform;
// Calculate zoom delta
const deltaK = event.transform.k / zoomState.startTransform.k;
const deltaX = event.transform.x - zoomState.startTransform.x;
const deltaY = event.transform.y - zoomState.startTransform.y;
console.log("Zoom delta:", { deltaK, deltaX, deltaY });
})
.on("end", function(event) {
zoomState.isZooming = false;
// Determine interaction type
const transform = event.transform;
const startTransform = zoomState.startTransform;
if (Math.abs(transform.k - startTransform.k) > 0.1) {
console.log("User zoomed");
} else if (Math.abs(transform.x - startTransform.x) > 5 ||
Math.abs(transform.y - startTransform.y) > 5) {
console.log("User panned");
}
});Event Delegation:
import { zoom } from "d3-zoom";
import { select } from "d3-selection";
// Single zoom behavior for multiple elements
const sharedZoom = zoom()
.on("zoom", function(event) {
const targetClass = this.className;
// Different behavior based on element
if (targetClass.includes("main-view")) {
// Main visualization
select(this).select("g").attr("transform", event.transform);
} else if (targetClass.includes("mini-map")) {
// Mini-map with inverted behavior
const invertedTransform = event.transform.scale(0.1);
select(this).select("g").attr("transform", invertedTransform);
}
});
// Apply to multiple elements
select(".main-view").call(sharedZoom);
select(".mini-map").call(sharedZoom);Install with Tessl CLI
npx tessl i tessl/npm-d3-zoom