- 数据可视化
【免费下载链接】cytoscape.js
Graph theory (network) library for visualisation and analysis
导读
在 Cytoscape.js 中,布局(layout)是决定图节点坐标的核心机制。本文聚焦于布局对象的run()方法,讲解如何启动同步(discrete)与异步(continuous)两类布局、如何通过layoutstart/layoutstop事件掌控布局生命周期,并结合仓库源码剖析run()背后的执行链路,帮助你正确编写可复现、可维护的布局调用代码。
一、布局是什么:先厘清run()的作用对象
Cytoscape.js 将布局设计为可插拔的扩展机制,其唯一职责是"为图中的节点设置位置"。布局通过cy.layout(options)或eles.layout(options)创建,返回一个布局对象,而run()正是驱动该对象真正开始计算与落位的方法。
从布局介绍文档可以明确以下几点前提:
- 布局作用于你指定的子图:
cy.layout()使用图中全部元素,eles.layout()仅作用于所选元素集合;元素的可见性等状态不影响其是否参与布局。 - 每种布局有自己的位置算法,可通过 options 定制(例如力导向布局的边权重、间距因子、角度、重叠避免等)。
- 无头(headless)实例运行时通常需要显式指定
boundingBox,而渲染实例可由container的 DOM 尺寸推断范围。
在此基础上,run()的意义才得以展开——它是布局从"配置完成"走向"开始执行"的唯一入口。
二、layout.run()的核心语义:同步与异步的差异
根据run()方法文档:
如果布局是异步的(即连续型 continuous),调用
layout.run()只是"启动"布局;而同步(即离散型 discrete)布局会在run()返回之前就完成。无论何时启动布局,都会触发layoutstart事件。
这段话定义了run()最关键的语义边界:
- 同步 / 离散型布局:
layout.run()调用返回时,所有节点位置已经计算并设置完毕。典型代表是内置的random、grid、circle、concentric、breadthfirst等布局。 - 异步 / 连续型布局:
run()立即返回,布局在后台持续迭代计算(通常是力导向物理模拟),位置随每一帧刷新,直到收敛或被手动停止。典型代表是cose(以及常见的第三方fcose、cola等)。
以内置random布局为例,其 random.mjs 源码 中run()直接完成随机位置计算并通过layoutPositions()落位,整个过程在调用栈内同步完成:
RandomLayout.prototype.run = function(){ let options = this.options; let cy = options.cy; let eles = options.eles; let bb = math.makeBoundingBox( options.boundingBox ? options.boundingBox : { x1: 0, y1: 0, w: cy.width(), h: cy.height() } ); let getPos = function( node, i ){ return { x: bb.x1 + Math.round( Math.random() * bb.w ), y: bb.y1 + Math.round( Math.random() * bb.h ) }; }; eles.nodes().layoutPositions( this, options, getPos ); return this; // chaining };而cose布局的 cose.mjs 源码 则展示了异步路径:run()内部通过requestAnimationFrame驱动的frame()循环逐步执行物理模拟(step()),并在温度低于minTemp或达到numIter迭代上限时收敛。显然,这种情况下run()返回时布局仍在进行。
同步与异步的统一入口
有趣的是,库本身并不强制布局作者区分二者。extension.mjs 源码 显示,只要布局原型上定义了start()或run()其中之一,另一个会自动生成并互相委托:
// either .start() or .run() is defined, so autogen the other if( layoutProto.start && !layoutProto.run ){ layoutProto.run = function(){ this.start(); return this; }; } else if( !layoutProto.start && layoutProto.run ){ layoutProto.start = function(){ this.run(); return this; }; }因此无论内置布局还是第三方扩展,开发者都统一通过layout.run()启动。
三、生命周期事件:layoutstart与layoutstop
run()之所以重要,不仅因为它触发计算,更因为它串起了布局的完整生命周期事件。
layoutstart:布局启动
当布局启动时触发layoutstart。在集合级布局落位实现中,layoutPositions()的第一行便是:
layout.emit( { type: 'layoutstart', layout: layout } );cose等连续布局的run()在开始迭代前也会显式发出该事件(见 cose.mjs)。此外,内置的null布局同样遵循此约定(null.mjs),说明layoutstart是所有布局的通用契约。
layoutstop:布局结束
文档进一步说明:
布局在完成或被人为停止(例如调用
layout.stop())时,会触发layoutstop事件。开发者可以通过layout.on()监听该事件,或在布局选项中配置回调。
在同步路径下,layoutPositions()在设置完节点位置后按顺序发出layoutready与layoutstop(collection/layout.mjs):
layout.one( 'layoutready', options.ready ); layout.emit( { type: 'layoutready', layout: layout } ); layout.one( 'layoutstop', options.stop ); layout.emit( { type: 'layoutstop', layout: layout } );若开启了animate,则layoutstop会推迟到所有节点动画(以及可选的fit视图动画)的 Promise 全部 resolve 之后再触发(collection/layout.mjs):
Promise.all( layout.animations.map(function( ani ){ return ani.promise(); }) ).then(function(){ layout.one( 'layoutstop', options.stop ); layout.emit( { type: 'layoutstop', layout: layout } ); });而连续布局被提前终止时,stop()方法会直接发出layoutstop(cose.mjs):
CoseLayout.prototype.stop = function(){ this.stopped = true; if( this.thread ){ this.thread.stop(); } this.emit( 'layoutstop' ); return this; // chaining };注意extension.mjs中还有一个兜底逻辑:布局销毁时若未正常结束也会补发layoutstop(extension.mjs),确保监听方不会因异常终止而漏掉收尾事件。
四、实战:三种启动与监听方式
方式一:直接调用 + 事件监听
var layout = cy.layout({ name: 'random' }); layout.run();对于后续需要感知结束时机(例如结束后执行导图、截图或统计)的场景,可监听layoutstop:
var layout = cy.layout({ name: 'random' }); layout.on('layoutstop', function( event ){ console.log('layout 已结束'); }); layout.run();方式二:在布局选项中配置回调
文档指出layoutstop也可以通过布局 options 中的回调绑定。内置布局默认选项中均定义了stop: undefined // callback on layoutstop(可参见 random.mjs 默认选项、breadthfirst.mjs、grid.mjs 等),cose的选项注释同样标注// Called on layoutstop(cose.mjs):
var layout = cy.layout({ name: 'cose', // 布局结束时触发(无论自然收敛还是被 stop() 终止) stop: function(){ console.log('cose 布局停止'); } }); layout.run();由于底层通过layout.one('layoutstop', options.stop)绑定(见上文 collection/layout.mjs),该回调保证只触发一次,适合一次性收尾逻辑。
方式三:使用 Promise 链式等待(pon())
布局对象还暴露了pon()方法,可返回事件对应的 Promise,便于融入 async/await 流程:
var layout = cy.layout({ name: 'random' }); layout.pon('layoutstop').then(function( event ){ console.log('layoutstop promise fulfilled'); }); layout.run();// 配合 async/await 的典型写法 async function runAndWait(){ var layout = cy.layout({ name: 'cose' }); var p = layout.pon('layoutstop'); layout.run(); await p; console.log('布局完成,可安全导出或截图'); }中断连续布局:layout.stop()
对于异步连续布局,可以在任意时刻调用layout.stop()提前终止:
var layout = cy.layout({ name: 'cose' }); layout.run(); // 100ms 后强制停止 setTimeout(function(){ layout.stop(); }, 100);调用后布局会置stopped标志、停止内部线程并发出layoutstop,之前注册的stop回调与pon('layoutstop')同样会生效。
五、从源码看run()的完整调用链
将文档描述与源码对照,可以得到layout.run()的完整行为链路:
- 开发者调用
cy.layout(options)/eles.layout(options)创建布局对象;eles.layout()内部通过cy.makeLayout并把自身集合注入options.eles(collection/layout.mjs)。 - 调用
layout.run()。对同步布局(如random),run()内完成位置计算后调用eles.nodes().layoutPositions(layout, options, fn)。 layoutPositions()依次:- 发出
layoutstart(若启用动画则同时构建节点动画与 fit/zoom-pan 动画); - 应用
spacingFactor缩放与transform位置变换(collection/layout.mjs); - 同步或动画式地设置节点位置;
- 按需执行
cy.fit/cy.zoom/cy.pan; - 发出
layoutready,随后发出layoutstop(动画模式下等待动画 Promise 完成)。
- 发出
- 对异步布局(如
cose),run()返回后由requestAnimationFrame驱动的模拟循环继续执行,收敛后走同样的layoutstop流程;被stop()中断时由stop()直接发出layoutstop。
其中layoutPositions对spacingFactor、transform、animateFilter的处理(collection/layout.mjs)是理解"布局选项如何影响最终落位"的关键——例如通过transform(node, newPos)可以在离散布局中整体改变流向,通过animateFilter可以只对部分节点做动画过渡。
六、常见误用与注意事项
- 对异步布局假设"run() 后位置已就绪":连续布局调用
run()后立刻读取节点坐标可能还是初始值,务必在layoutstop/pon('layoutstop')/stop回调中再处理后续逻辑。 - 重复调用
run():同一布局对象再次run()会重新触发layoutstart并重新计算;如需全新配置,建议重新cy.layout()。 - 事件监听时机:
layoutstart在run()内部同步发出(同步布局),因此监听器必须在run()之前通过on()或 options 回调注册,否则可能错过事件。 - 无头模式下的
boundingBox:headless 实例没有 DOM 容器推断尺寸,需在 options 中显式传入boundingBox(如{ x1, y1, x2, y2 }或{ x1, y1, w, h }),否则部分布局会退化为默认范围(random布局默认以cy.width()/cy.height()为边界,见 random.mjs)。
结语
layout.run()是 Cytoscape.js 布局体系中"启动一切"的开关:对离散布局它同步交付结果,对连续布局它开启后台迭代,并通过layoutstart/layoutstop事件把开始与结束的时机完整暴露给开发者。理解同步与异步语义的差异,熟练运用on()、options 回调与pon()三种监听手段,再结合对layoutPositions与extension.mjs底层约定的认识,你就能在任意场景下稳妥地编排布局流程,避免时序竞争类 bug。
- 数据可视化
【免费下载链接】cytoscape.js
Graph theory (network) library for visualisation and analysis
相关推荐
Terminal.Gui View 深度解析:布局、绘制、输入与生命周期完整指南
Terminal.Gui View 深度解析:布局、绘制、输入与生命周期完整指南 本指南以 Terminal.Gui 中所有可见 UI 元素的基类 View 为
UI组件跨平台桌面应用Cytoscape.js布局算法大全:从网格布局到力导向布局的完整指南
Cytoscape.js布局算法大全:从网格布局到力导向布局的完整指南 Cytoscape.js是一个强大的JavaScript图形理论库,专门用于网络可视化和
数据可视化Apache APISIX Lua 插件开发完全指南:从目录布局、生命周期到发布与测试
Apache APISIX Lua 插件开发完全指南:从目录布局、生命周期到发布与测试 本篇技术指南以 Apache APISIX 官方插件开发文档为核心,系统
API网关后端云原生微服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考