In my d3.js-based project I want to keep what is centered in the current view to stay centered even if the width or height of the view is changing. At the same time I want to allow the user to interact through panning and zooming.
My idea was to use have nested groups, with the outer group for the centering and the inner one as the d3.zoom target:
<g transform="translate(WIDTH/2, HEIGHT/2)">
<g transform="translate(ZOOM_X, ZOOM_Y) scale(ZOOM_K)">
...
</g>
</g>
See the following attempt:
var svgWrapper = d3.select("#svgWrapper").node(),
svg = d3.select("svg"),
width = svgWrapper.offsetWidth,
height = svgWrapper.offsetHeight;
var outerG = svg.append("g");
var g = outerG.append("g");
outerG.append("line")
.attr("stroke", "red")
.attr("stroke-width", 2)
.attr("x1", -10)
.attr("x2", 10)
.attr("y1", -10)
.attr("y2", 10);
outerG.append("line")
.attr("stroke", "red")
.attr("stroke-width", 2)
.attr("x1", -10)
.attr("x2", 10)
.attr("y1", 10)
.attr("y2", -10);
for (var i = 1; i <= 3; ++i) {
g.append("circle")
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-width", 1.5)
.attr("r", i * 5);
}
var zoom = d3.zoom();
svg.call(zoom
.scaleExtent([1 / 2, 4])
.on("zoom", zoomed));
g.call(zoom.transform, d3.zoomIdentity.translate(-width / 2, -height / 2));
function zoomed() {
g.attr("transform", d3.event.transform);
}
// https://developer.mozilla.org/en-US/docs/Web/Events/resize#requestAnimationFrame_customEvent
function throttle(type, name, obj) {
obj = obj || window;
var running = false;
var func = function() {
if (running) {
return;
}
running = true;
requestAnimationFrame(function() {
obj.dispatchEvent(new CustomEvent(name));
running = false;
});
};
obj.addEventListener(type, func);
}
function resize() {
width = svgWrapper.offsetWidth;
height = svgWrapper.offsetHeight;
zoom.extent([
[0, 0],
[width, height]
]);
outerG.attr("transform", "translate(" + (width / 2) + ", " + (height / 2) + ")");
}
resize();
throttle("resize", "optimizedResize");
window.addEventListener("optimizedResize", resize);
html,
body,
#svgWrapper,
svg {
width: 100%;
height: 100%;
overflow: hidden;
}
<div id="svgWrapper">
<svg></svg>
</div>
<script src="https://d3js.org/d3.v4.min.js"></script>
The red cross represents the viewport center and the circles the content which can be dragged around. Click "Full page" and resize the window to see the resizing in action.
Sadly the first pan as well as zooming towards a certain point seem broken. Putting both translates() into a single transform didn't work either.
Why is that? Is there anything which can be done about it?
Ideas and my failed attempts so far
Single Transform
Turning it into a single transform, i.e. calling zoom.transform on every resize, wouldn't be a good solution in my case for various reasons:
It would cause
start/zoom/endevents to be emitted when the window is being resized and even checkingevent.sourceEventthere wouldn't help muchThe pan limits (
.translateExtent()) would be relative to the display.
Transforming back inside inner transform
This would hardly have any advantages over a single transform approach and from my limited testing also seems to have issues.
zoom.extent()
I don't understand how this might help me.