I'm using this nice force layout from Flowingdata.com to create a network diagram.
My diagram currently shows between 5 and 750 nodes with their relations. It works great with some custom changes to fit my needs. However one thing I can't get to work. I have a viewBox
with preserveAspectRatio
to auto-fit the container it is in. But depending on the number of nodes there are always some nodes around the edges (mainly top and bottom) that get cut off. And if there are very few nodes, it shows them in the middle with huge empty space around it (it's a big container it's in).
Is there any way to auto-zoom or scale the layout to auto-fit? So that a big layout gets somewhat zoomed out and a small layout zoomed in. I have a zoom event setup so scrolling and panning work like a charm. But can it automatically do that to fit the contents?
The d3.js startup code:
vis = d3.select(selection)
.append("svg")
.attr("viewBox", "0 0 " + width + " " + height )
.attr("preserveAspectRatio", "xMidYMid meet")
.attr("pointer-events", "all")
.call(d3.behavior.zoom().scaleExtent([.1, 3])
.on("zoom", redraw)).append('g');
All the other answers to date require access to data, and iterates through it so the complexity is at least O(nodes)
. I kept looking and found a way that is solely based on already rendered visual size, getBBox()
which is hopefully O(1)
. It doesn't matter what's in it or how it's laid out, just its size and the parent container's size. I managed to whip up this based on http://bl.ocks.org/mbostock/9656675:
var root = // any svg.select(...) that has a single node like a container group by #id
function lapsedZoomFit(ticks, transitionDuration) {
for (var i = ticks || 100; i > 0; --i) force.tick();
force.stop();
zoomFit(transitionDuration);
}
function zoomFit(transitionDuration) {
var bounds = root.node().getBBox();
var parent = root.node().parentElement;
var fullWidth = parent.clientWidth || parent.parentNode.clientWidth,
fullHeight = parent.clientHeight || parent.parentNode.clientHeight;
var width = bounds.width,
height = bounds.height;
var midX = bounds.x + width / 2,
midY = bounds.y + height / 2;
if (width == 0 || height == 0) return; // nothing to fit
var scale = 0.85 / Math.max(width / fullWidth, height / fullHeight);
var translate = [
fullWidth / 2 - scale * midX,
fullHeight / 2 - scale * midY
];
console.trace("zoomFit", translate, scale);
root
.transition()
.duration(transitionDuration || 0) // milliseconds
.call(zoom.translate(translate).scale(scale).event);
}
EDIT: The above works in D3 v3. Zoom is changed in D3 v4 and v5, so you have to make some minor changes to the last portion (the code below console.trace
):
var transform = d3.zoomIdentity
.translate(translate[0], translate[1])
.scale(scale);
root
.transition()
.duration(transitionDuration || 0) // milliseconds
.call(zoom.transform, transform);
WARNING (untested, but beware): According to Ngo Quang Duong in comments, if your SVG viewBox
is not 0 0 width height
format, you might need to adjust some variables, but even this might not be sufficient:
var fullWidth = parent.viewBox.baseVal.width ;
var fullHeight = parent.viewBox.baseVal.height;
var translate = [
parent.viewBox.baseVal.x + fullWidth / 2 - scale * midX,
parent.viewBox.baseVal.y + fullHeight / 2 - scale * midY
];
Your code should be similar to this
vis = d3.select(selection)
.append("svg")
.attr("viewBox", "0 0 " + width + " " + height)
.attr("preserveAspectRatio", "xMidYMid meet")
.attr("pointer-events", "all")
.call(zoomListener)
.on("zoom", redraw);
var mainGroup = vis.append('g');
function zoom() {
mainGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
var zoomListener = d3.behavior.zoom().on("zoom", zoom);
var xArray = YOUR_NODES.map(function(d) { //YOUR_NODES is something like json.nodes after forse.end()
return d.x
});
var minX = d3.min(xArray);
var maxX = d3.max(xArray);
var scaleMin = Math.abs(width / (maxX - minX));
var startX = (minX) / scaleMin;
var startY = 50 / scaleMin;
// Same as in the zoom function
mainGroup.attr("transform", "translate(" + [startX, startY] + ")scale(" + scaleMin + ")");
// Initialization start param of zoomListener
zoomListener.translate([startX, startY]);
zoomListener.scale(scaleMin);
zoomListener.scaleExtent([scaleMin, 1])
vis.call(zoomListener);
This code work only for xAxis. Because "global circle" SVG RX === RY. If it was not for you then you can add the same logic for yAxis var startY
. Also, you need to adjust the initial coordinates considering cr
of circle nodes.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With