D3.js 力导向图的显示优化

简介: 我们用 D3.js 力导向图来对图数据库的数据关系进行分析,其节点和关系线直观地体现出图数据库的数据关系,并且还可以关联相对应的图数据库语句完成拓展查询。此外,本文还讲解了如何优化新增节点和多边关系的显示

image

D3.js

作为一个前端,说到可视化除了听过 D3.js 的大名,常见的可视化库还有 EChartsChart.js,这两个库功能也很强大,但是有一个共同特点是封装层次高,留给开发者可设计和控制的部分太少。和 EChart、Chart.js 等相比,D3.js 的相对来说自由度会高很多,得益于 D3.js 中的 SVG 画图对事件处理器的支持,D3.js 可将任意数据绑定到文档对象模型(DOM)上,也可以直接操作对象模型(DOM)完成 W3C DOM API 相关操作,对于想要展示自己设计图形的开发者,D3.js 绝对是一个不错的选择。

d3-force 力导向图

以实现一个关系网来说,d3-force 力导向图是不二的选择。d3-force 是 D3.js 实现以模拟粒子物理运动的 velocity Verlet 数值积分器的模块,可用来控制粒子和边秩序。在力导向图中,d3-force 中的每个节点都可以看成是一个放电粒子,粒子间存在某种斥力(库仑斥力)。同时,这些粒子间被它们之间的“边”所牵连,从而产生牵引力。

而 d3-force 中的粒子在斥力和牵引力的作用下,从随机无序的初态不断发生位移,逐渐趋于平衡有序。整个图只有点 / 边,图形实现样例较少且自定义样式居多。

下图就是最简单的关系网图,想要实现自己想要的关系网图,还是动手自己实现一个 D3.js 力导向图最佳。

image

构建 D3.js 力导向图

在这里实践过程中,我们用 D3.js 力导向图来对图数据库的数据关系进行分析,其节点和关系线直观地体现出图数据库的数据关系,并且还可以关联相对应的图数据库语句完成拓展查询。进阶来说,可通过对文档对象模型(DOM)的直接操作同步到数据库进而更新数据,当然操作这个比较复杂,😂 不在本文中详细讲述。

下面,我们来实现一个简单的力导向图,初窥 D3.js 对数据分析的作用和显示优化的一些思路。首先我们创建一个力导向图:

this.force = d3
        .forceSimulation()
        // 为节点分配坐标
        .nodes(data.vertexes)
        // 连接线
        .force('link', linkForce)
        // 整个实例中心
        .force('center', d3.forceCenter(width / 2, height / 2))
        // 引力
        .force('charge', d3.forceManyBody().strength(-20))
        // 碰撞力 防止节点重叠
        .force('collide',d3.forceCollide().radius(60).iterations(2));

通过上述代码,我们可以得到下面这样一个可视化的节点和关系图。

image

实现拓展查询显示优化

看到关系图(上图),我们会发现有一个新需求:选中节点继续往下拓展查询。为了实现拓展查询,在这里笔者要介绍下 D3.js 自带 API。

D3.js 的 enter() API 可对新增的节点作单独的逻辑处理,所以当拓展查询到新的节点 push 进节点数组时,不会去改变之前存在的节点信息(包括 x,y 坐标),而是按照 d3-force 实例分配的坐标进行渲染。从 API 上理解来说确实是这样,但是新增的节点对于 d3-force 这个已经存在的实例来说是一个不是简单的 push 就能处理的。因为 d3.forceSimulation()  这个模型给当前节点分配的位置坐标(x,y)是随机,目前看来没什么问题对不对?

但由于d3.forceSimulation().node()  的坐标随机分配导致了图形拓展出来位置的随机出现,加上之前 d3-force 实例中我们设定好的 collide(碰撞力)和 links (引力)参数,所以和新节点相关联的节点受到牵引力影响互相靠近。在靠近的过程中又会和其他节点发送碰撞力的作用,当力导图存在的节点的情况下,这些新增节点出现时会让整个力导向图在 collide 和 links 的作用下不停地碰撞,进行牵引,直到每个节点都找到自己合适的位置,即碰撞力和牵引力都满足要求时才停止移动,看看下图,像不像宇宙大爆炸 🌞。

image

上述无序到有序熵减的过程,站在用户角度,每新增一个节点导致整个力导图都在一直在动,除了有一种抽搐的感,停止图形变化又需要长时间的等待,这是不能接受的。可 D3.js API enter() 又是这样定义规定好的,难道新增的节点和之前的节点的呈现处理需要开发者分开单独处理吗?如果是分开单独处理,每次节点渲染都要遍历判断是不是新增,在节点较多时反而更影响性能?那么如何优化这个新增节点呈现的问题呢?

网上解决新增节点呈现问题,大多采用减小 d3-force 实例 collide,增大 links 的 distance 参数值,这样会让节点很快地找到平衡点从而使整个力导图稳定下来,这确实是一个好办法。但是这样节点之间的连线长度相差明显,而且图形整体偏大,对于大数据量的 case 来说,这种显示方式并不太适合。

基于上述的方法,笔者有了另一种解决思路——保证新增节点是在该选中拓展的节点周围,也就是说直接把新增节点的坐标设置为对应选择拓展节点一样的 x,y 坐标而不用 D3 .forceSimulation().node() 分配,这样利用 d3-force 这个实例的节点碰撞力,保证新增节点的出现都不会覆盖,最终会在选中拓展节点周围出现。 这样处理虽然还是对新增节点小的范围内的节点有影响,但相对来说,不会大幅度地影响整个关系图形走势。关键代码如下:

# 给新增的坐标设置为拓展起点中心或整个图中心
addVertexes.map(d => {
  d.x = _.meanBy(selectVertexes, 'x') || svg.style('width') / 2;
  d.y = _.meanBy(selectVertexes, 'y') || svg.style('heigth') / 2;
});

如果没有选中节点(既添加起点)则该起点坐标位置就在图中心位置,对已存在的节点来说,影响程度会小很多,这还是一个很不错的思路,这个解决办法可以推荐一下。

除了新增节点的呈现问题,整个图形的呈现还有另外一个问题:两点之间多边优化显示处理。

两点之间多边优化显示处理

当两个节点之间存在多条边关系时,默认连接线是直线的情况下肯定会出现多线覆盖。因此曲线连接便成了我们的另外需要解决的问题。
曲线如何定义弯曲度保证两点之间的多条线不会交互覆盖呢?在多条线弯曲下,如何平均半圆弧弯曲避免全跑到某半圆弧上?定义曲线弧方向?

上述问题都是下一步需要解决的问题,其实问题的解决方法也不少。目前笔者采用了先统计下两点之间的线条数,再将这些连接线分配到一个 map 里,两个节点的 name 字段进行拼接做成 key,这样计算得到两点之间的连接线总数。

然后在遍历时同 map 的线根据方向分成正向、反向两组,正向组遍历给每条线追加设置一个 linknum 编号,同理,反向组遍历追加一个 -linknum 编号值。这个正向、反向判断方法很多,笔者是根据节点 source.name、target.name 进行比较,btw,这里其实是比较 ASCII 码。定义连接线的正反方向办法太多了,用两点之间的任意固定字段比较即可,在这里不做赘述。而我们设定的 linknum 值就是来确定该条弧线的弯曲度和弯曲方向的,这里搭配下面代码讲解比较好理解:

  const linkGroup = {};
  // 两点之间的线根据两点的 name 属性设置为同一个 key,加入到 linkGroup 中,给两点之间的所有边分成一个组
  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);
  });
  // 遍历给每组去调用 setLinkNumbers 来分配 linkum
  edges.forEach((link: any) => {
    const key = setLinkName(link);
    link.size = linkGroup[key].length;
    const group = linkGroup[key];
    const keyPair = key.split(':');
    let type = 'noself';
    if (keyPair[0] === keyPair[1]) {
      type = 'self';
    }
    setLinkNumbers(group, type);
  });
#根据不同方向分为 linkA,linkB 两个数组,分别分配两种 linknum,从而控制上下椭圆弧
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 正负数判断设置 path 路径的弯曲度和方向 就行了,最终效果如下图

image

结语

好了,以上便是笔者使用 D3.js 力导向图实现关系网的优化思路和方法。其实要构建一个复杂的关系网,需要考虑的问题很多,需要优化的地方也很多,今天给大家分享两个最容易遇到的新节点呈现、多边处理问题,后续我们会继续产出 D3.js 优化系列文,欢迎订阅 Nebula Graph 博客

最后,你可以通过访问图数据库 Nebula Graph Studio:Nebula-Graph-Studio,体验下 D3.js 是如何呈现关系的。我们用 D3.js 力导向图来对图数据库的数据关系进行分析,其节点和关系线直观地体现出图数据库的数据关系,并且还可以关联相对应的图数据库语句完成拓展查询。此外,本文还讲解了如何优化新增节点和多边关系的显示2github) issue 区向我们提 issue 或者前往官方论坛:https://discuss.nebula-graph.com.cn/建议反馈 分类下提建议 👏;加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot

作者有话说:Hi,我是 Nico,是 Nebula Graph 的前端工程师,对数据可视化比较感兴趣,分享一些自己的实践心得,希望这次分享能给大家带来帮助,如有不当之处,欢迎帮忙纠正,谢谢~

关注公众号

相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
目录
相关文章
|
6天前
|
缓存 JavaScript 前端开发
介绍一下 JavaScript 中数组方法的常见优化技巧
通过合理运用这些优化技巧,可以提高 JavaScript 中数组方法的执行效率,提升代码的整体性能。在实际开发中,需要根据具体的业务场景和数据特点选择合适的优化方法。
17 6
|
4天前
|
缓存 前端开发 JavaScript
优化CSS和JavaScript加载
Next.js和Nuxt.js在优化CSS和JavaScript加载方面提供了多种策略和工具。Next.js通过代码拆分、图片优化和特定的CSS/JavaScript优化措施提升性能;Nuxt.js则通过代码分割、懒加载、预渲染静态页面、Webpack配置和服务端缓存来实现优化。两者均能有效提高应用性能。
|
12天前
|
JSON 监控 JavaScript
Node.js-API 限流与日志优化
Node.js-API 限流与日志优化
|
21天前
|
存储 JavaScript 前端开发
JavaScript垃圾回收机制与优化
【10月更文挑战第21】JavaScript垃圾回收机制与优化
25 5
|
2月前
|
缓存 JavaScript 中间件
优化Express.js应用程序性能:缓存策略、请求压缩和路由匹配
在开发Express.js应用时,采用合理的缓存策略、请求压缩及优化路由匹配可大幅提升性能。本文介绍如何利用`express.static`实现缓存、`compression`中间件压缩响应数据,并通过精确匹配、模块化路由及参数化路由提高路由处理效率,从而打造高效应用。
151 10
|
2月前
|
存储 缓存 JavaScript
JavaScript 中数组方法的常见优化技巧
JavaScript 中数组方法的常见优化技巧
|
3月前
|
机器学习/深度学习 存储 前端开发
实战揭秘:如何借助TensorFlow.js的强大力量,轻松将高效能的机器学习模型无缝集成到Web浏览器中,从而打造智能化的前端应用并优化用户体验
【8月更文挑战第31天】将机器学习模型集成到Web应用中,可让用户在浏览器内体验智能化功能。TensorFlow.js作为在客户端浏览器中运行的库,提供了强大支持。本文通过问答形式详细介绍如何使用TensorFlow.js将机器学习模型带入Web浏览器,并通过具体示例代码展示最佳实践。首先,需在HTML文件中引入TensorFlow.js库;接着,可通过加载预训练模型如MobileNet实现图像分类;然后,编写代码处理图像识别并显示结果;此外,还介绍了如何训练自定义模型及优化模型性能的方法,包括模型量化、剪枝和压缩等。
51 1
|
3月前
|
C# 开发者 测试技术
震惊!Xamarin 竟能如此构建跨平台应用程序,代码共享、界面设计与性能优化全攻略大揭秘!
【8月更文挑战第31天】在移动应用开发领域,跨平台工具日益受到青睐。Xamarin 是一款强大的工具,支持使用 C# 开发适用于 iOS、Android 和 Windows 的应用。通过安装 Visual Studio 或 Visual Studio for Mac,并创建 Xamarin 项目,开发者可以利用丰富的功能和工具进行开发。Xamarin 的主要优势在于代码共享,能够显著提高开发效率。
69 0
|
3月前
|
API UED 开发者
如何在Uno Platform中轻松实现流畅动画效果——从基础到优化,全方位打造用户友好的动态交互体验!
【8月更文挑战第31天】在开发跨平台应用时,确保用户界面流畅且具吸引力至关重要。Uno Platform 作为多端统一的开发框架,不仅支持跨系统应用开发,还能通过优化实现流畅动画,增强用户体验。本文探讨了Uno Platform中实现流畅动画的多个方面,包括动画基础、性能优化、实践技巧及问题排查,帮助开发者掌握具体优化策略,提升应用质量与用户满意度。通过合理利用故事板、减少布局复杂性、使用硬件加速等技术,结合异步方法与预设缓存技巧,开发者能够创建美观且流畅的动画效果。
79 0
|
3月前
|
JavaScript 前端开发 应用服务中间件
Vue.js项目部署与优化:一场从本地到生产环境的华丽蜕变,见证你的应用如何凤凰涅槃,惊艳上线!
【8月更文挑战第30天】作为一名前端开发者,掌握从本地开发环境到生产环境的迁移至关重要。本文将带你了解如何使用 Vue.js 构建和打包应用,确保其在生产环境中流畅运行。首先,通过 `npm run build` 或 `yarn build` 命令生成生产环境文件;接着,配置服务器(如 Nginx)以支持静态文件服务;最后,通过代码分割、资源压缩、CDN 使用、服务端渲染及缓存策略等手段优化应用性能。跟随本文,你将学会如何让 Vue.js 应用在真实环境中表现优异,为用户提供流畅体验。
52 0