我在页面上显示了大量节点,由于节点放置,大多数时候圆圈离开屏幕的可见区域。
有没有办法根据节点的整个边界框动态设置初始缩放级别,以便所有节点都适合屏幕的可见区域?
更新
我为这个https://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>
预期:
答案 0 :(得分:5)
在完成冷却之前,您无法预测力布局将占据的最终界限。但是,有两种可能的解决方案可以达到预期的效果。
当节点接近svg的边界时,约束布局或者探索减小力和速度。
在冷却时,当力展开超出svg的范围时,更改变焦。
通过绑定视口中的节点,第一个实现相同的效果。但是,节点的大小不会缩小,这可能会导致相当多的混乱。有关堆栈溢出的问题和答案有很多处理这种方法(例如one)。
我不相信我之前见过第二个例子。使用d3比例功能这不应该太难。虽然我们无法在不运行布局的情况下预测布局的大小,但我们可以根据任何给定时间点的力的大小动态缩放。为此,我们可以采用与缩放到单个节点相同的方法:应用新的缩放标识。
但是,与缩放到节点时不同,我们需要确定比例。要确定我们需要的比例,找到力布局的边界,并将其与svg的边界进行比较。我将使用与其他answer不同的方法,但任何一种方法都应该可以正常工作(我不确定哪种方法更具性能)。
首先我们得到x和y coordiantes的范围:
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);
现在我们设置缩放标识就像放大到点时一样,但是我们想要居中的点是力布局的中间(我们可以使用我们刚刚计算的范围来确定中间),以及比例是我们刚刚确定的比例。但是,如果满足某些条件,我们只会应用更改 - 在下面的示例中,如果节点超出了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>
只有在知道最终边界的一般概念时才能渲染力,从而避免任何时候自动缩放覆盖导航。
答案 1 :(得分:1)
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的变焦可能会进一步增加复杂性。