力导向图的鱼眼效应:在图稳定之前不生效
Posted
技术标签:
【中文标题】力导向图的鱼眼效应:在图稳定之前不生效【英文标题】:Fisheye effect with force-directed graph : not taking effect until the graph settles 【发布时间】:2021-09-20 08:44:42 【问题描述】:我正在创建一个带有鱼眼效果的图表,用户可以在其光标下进行永久缩放,并且可以四处移动图表节点。
Here's what I have: (ObservableHQ)
并以 sn-p 形式:
d3.json("https://gist.githubusercontent.com/mbostock/4062045/raw/5916d145c8c048a6e3086915a6be464467391c62/miserables.json").then(draw);
function draw(data)
const fisheye = fisheyeO.circular()
.radius(100)
.distortion(5);
const height = 400;
const width = 500;
data.nodes.forEach(d=>d.fisheye=x:0,y:0,z:0)
const simulation = d3.forceSimulation(data.nodes)
.alphaDecay(0.0125)
.alphaMin(0.01)
.force("link", d3.forceLink(data.links).id(d => d.id))
.force("charge", d3.forceManyBody())
.force("x", d3.forceX(width/2))
.force("y", d3.forceY(height/2));
const svg = d3.select("body").append("svg")
.attr("viewBox", [0, 0, width, height])
const link = svg.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(data.links)
.join("line")
.attr("stroke-width", 2);
const node = svg.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll("circle")
.data(data.nodes)
.join("circle")
.attr("r", 5)
.attr("fill", "black")
svg.on("mousemove", function()
fisheye.focus(d3.mouse(this));
node.each(function(d) d.fisheye = fisheye(d); )
.attr("cx", function(d) return d.fisheye.x; )
.attr("cy", function(d) return d.fisheye.y; )
.attr("r", function(d) return d.fisheye.z * 4.5; );
link.attr("x1", function(d) return d.source.fisheye.x; )
.attr("y1", function(d) return d.source.fisheye.y; )
.attr("x2", function(d) return d.target.fisheye.x; )
.attr("y2", function(d) return d.target.fisheye.y; );
)
simulation.on("tick", () =>
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
);
const fisheye0 = fisheyeO =
circular: () =>
var radius = 200,
distortion = 2,
k0,
k1,
focus = [0, 0];
function fisheye(d)
var dx = d.x - focus[0],
dy = d.y - focus[1],
dd = Math.sqrt(dx * dx + dy * dy);
if (!dd || dd >= radius) return x: d.x, y: d.y, z: dd >= radius ? 1 : 10;
var k = k0 * (1 - Math.exp(-dd * k1)) / dd * .75 + .25;
return x: focus[0] + dx * k, y: focus[1] + dy * k, z: Math.min(k, 10);
function rescale()
k0 = Math.exp(distortion);
k0 = k0 / (k0 - 1) * radius;
k1 = distortion / radius;
return fisheye;
fisheye.radius = function(_)
if (!arguments.length) return radius;
radius = +_;
return rescale();
;
fisheye.distortion = function(_)
if (!arguments.length) return distortion;
distortion = +_;
return rescale();
;
fisheye.focus = function(_)
if (!arguments.length) return focus;
focus = _;
return fisheye;
;
return rescale();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js"></script>
我使用了 Bostock 的鱼眼效果,只要图形是静态的,它就可以正常工作。但是,如果正在运行力模拟,则它不起作用,从而产生这种效果:
我尝试将鱼眼效果重构为一种力,并直接在力模拟中使用它,如下所示:
function forceFisheye(fisheye)
let nodes;
function force()
let i;
let n = nodes.length;
let node;
for (i = 0; i < n; ++i)
node = nodes[i];
let x, y, z = fisheye(node);
node.x = x;
node.y = y;
node.z = z;
force.initialize = function (_)
nodes = _;
;
return force;
let fisheye = fisheye();
// ...
d3.forceSimulation()
.force("fisheye", forceFisheye(fisheye));
但这会产生奇怪的结果,使节点远离我的光标。
如何使用具有鱼眼效果的力导向图?
感谢您的宝贵时间!
【问题讨论】:
【参考方案1】:关键的挑战是,您有两个定位源同时工作来移动节点:一个鼠标移动函数设置位置以实现鱼眼效果,一个刻度函数设置位置以反映更新的力布局。由于 tick 函数是不断触发的,这很可能解释了您的评论,即鱼眼效果仅在力冷却时才起作用:不再调用 tick 函数并且两种定位方法之间没有冲突。
要删除相互竞争的定位方法,最好在力冷却期间使用刻度功能,并且在力冷却后,使用鼠标事件本身来定位:因为鼠标不会一直在移动模拟,然后滴答声肯定不会触发。
另一个挑战是,如果鼠标停止移动,尽管力布局发生了运动,但鱼眼效果不会更新:我们需要在每个滴答声中更新鱼眼效果,以反映哪些节点在节点漂移进出时受到影响重点地区。无论鼠标是否移动,都需要进行此更新。
如上所述,使用力来创建鱼眼并不是很好:光标会强制节点更改 x/y 属性,而不仅仅是扭曲它们的外观:鱼眼效果不应干扰力布局的力/位置数据。
鉴于这些限制,或许可以随着时间的推移将其清理为更优雅的快速解决方案是:
跟踪上次鼠标移动位置或鼠标是否已退出 SVG: let xy = false;
svg.on("mousemove", function() xy = d3.mouse(this); )
.on("mouseleave", function() xy = false; )
在力定位期间,数据基于力和最近已知的鼠标位置来实现鱼眼:
simulation.on("tick",position)
function position()
if(xy)
fisheye.focus(xy);
node.each(d=> d.fisheye = fisheye(d); )
else node.each(d=>d.fisheye=x:0,y:0,z:0)
link
.attr("x1", d => d.source.fisheye.x || d.source.x)
.attr("y1", d => d.source.fisheye.y || d.source.y)
.attr("x2", d => d.target.fisheye.x || d.target.x)
.attr("y2", d => d.target.fisheye.y || d.target.y);
node
.attr("cx", d => d.fisheye.x || d.x)
.attr("cy", d => d.fisheye.y || d.y);
然后当模拟结束时,使用鼠标移动事件计算静态节点上的鱼眼效果,因为滴答声不再触发:
simulation.on("end", function()
svg.on("mousemove.position", position);
)
d3.json("https://gist.githubusercontent.com/mbostock/4062045/raw/5916d145c8c048a6e3086915a6be464467391c62/miserables.json").then(draw);
function draw(data)
const fisheye = fisheyeO.circular()
.radius(100)
.distortion(5);
const height = 400;
const width = 500;
data
const simulation = d3.forceSimulation(data.nodes)
.alphaDecay(0.001)
.alphaMin(0.01)
.force("link", d3.forceLink(data.links).id(d => d.id))
.force("charge", d3.forceManyBody())
.force("x", d3.forceX(width/2))
.force("y", d3.forceY(height/2));
const svg = d3.select("body").append("svg")
.attr("viewBox", [0, 0, width, height])
const link = svg.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(data.links)
.join("line")
.attr("stroke-width", 2);
const node = svg.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll("circle")
.data(data.nodes)
.join("circle")
.attr("r", 5)
.attr("fill", "black")
let xy = false;
svg.on("mousemove", function() xy = d3.mouse(this); )
.on("mouseleave", function() xy = false; )
simulation.on("tick", position)
.on("end", function()
svg.on("mousemove.position", position);
)
function position()
if(xy)
fisheye.focus(xy);
node.each(d=> d.fisheye = fisheye(d); )
else node.each(d=>d.fisheye=x:0,y:0,z:0)
link
.attr("x1", d => d.source.fisheye.x || d.source.x)
.attr("y1", d => d.source.fisheye.y || d.source.y)
.attr("x2", d => d.target.fisheye.x || d.target.x)
.attr("y2", d => d.target.fisheye.y || d.target.y);
node
.attr("cx", d => d.fisheye.x || d.x)
.attr("cy", d => d.fisheye.y || d.y);
const fisheye0 = fisheyeO =
circular: () =>
var radius = 200,
distortion = 2,
k0,
k1,
focus = [0, 0];
function fisheye(d)
var dx = d.x - focus[0],
dy = d.y - focus[1],
dd = Math.sqrt(dx * dx + dy * dy);
if (!dd || dd >= radius) return x: 0, y: 0, z: dd >= radius ? 1 : 10;
var k = k0 * (1 - Math.exp(-dd * k1)) / dd * .75 + .25;
return x: focus[0] + dx * k, y: focus[1] + dy * k, z: Math.min(k, 10);
function rescale()
k0 = Math.exp(distortion);
k0 = k0 / (k0 - 1) * radius;
k1 = distortion / radius;
return fisheye;
fisheye.radius = function(_)
if (!arguments.length) return radius;
radius = +_;
return rescale();
;
fisheye.distortion = function(_)
if (!arguments.length) return distortion;
distortion = +_;
return rescale();
;
fisheye.focus = function(_)
if (!arguments.length) return focus;
focus = _;
return fisheye;
;
return rescale();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js"></script>
【讨论】:
以上是关于力导向图的鱼眼效应:在图稳定之前不生效的主要内容,如果未能解决你的问题,请参考以下文章
spring-cloud feign hystrix配置熔断为啥不生效的原因