力导向图的鱼眼效应:在图稳定之前不生效

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();
    
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js"&gt;&lt;/script&gt;

【讨论】:

以上是关于力导向图的鱼眼效应:在图稳定之前不生效的主要内容,如果未能解决你的问题,请参考以下文章

spring-cloud feign hystrix配置熔断为啥不生效的原因

Echarts 力导向图的连线,怎么配置不同长度

element禁用当前系统日期之前,为什么不生效

修改Host为啥不生效

thinkphp session 过期时间配置不生效是怎么回事?

学习springboot时候,自定义国际化不生效问题.