在 NebulaGraph Studio 中的D3力导向图布局优化(D3-Force Directed Graph)

主图:D3-Force Directed Graph Layout Optimization

What is D3.js

D3.js 是一个开源的 JavaScript 库,用于使用 SVG、HTML 和 CSS 在 Web 浏览器中生成动态的交互式数据可视化。

除了 D3 之外,还有其它流行且功能强大的库,例如 EChartsChart.js。但是,它们是高度封装的,因此留下的定制空间太小。

相反,D3 由于支持 SVG 元素的事件处理,因此是高度可定制的。它可以将任意数据绑定到文档对象模型 (DOM) 或直接在 DOM 上运行 W3C DOM API

对于那些想要完全控制图形布局的人来说,D3 绝对是一个首选。

D3-Force有向图

对于社交网络分析,D3-force 有向图是最佳选择。它是在 D3 中通过模拟粒子动力学的 Velocity Verlet 数值算法实现的模块。D3-force 中的每个结点都可以看作是一个电子,电子之间存在斥力(Coulomb repulsion)。同时,这些粒子通过它们之间的边连接,从而产生牵引力。

由于排斥和牵引,D3-force 中的粒子不断从随机无序的初始状态转变为平衡有序的布局。Velocity Verlet 数值算法控制粒子和边的顺序。使用 d3-force 生成的图形仅包含结点和边,并且只有一小部分图形例子可供参考,因为大多数图形都是自定义的。

以下是我发现的用 D3-force 实现的流行网络图。显然,对于某些用例来说,这太简单了。因此,如果您的图形比这更复杂,那么您必须创建自己的图形。
网络图

构建 D3-Force 有向图

在 NebulaGraph Studio 中,我们使用 D3-force 有向图来分析数据关系,因为结点和边直观地显示了数据连接,并且它允许通过图查询语言进行图探索。此外,可以通过将 DOM 上的操作同步到数据库来更新图形数据(这部分得另一篇文章来介绍)。

让我们构建一个简单的 D3-force 有向图来说明 D3.js 如何显示数据连接,并基于此示例分享一些布局优化思路。

this.force = d3
    .forceSimulation()
    // Assign coordinates to nodes
    .nodes(data.vertexes)
    // Connect edges
    .force('link', linkForce)
    // The instance center
    .force('center', d3.forceCenter(width / 2, height / 2))
    // Gravitation
    .force('charge', d3.forceManyBody().strength(-20))
    // Collide force to prevent nodes overlap
    .force('collide',d3.forceCollide().radius(60).iterations(2));

上面的代码生成一个图形,如下所示:
力导向图 in NebulaGraph Studio

图展示布局优化

上图仅显示起始结点的单跃点关系。两跳或三跳关系怎么样?答案是 D3.js enter() API。

D3.js 的 enter() API 独立处理新结点。当查询新结点并将其推送到结点数组中时,API 会根据 D3-force 实例分配的坐标呈现它们,而不会更改现有结点的信息(包括 x、y 坐标)。

从 API 的角度来看,这绝对是可以理解的。但是因为 d3.forceSimulation() 模块随机分配位置坐标,新添加的结点不能仅仅通过简单地推送到现有的 D3-force 实例来处理。

d3.forceSimulation().node() 分配的坐标是随机的,已呈现的结点的位置也是随机的。通过碰撞和链接参数的作用,与新结点相关的结点的在牵引力的影响下彼此靠近。 此外,在接近过程中,其它结点之间存在碰撞。当力导向图上有结点时,这些新添加的结点会使整个图在碰撞和牵引的作用下发生碰撞,直到每个结点都找到自己的位置。这意味着只有当碰撞和牵引力都满足要求时,运动才会停止。它看起来像大爆炸吗?
the Big Bang
上述过程中存在两个问题:

  1. 添加新结点将导致整个图形不断移动
  2. 稳定需要相对较长的时间

但是,这就是 enter() API 的设计方式。

一种解决方案是分别处理新结点和现有结点,这意味着每个结点渲染都需要遍历来确定它是新的还是现有的。不是一个高性能的解决方案,尤其是当数量很大时。

另一种常见的解决方案是减少碰撞并增加 D3-force 实例的牵引力。通过这种方式,结点可以更快地找到平衡状态,从而使整个图形稳定。这是一个更好的解决方案,但缺点是它使结点之间的连接长度差异极大,并且图形大小会很大。因此,对于拥有大量数据的情况,这不是理想的解决方案。

我们提出了一个新的解决方案。

这个想法是为了确保新结点围绕源结点。代替 D3.forceSimulation().node() 分配,将新结点的坐标设置为与源结点的坐标相同。D3-force 实例的结点冲突保证了新结点的外观不会被覆盖,最终会出现在源结点周围。

虽然这样的解决方案仍然影响小范围内的结点,但不会对整个图的趋势产生太大影响。关键代码如下:

# Set the new nodes coordinates as the source node center or the entire graph center
addVertexes.map(d => {
  d.x = _.meanBy(selectVertexes, 'x') || svg.style('width') / 2;
  d.y = _.meanBy(selectVertexes, 'y') || svg.style('height') / 2;
});

如果没有源结点,则新结点将显示在图形的中心。这将对现有结点造成较小的影响,因此值得考虑。

两个结点之间多边显示的布局优化

当两个结点之间有多个边时,会出现以下问题:

  1. 默认直线将相互覆盖。因此,在这种情况下,曲线是更好的选择。
  2. 如何定义曲线的曲率以确保没有覆盖?
  3. 当你有多个曲线时,如何确保平均半圆弧不在某个半圆上?

以下是我们如何解决上述问题:

  1. 首先计算任意两个结点之间的边,并将它们分组到一个 map 中,map中的 key 基于所涉及的两个结点的名称。
  2. 然后按方向将同一 map 中的边分为两组。有很多方法可以确定边的方向,这里我们采用比较结点的 source.name 和 target.name 的 ASCII 码的方法。为每个具有正方向的边设置一个 linknum(链接值)。同样,为每条方向为负的边设置 -linknum。我们设置的 linknum 值用于确定弧的曲率和弯曲方向。

请参阅以下代码以更好地理解:

  const linkGroup = {};
  // Set the edges between two nodes as the same key based on their name property. 
  // Then add the key to the linkGroup, making all edges a group
  edges.forEach((link: any) => {
    const key =
      link.source.name < link.target.name
        ? link.source.name + ':' + link.target.name
        : link.target.name + ':' + link.source.name;
    if (!linkGroup.hasOwnProperty(key)) {
      linkGroup[key] = [];
    }
    linkGroup[key].push(link);
  });
  // Traverse each group to call setLinkNumbers to allocate linknum
  edges.forEach((link: any) => {
    const key = setLinkName(link);
    link.size = linkGroup[key].length;
    const group = linkGroup[key];
    if (group[group.length - 1] === link) {
      setLinkNumbers(group);
    }
  });


// Divide the edges into linkA and linkB based on their directions.
// Then allocate two kinds of linknum to control the upper and lower elliptical arc.

export function setLinkNumbers(group) {
  const len = group.length;
  const linksA: any = [];
  const linksB: any = [];
  for (let i = 0; i < len; i++) {
    const link = group[i];
    if (link.source.name < link.target.name) {
      linksA.push(link);
    } else {
      linksB.push(link);
    }
  }
  let startLinkANumber = 1;
  linksA.forEach(linkA=> {
    linkA.linknum = startLinkANumber++;
  }
  let startLinkBNumber = -1;
  linksB.forEach(linkB=> {
    linkB.linknum = startLinkBNumber--;
  }
}

将 linknum 分配给每条边后,在监视边的 tick 事件函数中判断 linknum 的符号。我们只需要判断和设置路径的曲率和方向

下面是它的外观:
结点间存在多个边

结论

这就是我们优化 D3.js 力导向图的方式。如果您正在建立复杂的关系,则有许多问题需要关注和优化。

在本文中,我们分享了两种最常见的场景,即呈现新结点和在两个结点之间存在多个边的情况。我们将在未来分享更多的经验。敬请期待!

尝试使用 NebulaGraph Studio 体验 D3.js 可视化。在下面给我们留言,或者如果有任何问题,请前往我们的论坛

Hi,我是 NebulaGraph 的前端工程师 Nico。我对数据可视化感兴趣,并想分享我在这方面的经验。希望我的帖子对您有所帮助。如果您对此有任何想法,请告诉我。谢谢!

原文地址:https://www.nebula-graph.io/posts/d3-force-layout-optimization

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要使用Force-Directed Layout(导向布局)在echarts呈现表,你需要做以下步骤: 1. 引入必要的echarts库和组件。确保你已经正确引入了echarts库和相应的组件。例如,在HTML文件添加以下script标签: ```html <script src="https://cdn.jsdelivr.net/npm/echarts@5.1.2/dist/echarts.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/echarts-gl@1.1.1/dist/echarts-gl.min.js"></script> ``` 2. 创建一个具有合适配置的echarts实例。在JavaScript,使用`echarts.init`方法创建一个容器,并传入相关的配置项: ```javascript var myChart = echarts.init(document.getElementById('chart-container')); var option = { // 配置项... }; myChart.setOption(option); ``` 3. 配置表的系列类型为'graph',并设置相应的系列选项。在option对象,添加一个graph系列,并配置相关选项: ```javascript var option = { series: [{ type: 'graph', layout: 'force', // 设置布局为force // 其他系列选项... }], // 其他配置项... }; ``` 4. 配置导向布局的相关参数。在graph系列选项,你可以根据需求设置导向布局的参数,例如节点之间的斥、节点与边之间的吸引布局的迭代次数等等: ```javascript var option = { series: [{ type: 'graph', layout: 'force', force: { repulsion: 100, // 节点之间的斥 edgeLength: 150, // 边的默认长度 // 其他布局参数... }, // 其他系列选项... }], // 其他配置项... }; ``` 5. 设置节点和边的数据。根据你的数据,设置节点和边的相关信息,例如节点的名称、坐标、边的关系等等。 6. 渲染表。最后,使用`setOption`方法将配置应用到,并渲染出导向布局表: ```javascript myChart.setOption(option); ``` 以上是一个基本的使用Force-Directed Layout(导向布局)的示例。你可以根据你的需求和数据进行相应的配置和定制。 希望能对你有所帮助!如果还有其他问题,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值