基于边界动态设置初始 d3 缩放 - V4
Set initial d3 zoom based on boundary dynamically - V4
我有大量节点要显示在页面上,大多数情况下,由于节点放置,圆圈会超出屏幕的可见区域。
有没有办法根据节点的整个边界框动态设置初始缩放级别,以便所有节点都适合屏幕的可见区域?
更新:
我为此添加了一个fiddlehttps://jsfiddle.net/navinleon/6ygaxoyq/3/
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var zoom = d3.zoom()
.scaleExtent([-8 / 2, 4])
.on("zoom", zoomed);
svg.call(zoom);
var g = svg.append("g");
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id;
}))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
var graph = {
"nodes": [{
"id": "Myriel",
"group": 1
}, {
"id": "Napoleon",
"group": 1
}, {
"id": "Mlle.Baptistine",
"group": 1
}, {
"id": "Mme.Magloire",
"group": 1
}, {
"id": "CountessdeLo",
"group": 1
}, {
"id": "Geborand",
"group": 1
}, {
"id": "Champtercier",
"group": 1
}, {
"id": "Cravatte",
"group": 1
}, {
"id": "Count",
"group": 1
}, {
"id": "OldMan",
"group": 1
}, {
"id": "Labarre",
"group": 2
}, {
"id": "Valjean",
"group": 2
}, {
"id": "Marguerite",
"group": 3
}, {
"id": "Mme.deR",
"group": 2
}, {
"id": "Isabeau",
"group": 2
}, {
"id": "Gervais",
"group": 2
}, {
"id": "Tholomyes",
"group": 3
}, {
"id": "Listolier",
"group": 3
}, {
"id": "Fameuil",
"group": 3
}, {
"id": "Blacheville",
"group": 3
}, {
"id": "Favourite",
"group": 3
}, {
"id": "Dahlia",
"group": 3
}, {
"id": "Zephine",
"group": 3
}, {
"id": "Fantine",
"group": 3
}, {
"id": "Mme.Thenardier",
"group": 4
}, {
"id": "Thenardier",
"group": 4
}, {
"id": "Cosette",
"group": 5
}, {
"id": "Javert",
"group": 4
}, {
"id": "Fauchelevent",
"group": 0
}, {
"id": "Bamatabois",
"group": 2
}, {
"id": "Perpetue",
"group": 3
}, {
"id": "Simplice",
"group": 2
}, {
"id": "Scaufflaire",
"group": 2
}, {
"id": "Woman1",
"group": 2
}, {
"id": "Judge",
"group": 2
}, {
"id": "Champmathieu",
"group": 2
}, {
"id": "Brevet",
"group": 2
}, {
"id": "Chenildieu",
"group": 2
}, {
"id": "Cochepaille",
"group": 2
}, {
"id": "Pontmercy",
"group": 4
}, {
"id": "Boulatruelle",
"group": 6
}, {
"id": "Eponine",
"group": 4
}, {
"id": "Anzelma",
"group": 4
}, {
"id": "Woman2",
"group": 5
}, {
"id": "MotherInnocent",
"group": 0
}, {
"id": "Gribier",
"group": 0
}, {
"id": "Jondrette",
"group": 7
}, {
"id": "Mme.Burgon",
"group": 7
}, {
"id": "Gavroche",
"group": 8
}, {
"id": "Gillenormand",
"group": 5
}, {
"id": "Magnon",
"group": 5
}, {
"id": "Mlle.Gillenormand",
"group": 5
}, {
"id": "Mme.Pontmercy",
"group": 5
}, {
"id": "Mlle.Vaubois",
"group": 5
}, {
"id": "Lt.Gillenormand",
"group": 5
}, {
"id": "Marius",
"group": 8
}, {
"id": "BaronessT",
"group": 5
}, {
"id": "Mabeuf",
"group": 8
}, {
"id": "Enjolras",
"group": 8
}, {
"id": "Combeferre",
"group": 8
}, {
"id": "Prouvaire",
"group": 8
}, {
"id": "Feuilly",
"group": 8
}, {
"id": "Courfeyrac",
"group": 8
}, {
"id": "Bahorel",
"group": 8
}, {
"id": "Bossuet",
"group": 8
}, {
"id": "Joly",
"group": 8
}, {
"id": "Grantaire",
"group": 8
}, {
"id": "MotherPlutarch",
"group": 9
}, {
"id": "Gueulemer",
"group": 4
}, {
"id": "Babet",
"group": 4
}, {
"id": "Claquesous",
"group": 4
}, {
"id": "Montparnasse",
"group": 4
}, {
"id": "Toussaint",
"group": 5
}, {
"id": "Child1",
"group": 10
}, {
"id": "Child2",
"group": 10
}, {
"id": "Brujon",
"group": 4
}, {
"id": "Mme.Hucheloup",
"group": 8
}],
"links": [{
"source": "Napoleon",
"target": "Myriel",
"value": 1
}, {
"source": "Mlle.Baptistine",
"target": "Myriel",
"value": 8
}, {
"source": "Mme.Magloire",
"target": "Myriel",
"value": 10
}]
}
var link = g.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line");
var node = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("r", 2.5)
.on('click', clicked);
node.append("title")
.text(function(d) {
return d.id;
});
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
function ticked() {
link
.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr('r',20)
}
var active = d3.select(null);
function clicked(d) {
if (active.node() === this){
active.classed("active", false);
return reset();
}
active = d3.select(this).classed("active", true);
svg.transition()
.duration(750)
.call(zoom.transform,
d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(8)
.translate(-(+active.attr('cx')), -(+active.attr('cy')))
);
}
function reset() {
svg.transition()
.duration(750)
.call(zoom.transform,
d3.zoomIdentity
.translate(0, 0)
.scale(1)
);
}
function zoomed() {
g.attr("transform", d3.event.transform);
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="960" height="600"></svg>
预计:
在 d3 添加了所有数据后检查包含元素的大小,然后相应地缩放。参见 this question about checking the size of containers in SVG。
其中一个答案实际上有一个 d3 特定实现:
var height = d3.select('#myGroup').select('svg').node().getBBox().height;
var width = d3.select('#myGroup').select('svg').node().getBBox().width;
我把它包括在这里只是因为我的 d3 有点生锈,但我相信你可以用你现有的 g
变量做这样的事情:
g.node().getBBox().height
g.node().getBBox().width
我对 d3 的缩放不熟悉,但通常你会这样做:
let scaleRatioX = containerWidth / elementWidth
let scaleRatioY = containerHeight / elementHeight
elementWidth = elementWidth * scaleRatioX
elementHeight = elementHeight * scaleRatioY
// or, if you want to scale evenly
let scaleRatio = Math.min(scaleRatioX, scaleRatioY)
elementWidth = elementWidth * scaleRatio
elementHeight = elementHeight * scaleRatio
// then set the element size to the new values
d3 的缩放可能会进一步增加复杂性。
在冷却完成之前,你无法预料部队布局将占据的极限范围。但是,有两种可能的解决方案可能会达到预期的效果。
约束布局,或者探索在节点接近 svg 边界时减少力和速度。
随着力在冷却时扩展到 svg 的边界之外,更改缩放比例。
第一个通过在视口中限制节点来实现相同的效果。但是,节点的大小不会缩小,这可能会导致相当多的混乱。有许多关于堆栈溢出的问题和答案是处理这种方法的(比如这个one)。
我不相信我以前见过第二个例子。使用 d3 缩放功能,这应该不会太难。虽然我们无法在没有 运行 的情况下预测布局的大小,但我们可以根据任何给定时间点的力大小动态缩放。为此,我们可以在很大程度上采用您用于缩放到单个节点的相同方法:应用新的缩放标识。
但是,与缩放到节点时不同,我们需要确定比例。为了确定比例,我们需要找到力布局的边界并将其与 svg 的边界进行比较。我将使用与其他 不同的方法,但任何一种方法都应该可以正常工作(我不确定哪种方法性能更高)。
首先我们得到 x 和 y 坐标的范围:
var xExtent = d3.extent(node.data(), function(d) { return d.x; });
var yExtent = d3.extent(node.data(), function(d) { return d.y; });
我们也可以在这里容纳半径,我只是使用节点中心来回答这个问题
接下来我们得到 x 和 y 的尺度:
var xScale = width/(xExtent[1]-xExtent[0]);
var yScale = height/(yExtent[1]-yExtent[0]);
然后我们找出哪个更局限并使用该比例:
var minScale = Math.min(xScale,yScale);
现在我们像缩放到一个点一样设置缩放标识,但是我们要居中的点是力布局的中间(我们可以使用刚刚计算的范围来确定中间) ,而scale就是我们刚才确定的scale。但是,我们仅在满足某些条件时应用更改 - 在我下面的示例中,如果节点超出 svg 的范围:
if(minScale < 1) {
var transform = d3.zoomIdentity.translate(width/2,height/2)
.scale(minScale)
.translate(-(xExtent[0]+xExtent[1])/2,-(yExtent[0]+yExtent[1])/2)
svg.call(zoom.transform, transform);
}
下面是这种方法的演示,嵌入在 tick 函数中:
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var zoom = d3.zoom()
.scaleExtent([-8 / 2, 4])
.on("zoom", zoomed);
svg.call(zoom);
var g = svg.append("g");
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id;
}))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
var graph = {
"nodes": [{
"id": "Myriel",
"group": 1
}, {
"id": "Napoleon",
"group": 1
}, {
"id": "Mlle.Baptistine",
"group": 1
}, {
"id": "Mme.Magloire",
"group": 1
}, {
"id": "CountessdeLo",
"group": 1
}, {
"id": "Geborand",
"group": 1
}, {
"id": "Champtercier",
"group": 1
}, {
"id": "Cravatte",
"group": 1
}, {
"id": "Count",
"group": 1
}, {
"id": "OldMan",
"group": 1
}, {
"id": "Labarre",
"group": 2
}, {
"id": "Valjean",
"group": 2
}, {
"id": "Marguerite",
"group": 3
}, {
"id": "Mme.deR",
"group": 2
}, {
"id": "Isabeau",
"group": 2
}, {
"id": "Gervais",
"group": 2
}, {
"id": "Tholomyes",
"group": 3
}, {
"id": "Listolier",
"group": 3
}, {
"id": "Fameuil",
"group": 3
}, {
"id": "Blacheville",
"group": 3
}, {
"id": "Favourite",
"group": 3
}, {
"id": "Dahlia",
"group": 3
}, {
"id": "Zephine",
"group": 3
}, {
"id": "Fantine",
"group": 3
}, {
"id": "Mme.Thenardier",
"group": 4
}, {
"id": "Thenardier",
"group": 4
}, {
"id": "Cosette",
"group": 5
}, {
"id": "Javert",
"group": 4
}, {
"id": "Fauchelevent",
"group": 0
}, {
"id": "Bamatabois",
"group": 2
}, {
"id": "Perpetue",
"group": 3
}, {
"id": "Simplice",
"group": 2
}, {
"id": "Scaufflaire",
"group": 2
}, {
"id": "Woman1",
"group": 2
}, {
"id": "Judge",
"group": 2
}, {
"id": "Champmathieu",
"group": 2
}, {
"id": "Brevet",
"group": 2
}, {
"id": "Chenildieu",
"group": 2
}, {
"id": "Cochepaille",
"group": 2
}, {
"id": "Pontmercy",
"group": 4
}, {
"id": "Boulatruelle",
"group": 6
}, {
"id": "Eponine",
"group": 4
}, {
"id": "Anzelma",
"group": 4
}, {
"id": "Woman2",
"group": 5
}, {
"id": "MotherInnocent",
"group": 0
}, {
"id": "Gribier",
"group": 0
}, {
"id": "Jondrette",
"group": 7
}, {
"id": "Mme.Burgon",
"group": 7
}, {
"id": "Gavroche",
"group": 8
}, {
"id": "Gillenormand",
"group": 5
}, {
"id": "Magnon",
"group": 5
}, {
"id": "Mlle.Gillenormand",
"group": 5
}, {
"id": "Mme.Pontmercy",
"group": 5
}, {
"id": "Mlle.Vaubois",
"group": 5
}, {
"id": "Lt.Gillenormand",
"group": 5
}, {
"id": "Marius",
"group": 8
}, {
"id": "BaronessT",
"group": 5
}, {
"id": "Mabeuf",
"group": 8
}, {
"id": "Enjolras",
"group": 8
}, {
"id": "Combeferre",
"group": 8
}, {
"id": "Prouvaire",
"group": 8
}, {
"id": "Feuilly",
"group": 8
}, {
"id": "Courfeyrac",
"group": 8
}, {
"id": "Bahorel",
"group": 8
}, {
"id": "Bossuet",
"group": 8
}, {
"id": "Joly",
"group": 8
}, {
"id": "Grantaire",
"group": 8
}, {
"id": "MotherPlutarch",
"group": 9
}, {
"id": "Gueulemer",
"group": 4
}, {
"id": "Babet",
"group": 4
}, {
"id": "Claquesous",
"group": 4
}, {
"id": "Montparnasse",
"group": 4
}, {
"id": "Toussaint",
"group": 5
}, {
"id": "Child1",
"group": 10
}, {
"id": "Child2",
"group": 10
}, {
"id": "Brujon",
"group": 4
}, {
"id": "Mme.Hucheloup",
"group": 8
}],
"links": [{
"source": "Napoleon",
"target": "Myriel",
"value": 1
}, {
"source": "Mlle.Baptistine",
"target": "Myriel",
"value": 8
}, {
"source": "Mme.Magloire",
"target": "Myriel",
"value": 10
}]
}
var link = g.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line");
var node = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("r", 2.5)
.on('click', clicked);
node.append("title")
.text(function(d) {
return d.id;
});
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
function ticked() {
// set up zoom transform:
var xExtent = d3.extent(node.data(), function(d) { return d.x; });
var yExtent = d3.extent(node.data(), function(d) { return d.y; });
// get scales:
var xScale = width/(xExtent[1] - xExtent[0]);
var yScale = height/(yExtent[1] - yExtent[0]);
// get most restrictive scale
var minScale = Math.min(xScale,yScale);
if (minScale < 1) {
var transform = d3.zoomIdentity.translate(width/2,height/2)
.scale(minScale)
.translate(-(xExtent[0]+xExtent[1])/2,-(yExtent[0]+yExtent[1])/2)
svg.call(zoom.transform, transform);
}
link
.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr('r',20)
}
var active = d3.select(null);
function clicked(d) {
if (active.node() === this){
active.classed("active", false);
return reset();
}
active = d3.select(this).classed("active", true);
svg.transition()
.duration(750)
.call(zoom.transform,
d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(8)
.translate(-(+active.attr('cx')), -(+active.attr('cy')))
);
}
function reset() {
svg.transition()
.duration(750)
.call(zoom.transform,
d3.zoomIdentity
.translate(0, 0)
.scale(1)
);
}
function zoomed() {
g.attr("transform", d3.event.transform);
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="960" height="600"></svg>
上面的问题是在模拟过程中鼠标事件基本上被忽略了 运行 - tick 事件 运行 足够快,可以有效地覆盖由于鼠标导航引起的任何更改。
有几个可能的解决方案:
当可视化冷却到鼠标导航可能有用时停止自动缩放
启动用户缩放时停止自动缩放
在力冷却之前不要启用用户缩放
我将在这里快速实现第一个,因为它可能是最简单的。我还将按常数因子减小比例以留出一些余量,以便在自动缩放停止时,节点应保持在视图中。我还在鼠标导航不会导致可见变化的时间内更改了光标(从等待开始,更改为指针):
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var zoom = d3.zoom()
.scaleExtent([-8 / 2, 4])
.on("zoom", zoomed);
svg.call(zoom);
var g = svg.append("g");
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id;
}))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
var graph = {
"nodes": [{
"id": "Myriel",
"group": 1
}, {
"id": "Napoleon",
"group": 1
}, {
"id": "Mlle.Baptistine",
"group": 1
}, {
"id": "Mme.Magloire",
"group": 1
}, {
"id": "CountessdeLo",
"group": 1
}, {
"id": "Geborand",
"group": 1
}, {
"id": "Champtercier",
"group": 1
}, {
"id": "Cravatte",
"group": 1
}, {
"id": "Count",
"group": 1
}, {
"id": "OldMan",
"group": 1
}, {
"id": "Labarre",
"group": 2
}, {
"id": "Valjean",
"group": 2
}, {
"id": "Marguerite",
"group": 3
}, {
"id": "Mme.deR",
"group": 2
}, {
"id": "Isabeau",
"group": 2
}, {
"id": "Gervais",
"group": 2
}, {
"id": "Tholomyes",
"group": 3
}, {
"id": "Listolier",
"group": 3
}, {
"id": "Fameuil",
"group": 3
}, {
"id": "Blacheville",
"group": 3
}, {
"id": "Favourite",
"group": 3
}, {
"id": "Dahlia",
"group": 3
}, {
"id": "Zephine",
"group": 3
}, {
"id": "Fantine",
"group": 3
}, {
"id": "Mme.Thenardier",
"group": 4
}, {
"id": "Thenardier",
"group": 4
}, {
"id": "Cosette",
"group": 5
}, {
"id": "Javert",
"group": 4
}, {
"id": "Fauchelevent",
"group": 0
}, {
"id": "Bamatabois",
"group": 2
}, {
"id": "Perpetue",
"group": 3
}, {
"id": "Simplice",
"group": 2
}, {
"id": "Scaufflaire",
"group": 2
}, {
"id": "Woman1",
"group": 2
}, {
"id": "Judge",
"group": 2
}, {
"id": "Champmathieu",
"group": 2
}, {
"id": "Brevet",
"group": 2
}, {
"id": "Chenildieu",
"group": 2
}, {
"id": "Cochepaille",
"group": 2
}, {
"id": "Pontmercy",
"group": 4
}, {
"id": "Boulatruelle",
"group": 6
}, {
"id": "Eponine",
"group": 4
}, {
"id": "Anzelma",
"group": 4
}, {
"id": "Woman2",
"group": 5
}, {
"id": "MotherInnocent",
"group": 0
}, {
"id": "Gribier",
"group": 0
}, {
"id": "Jondrette",
"group": 7
}, {
"id": "Mme.Burgon",
"group": 7
}, {
"id": "Gavroche",
"group": 8
}, {
"id": "Gillenormand",
"group": 5
}, {
"id": "Magnon",
"group": 5
}, {
"id": "Mlle.Gillenormand",
"group": 5
}, {
"id": "Mme.Pontmercy",
"group": 5
}, {
"id": "Mlle.Vaubois",
"group": 5
}, {
"id": "Lt.Gillenormand",
"group": 5
}, {
"id": "Marius",
"group": 8
}, {
"id": "BaronessT",
"group": 5
}, {
"id": "Mabeuf",
"group": 8
}, {
"id": "Enjolras",
"group": 8
}, {
"id": "Combeferre",
"group": 8
}, {
"id": "Prouvaire",
"group": 8
}, {
"id": "Feuilly",
"group": 8
}, {
"id": "Courfeyrac",
"group": 8
}, {
"id": "Bahorel",
"group": 8
}, {
"id": "Bossuet",
"group": 8
}, {
"id": "Joly",
"group": 8
}, {
"id": "Grantaire",
"group": 8
}, {
"id": "MotherPlutarch",
"group": 9
}, {
"id": "Gueulemer",
"group": 4
}, {
"id": "Babet",
"group": 4
}, {
"id": "Claquesous",
"group": 4
}, {
"id": "Montparnasse",
"group": 4
}, {
"id": "Toussaint",
"group": 5
}, {
"id": "Child1",
"group": 10
}, {
"id": "Child2",
"group": 10
}, {
"id": "Brujon",
"group": 4
}, {
"id": "Mme.Hucheloup",
"group": 8
}],
"links": [{
"source": "Napoleon",
"target": "Myriel",
"value": 1
}, {
"source": "Mlle.Baptistine",
"target": "Myriel",
"value": 8
}, {
"source": "Mme.Magloire",
"target": "Myriel",
"value": 10
}]
}
var link = g.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line");
var node = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("r", 2.5)
.on('click', clicked);
node.append("title")
.text(function(d) {
return d.id;
});
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
var check = true;
svg.attr("cursor","wait")
function ticked() {
if(this.alpha() > 0.04) {
// set up zoom transform:
var xExtent = d3.extent(node.data(), function(d) { return d.x; });
var yExtent = d3.extent(node.data(), function(d) { return d.y; });
// get scales:
var xScale = width/(xExtent[1] - xExtent[0]) * 0.75;
var yScale = height/(yExtent[1] - yExtent[0]) * 0.75;
// get most restrictive scale
var minScale = Math.min(xScale,yScale);
if (minScale < 1) {
var transform = d3.zoomIdentity.translate(width/2,height/2)
.scale(minScale)
.translate(-(xExtent[0]+xExtent[1])/2,-(yExtent[0]+yExtent[1])/2)
svg.call(zoom.transform, transform);
}
}
else {
svg.attr("cursor","pointer")
if(check) console.log("check");
var check = false;
}
link
.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr('r',20)
}
var active = d3.select(null);
function clicked(d) {
if (active.node() === this){
active.classed("active", false);
return reset();
}
active = d3.select(this).classed("active", true);
svg.transition()
.duration(750)
.call(zoom.transform,
d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(8)
.translate(-(+active.attr('cx')), -(+active.attr('cy')))
);
}
function reset() {
svg.transition()
.duration(750)
.call(zoom.transform,
d3.zoomIdentity
.translate(0, 0)
.scale(1)
);
}
function zoomed() {
g.attr("transform", d3.event.transform);
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="960" height="600"></svg>
力也只能在知道最终边界的一般概念后才能呈现,避免任何时候导航被自动缩放覆盖。
我有大量节点要显示在页面上,大多数情况下,由于节点放置,圆圈会超出屏幕的可见区域。
有没有办法根据节点的整个边界框动态设置初始缩放级别,以便所有节点都适合屏幕的可见区域?
更新:
我为此添加了一个fiddlehttps://jsfiddle.net/navinleon/6ygaxoyq/3/
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var zoom = d3.zoom()
.scaleExtent([-8 / 2, 4])
.on("zoom", zoomed);
svg.call(zoom);
var g = svg.append("g");
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id;
}))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
var graph = {
"nodes": [{
"id": "Myriel",
"group": 1
}, {
"id": "Napoleon",
"group": 1
}, {
"id": "Mlle.Baptistine",
"group": 1
}, {
"id": "Mme.Magloire",
"group": 1
}, {
"id": "CountessdeLo",
"group": 1
}, {
"id": "Geborand",
"group": 1
}, {
"id": "Champtercier",
"group": 1
}, {
"id": "Cravatte",
"group": 1
}, {
"id": "Count",
"group": 1
}, {
"id": "OldMan",
"group": 1
}, {
"id": "Labarre",
"group": 2
}, {
"id": "Valjean",
"group": 2
}, {
"id": "Marguerite",
"group": 3
}, {
"id": "Mme.deR",
"group": 2
}, {
"id": "Isabeau",
"group": 2
}, {
"id": "Gervais",
"group": 2
}, {
"id": "Tholomyes",
"group": 3
}, {
"id": "Listolier",
"group": 3
}, {
"id": "Fameuil",
"group": 3
}, {
"id": "Blacheville",
"group": 3
}, {
"id": "Favourite",
"group": 3
}, {
"id": "Dahlia",
"group": 3
}, {
"id": "Zephine",
"group": 3
}, {
"id": "Fantine",
"group": 3
}, {
"id": "Mme.Thenardier",
"group": 4
}, {
"id": "Thenardier",
"group": 4
}, {
"id": "Cosette",
"group": 5
}, {
"id": "Javert",
"group": 4
}, {
"id": "Fauchelevent",
"group": 0
}, {
"id": "Bamatabois",
"group": 2
}, {
"id": "Perpetue",
"group": 3
}, {
"id": "Simplice",
"group": 2
}, {
"id": "Scaufflaire",
"group": 2
}, {
"id": "Woman1",
"group": 2
}, {
"id": "Judge",
"group": 2
}, {
"id": "Champmathieu",
"group": 2
}, {
"id": "Brevet",
"group": 2
}, {
"id": "Chenildieu",
"group": 2
}, {
"id": "Cochepaille",
"group": 2
}, {
"id": "Pontmercy",
"group": 4
}, {
"id": "Boulatruelle",
"group": 6
}, {
"id": "Eponine",
"group": 4
}, {
"id": "Anzelma",
"group": 4
}, {
"id": "Woman2",
"group": 5
}, {
"id": "MotherInnocent",
"group": 0
}, {
"id": "Gribier",
"group": 0
}, {
"id": "Jondrette",
"group": 7
}, {
"id": "Mme.Burgon",
"group": 7
}, {
"id": "Gavroche",
"group": 8
}, {
"id": "Gillenormand",
"group": 5
}, {
"id": "Magnon",
"group": 5
}, {
"id": "Mlle.Gillenormand",
"group": 5
}, {
"id": "Mme.Pontmercy",
"group": 5
}, {
"id": "Mlle.Vaubois",
"group": 5
}, {
"id": "Lt.Gillenormand",
"group": 5
}, {
"id": "Marius",
"group": 8
}, {
"id": "BaronessT",
"group": 5
}, {
"id": "Mabeuf",
"group": 8
}, {
"id": "Enjolras",
"group": 8
}, {
"id": "Combeferre",
"group": 8
}, {
"id": "Prouvaire",
"group": 8
}, {
"id": "Feuilly",
"group": 8
}, {
"id": "Courfeyrac",
"group": 8
}, {
"id": "Bahorel",
"group": 8
}, {
"id": "Bossuet",
"group": 8
}, {
"id": "Joly",
"group": 8
}, {
"id": "Grantaire",
"group": 8
}, {
"id": "MotherPlutarch",
"group": 9
}, {
"id": "Gueulemer",
"group": 4
}, {
"id": "Babet",
"group": 4
}, {
"id": "Claquesous",
"group": 4
}, {
"id": "Montparnasse",
"group": 4
}, {
"id": "Toussaint",
"group": 5
}, {
"id": "Child1",
"group": 10
}, {
"id": "Child2",
"group": 10
}, {
"id": "Brujon",
"group": 4
}, {
"id": "Mme.Hucheloup",
"group": 8
}],
"links": [{
"source": "Napoleon",
"target": "Myriel",
"value": 1
}, {
"source": "Mlle.Baptistine",
"target": "Myriel",
"value": 8
}, {
"source": "Mme.Magloire",
"target": "Myriel",
"value": 10
}]
}
var link = g.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line");
var node = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("r", 2.5)
.on('click', clicked);
node.append("title")
.text(function(d) {
return d.id;
});
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
function ticked() {
link
.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr('r',20)
}
var active = d3.select(null);
function clicked(d) {
if (active.node() === this){
active.classed("active", false);
return reset();
}
active = d3.select(this).classed("active", true);
svg.transition()
.duration(750)
.call(zoom.transform,
d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(8)
.translate(-(+active.attr('cx')), -(+active.attr('cy')))
);
}
function reset() {
svg.transition()
.duration(750)
.call(zoom.transform,
d3.zoomIdentity
.translate(0, 0)
.scale(1)
);
}
function zoomed() {
g.attr("transform", d3.event.transform);
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="960" height="600"></svg>
预计:
在 d3 添加了所有数据后检查包含元素的大小,然后相应地缩放。参见 this question about checking the size of containers in SVG。
其中一个答案实际上有一个 d3 特定实现:
var height = d3.select('#myGroup').select('svg').node().getBBox().height;
var width = d3.select('#myGroup').select('svg').node().getBBox().width;
我把它包括在这里只是因为我的 d3 有点生锈,但我相信你可以用你现有的 g
变量做这样的事情:
g.node().getBBox().height
g.node().getBBox().width
我对 d3 的缩放不熟悉,但通常你会这样做:
let scaleRatioX = containerWidth / elementWidth
let scaleRatioY = containerHeight / elementHeight
elementWidth = elementWidth * scaleRatioX
elementHeight = elementHeight * scaleRatioY
// or, if you want to scale evenly
let scaleRatio = Math.min(scaleRatioX, scaleRatioY)
elementWidth = elementWidth * scaleRatio
elementHeight = elementHeight * scaleRatio
// then set the element size to the new values
d3 的缩放可能会进一步增加复杂性。
在冷却完成之前,你无法预料部队布局将占据的极限范围。但是,有两种可能的解决方案可能会达到预期的效果。
约束布局,或者探索在节点接近 svg 边界时减少力和速度。
随着力在冷却时扩展到 svg 的边界之外,更改缩放比例。
第一个通过在视口中限制节点来实现相同的效果。但是,节点的大小不会缩小,这可能会导致相当多的混乱。有许多关于堆栈溢出的问题和答案是处理这种方法的(比如这个one)。
我不相信我以前见过第二个例子。使用 d3 缩放功能,这应该不会太难。虽然我们无法在没有 运行 的情况下预测布局的大小,但我们可以根据任何给定时间点的力大小动态缩放。为此,我们可以在很大程度上采用您用于缩放到单个节点的相同方法:应用新的缩放标识。
但是,与缩放到节点时不同,我们需要确定比例。为了确定比例,我们需要找到力布局的边界并将其与 svg 的边界进行比较。我将使用与其他
首先我们得到 x 和 y 坐标的范围:
var xExtent = d3.extent(node.data(), function(d) { return d.x; });
var yExtent = d3.extent(node.data(), function(d) { return d.y; });
我们也可以在这里容纳半径,我只是使用节点中心来回答这个问题
接下来我们得到 x 和 y 的尺度:
var xScale = width/(xExtent[1]-xExtent[0]);
var yScale = height/(yExtent[1]-yExtent[0]);
然后我们找出哪个更局限并使用该比例:
var minScale = Math.min(xScale,yScale);
现在我们像缩放到一个点一样设置缩放标识,但是我们要居中的点是力布局的中间(我们可以使用刚刚计算的范围来确定中间) ,而scale就是我们刚才确定的scale。但是,我们仅在满足某些条件时应用更改 - 在我下面的示例中,如果节点超出 svg 的范围:
if(minScale < 1) {
var transform = d3.zoomIdentity.translate(width/2,height/2)
.scale(minScale)
.translate(-(xExtent[0]+xExtent[1])/2,-(yExtent[0]+yExtent[1])/2)
svg.call(zoom.transform, transform);
}
下面是这种方法的演示,嵌入在 tick 函数中:
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var zoom = d3.zoom()
.scaleExtent([-8 / 2, 4])
.on("zoom", zoomed);
svg.call(zoom);
var g = svg.append("g");
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id;
}))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
var graph = {
"nodes": [{
"id": "Myriel",
"group": 1
}, {
"id": "Napoleon",
"group": 1
}, {
"id": "Mlle.Baptistine",
"group": 1
}, {
"id": "Mme.Magloire",
"group": 1
}, {
"id": "CountessdeLo",
"group": 1
}, {
"id": "Geborand",
"group": 1
}, {
"id": "Champtercier",
"group": 1
}, {
"id": "Cravatte",
"group": 1
}, {
"id": "Count",
"group": 1
}, {
"id": "OldMan",
"group": 1
}, {
"id": "Labarre",
"group": 2
}, {
"id": "Valjean",
"group": 2
}, {
"id": "Marguerite",
"group": 3
}, {
"id": "Mme.deR",
"group": 2
}, {
"id": "Isabeau",
"group": 2
}, {
"id": "Gervais",
"group": 2
}, {
"id": "Tholomyes",
"group": 3
}, {
"id": "Listolier",
"group": 3
}, {
"id": "Fameuil",
"group": 3
}, {
"id": "Blacheville",
"group": 3
}, {
"id": "Favourite",
"group": 3
}, {
"id": "Dahlia",
"group": 3
}, {
"id": "Zephine",
"group": 3
}, {
"id": "Fantine",
"group": 3
}, {
"id": "Mme.Thenardier",
"group": 4
}, {
"id": "Thenardier",
"group": 4
}, {
"id": "Cosette",
"group": 5
}, {
"id": "Javert",
"group": 4
}, {
"id": "Fauchelevent",
"group": 0
}, {
"id": "Bamatabois",
"group": 2
}, {
"id": "Perpetue",
"group": 3
}, {
"id": "Simplice",
"group": 2
}, {
"id": "Scaufflaire",
"group": 2
}, {
"id": "Woman1",
"group": 2
}, {
"id": "Judge",
"group": 2
}, {
"id": "Champmathieu",
"group": 2
}, {
"id": "Brevet",
"group": 2
}, {
"id": "Chenildieu",
"group": 2
}, {
"id": "Cochepaille",
"group": 2
}, {
"id": "Pontmercy",
"group": 4
}, {
"id": "Boulatruelle",
"group": 6
}, {
"id": "Eponine",
"group": 4
}, {
"id": "Anzelma",
"group": 4
}, {
"id": "Woman2",
"group": 5
}, {
"id": "MotherInnocent",
"group": 0
}, {
"id": "Gribier",
"group": 0
}, {
"id": "Jondrette",
"group": 7
}, {
"id": "Mme.Burgon",
"group": 7
}, {
"id": "Gavroche",
"group": 8
}, {
"id": "Gillenormand",
"group": 5
}, {
"id": "Magnon",
"group": 5
}, {
"id": "Mlle.Gillenormand",
"group": 5
}, {
"id": "Mme.Pontmercy",
"group": 5
}, {
"id": "Mlle.Vaubois",
"group": 5
}, {
"id": "Lt.Gillenormand",
"group": 5
}, {
"id": "Marius",
"group": 8
}, {
"id": "BaronessT",
"group": 5
}, {
"id": "Mabeuf",
"group": 8
}, {
"id": "Enjolras",
"group": 8
}, {
"id": "Combeferre",
"group": 8
}, {
"id": "Prouvaire",
"group": 8
}, {
"id": "Feuilly",
"group": 8
}, {
"id": "Courfeyrac",
"group": 8
}, {
"id": "Bahorel",
"group": 8
}, {
"id": "Bossuet",
"group": 8
}, {
"id": "Joly",
"group": 8
}, {
"id": "Grantaire",
"group": 8
}, {
"id": "MotherPlutarch",
"group": 9
}, {
"id": "Gueulemer",
"group": 4
}, {
"id": "Babet",
"group": 4
}, {
"id": "Claquesous",
"group": 4
}, {
"id": "Montparnasse",
"group": 4
}, {
"id": "Toussaint",
"group": 5
}, {
"id": "Child1",
"group": 10
}, {
"id": "Child2",
"group": 10
}, {
"id": "Brujon",
"group": 4
}, {
"id": "Mme.Hucheloup",
"group": 8
}],
"links": [{
"source": "Napoleon",
"target": "Myriel",
"value": 1
}, {
"source": "Mlle.Baptistine",
"target": "Myriel",
"value": 8
}, {
"source": "Mme.Magloire",
"target": "Myriel",
"value": 10
}]
}
var link = g.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line");
var node = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("r", 2.5)
.on('click', clicked);
node.append("title")
.text(function(d) {
return d.id;
});
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
function ticked() {
// set up zoom transform:
var xExtent = d3.extent(node.data(), function(d) { return d.x; });
var yExtent = d3.extent(node.data(), function(d) { return d.y; });
// get scales:
var xScale = width/(xExtent[1] - xExtent[0]);
var yScale = height/(yExtent[1] - yExtent[0]);
// get most restrictive scale
var minScale = Math.min(xScale,yScale);
if (minScale < 1) {
var transform = d3.zoomIdentity.translate(width/2,height/2)
.scale(minScale)
.translate(-(xExtent[0]+xExtent[1])/2,-(yExtent[0]+yExtent[1])/2)
svg.call(zoom.transform, transform);
}
link
.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr('r',20)
}
var active = d3.select(null);
function clicked(d) {
if (active.node() === this){
active.classed("active", false);
return reset();
}
active = d3.select(this).classed("active", true);
svg.transition()
.duration(750)
.call(zoom.transform,
d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(8)
.translate(-(+active.attr('cx')), -(+active.attr('cy')))
);
}
function reset() {
svg.transition()
.duration(750)
.call(zoom.transform,
d3.zoomIdentity
.translate(0, 0)
.scale(1)
);
}
function zoomed() {
g.attr("transform", d3.event.transform);
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="960" height="600"></svg>
上面的问题是在模拟过程中鼠标事件基本上被忽略了 运行 - tick 事件 运行 足够快,可以有效地覆盖由于鼠标导航引起的任何更改。
有几个可能的解决方案:
当可视化冷却到鼠标导航可能有用时停止自动缩放
启动用户缩放时停止自动缩放
在力冷却之前不要启用用户缩放
我将在这里快速实现第一个,因为它可能是最简单的。我还将按常数因子减小比例以留出一些余量,以便在自动缩放停止时,节点应保持在视图中。我还在鼠标导航不会导致可见变化的时间内更改了光标(从等待开始,更改为指针):
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var zoom = d3.zoom()
.scaleExtent([-8 / 2, 4])
.on("zoom", zoomed);
svg.call(zoom);
var g = svg.append("g");
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id;
}))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
var graph = {
"nodes": [{
"id": "Myriel",
"group": 1
}, {
"id": "Napoleon",
"group": 1
}, {
"id": "Mlle.Baptistine",
"group": 1
}, {
"id": "Mme.Magloire",
"group": 1
}, {
"id": "CountessdeLo",
"group": 1
}, {
"id": "Geborand",
"group": 1
}, {
"id": "Champtercier",
"group": 1
}, {
"id": "Cravatte",
"group": 1
}, {
"id": "Count",
"group": 1
}, {
"id": "OldMan",
"group": 1
}, {
"id": "Labarre",
"group": 2
}, {
"id": "Valjean",
"group": 2
}, {
"id": "Marguerite",
"group": 3
}, {
"id": "Mme.deR",
"group": 2
}, {
"id": "Isabeau",
"group": 2
}, {
"id": "Gervais",
"group": 2
}, {
"id": "Tholomyes",
"group": 3
}, {
"id": "Listolier",
"group": 3
}, {
"id": "Fameuil",
"group": 3
}, {
"id": "Blacheville",
"group": 3
}, {
"id": "Favourite",
"group": 3
}, {
"id": "Dahlia",
"group": 3
}, {
"id": "Zephine",
"group": 3
}, {
"id": "Fantine",
"group": 3
}, {
"id": "Mme.Thenardier",
"group": 4
}, {
"id": "Thenardier",
"group": 4
}, {
"id": "Cosette",
"group": 5
}, {
"id": "Javert",
"group": 4
}, {
"id": "Fauchelevent",
"group": 0
}, {
"id": "Bamatabois",
"group": 2
}, {
"id": "Perpetue",
"group": 3
}, {
"id": "Simplice",
"group": 2
}, {
"id": "Scaufflaire",
"group": 2
}, {
"id": "Woman1",
"group": 2
}, {
"id": "Judge",
"group": 2
}, {
"id": "Champmathieu",
"group": 2
}, {
"id": "Brevet",
"group": 2
}, {
"id": "Chenildieu",
"group": 2
}, {
"id": "Cochepaille",
"group": 2
}, {
"id": "Pontmercy",
"group": 4
}, {
"id": "Boulatruelle",
"group": 6
}, {
"id": "Eponine",
"group": 4
}, {
"id": "Anzelma",
"group": 4
}, {
"id": "Woman2",
"group": 5
}, {
"id": "MotherInnocent",
"group": 0
}, {
"id": "Gribier",
"group": 0
}, {
"id": "Jondrette",
"group": 7
}, {
"id": "Mme.Burgon",
"group": 7
}, {
"id": "Gavroche",
"group": 8
}, {
"id": "Gillenormand",
"group": 5
}, {
"id": "Magnon",
"group": 5
}, {
"id": "Mlle.Gillenormand",
"group": 5
}, {
"id": "Mme.Pontmercy",
"group": 5
}, {
"id": "Mlle.Vaubois",
"group": 5
}, {
"id": "Lt.Gillenormand",
"group": 5
}, {
"id": "Marius",
"group": 8
}, {
"id": "BaronessT",
"group": 5
}, {
"id": "Mabeuf",
"group": 8
}, {
"id": "Enjolras",
"group": 8
}, {
"id": "Combeferre",
"group": 8
}, {
"id": "Prouvaire",
"group": 8
}, {
"id": "Feuilly",
"group": 8
}, {
"id": "Courfeyrac",
"group": 8
}, {
"id": "Bahorel",
"group": 8
}, {
"id": "Bossuet",
"group": 8
}, {
"id": "Joly",
"group": 8
}, {
"id": "Grantaire",
"group": 8
}, {
"id": "MotherPlutarch",
"group": 9
}, {
"id": "Gueulemer",
"group": 4
}, {
"id": "Babet",
"group": 4
}, {
"id": "Claquesous",
"group": 4
}, {
"id": "Montparnasse",
"group": 4
}, {
"id": "Toussaint",
"group": 5
}, {
"id": "Child1",
"group": 10
}, {
"id": "Child2",
"group": 10
}, {
"id": "Brujon",
"group": 4
}, {
"id": "Mme.Hucheloup",
"group": 8
}],
"links": [{
"source": "Napoleon",
"target": "Myriel",
"value": 1
}, {
"source": "Mlle.Baptistine",
"target": "Myriel",
"value": 8
}, {
"source": "Mme.Magloire",
"target": "Myriel",
"value": 10
}]
}
var link = g.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line");
var node = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("r", 2.5)
.on('click', clicked);
node.append("title")
.text(function(d) {
return d.id;
});
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
var check = true;
svg.attr("cursor","wait")
function ticked() {
if(this.alpha() > 0.04) {
// set up zoom transform:
var xExtent = d3.extent(node.data(), function(d) { return d.x; });
var yExtent = d3.extent(node.data(), function(d) { return d.y; });
// get scales:
var xScale = width/(xExtent[1] - xExtent[0]) * 0.75;
var yScale = height/(yExtent[1] - yExtent[0]) * 0.75;
// get most restrictive scale
var minScale = Math.min(xScale,yScale);
if (minScale < 1) {
var transform = d3.zoomIdentity.translate(width/2,height/2)
.scale(minScale)
.translate(-(xExtent[0]+xExtent[1])/2,-(yExtent[0]+yExtent[1])/2)
svg.call(zoom.transform, transform);
}
}
else {
svg.attr("cursor","pointer")
if(check) console.log("check");
var check = false;
}
link
.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr('r',20)
}
var active = d3.select(null);
function clicked(d) {
if (active.node() === this){
active.classed("active", false);
return reset();
}
active = d3.select(this).classed("active", true);
svg.transition()
.duration(750)
.call(zoom.transform,
d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(8)
.translate(-(+active.attr('cx')), -(+active.attr('cy')))
);
}
function reset() {
svg.transition()
.duration(750)
.call(zoom.transform,
d3.zoomIdentity
.translate(0, 0)
.scale(1)
);
}
function zoomed() {
g.attr("transform", d3.event.transform);
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="960" height="600"></svg>
力也只能在知道最终边界的一般概念后才能呈现,避免任何时候导航被自动缩放覆盖。