JSFiddle - React, Tailwind, and code Playground
HTML
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<div id="wrapper">
<svg width="400" height="400">
<g>
<circle r="200" cx="200" cy="200"></circle>
<circle r="100" cx="200" cy="200" fill="white"></circle>
</g>
</svg>
</div>
CSS
#wrapper {
width: 200px;
height: 200px;
overflow: auto;
border: solid #888 1px;
}
JavaScript
const IMAGE_WIDTH = 400;
const IMAGE_HEIGHT = 400;
function zoomed(event) {
const scale = event.transform.k;
const scaledWidth = IMAGE_WIDTH * scale;
const scaledHeight = IMAGE_HEIGHT * scale;
// Change SVG dimensions.
d3.select('svg')
.attr('width', scaledWidth)
.attr('height', scaledHeight);
// Scale the image itself.
d3.select('g').attr('transform', `scale(${scale})`);
// Move scrollbars.
const wrapper = d3.select('#wrapper').node();
wrapper.scrollLeft = -event.transform.x;
wrapper.scrollTop = -event.transform.y;
// If the image is smaller than the wrapper, move the image towards the
// center of the wrapper.
const dx = d3.max([0, wrapper.clientWidth / 2 - scaledWidth / 2]);
const dy = d3.max([0, wrapper.clientHeight / 2 - scaledHeight / 2]);
d3.select('svg').attr('transform', `translate(${dx}, ${dy})`);
}
function scrolled() {
const wrapper = d3.select('#wrapper');
const x = wrapper.node().scrollLeft + wrapper.node().clientWidth / 2;
const y = wrapper.node().scrollTop + wrapper.node().clientHeight / 2;
const scale = d3.zoomTransform(wrapper.node()).k;
// Update zoom parameters based on scrollbar positions.
wrapper.call(d3.zoom().translateTo, x / scale, y / scale);
}
// Set initial SVG dimensions.
d3.select('svg')
.attr('width', IMAGE_WIDTH)
.attr('height', IMAGE_HEIGHT);
// Set up d3-zoom and callbacks.
d3.select('#wrapper')
.on('scroll', scrolled)
.call(d3.zoom()
.scaleExtent([0.1, 10])
.translateExtent([[0, 0], [IMAGE_WIDTH, IMAGE_HEIGHT]])
.on('zoom', zoomed));