|
|
1
Andrew Reid
8 年前
在完成冷却之前,您无法预测部队布局将占据的最终界限。然而,有两种可能的解决方案可以达到预期的效果。
-
约束布局,
或者,探索在节点接近svg边界时减少力和速度。
-
当力在冷却时扩展到svg的边界之外时,更改缩放。
第一,通过在视口中对节点进行边界处理,可以达到相同的效果。然而,节点的大小不会缩小,这可能会导致相当多的混乱。有许多关于堆栈溢出的问题和答案可以处理这种方法(例如
one
)。
我不相信我以前见过第二个例子。使用d3缩放功能应该不会太难做到这一点。虽然我们无法在不运行布局的情况下预测布局的大小,但我们可以根据任何给定时间点的力的大小动态缩放。为此,我们大体上可以采用与缩放单个节点相同的方法:应用新的缩放标识。
但是,与缩放到节点时不同,我们需要确定比例。要确定比例,我们需要找到force布局的边界,并将其与svg的边界进行比较。我会用不同的方法
answer
,但这两种方法都应该可以很好地工作(我不确定哪种性能更好)。
首先我们得到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);
现在,我们像缩放到点时一样设置缩放标识,但我们要以力布局的中间为中心的点(我们可以使用刚才计算的范围来确定中间),比例是刚才确定的比例。但是,我们仅在满足某些条件的情况下应用更改—在我下面的示例中,如果节点超出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>
上面的问题是,在模拟运行期间,鼠标事件基本上被忽略了-勾号事件运行得足够快,可以有效地覆盖由于鼠标导航而产生的任何更改。
有几个潜在的解决方案:
-
当可视化冷却到足以使用鼠标导航时,停止自动缩放
-
启动用户缩放时停止自动缩放
-
在力冷却之前不要启用用户缩放
我将在这里快速实现第一个,因为它可能是最简单的。我还将通过一个常数因子来减小比例,以提供一点余量,这样当自动缩放停止时,节点应该保持在视图中。在鼠标导航不会导致可见更改的情况下(从等待开始,更改指针),我也更改了光标:
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);
}
<脚本src=”https://d3js.org/d3.v5.min.js“></script>
<svg width=“960”height=“600”></svg>
只有在知道最终边界的一般概念后,才能渲染力,避免自动缩放过度书写导航的任何时间。
|