使用插件扩展
插件是一个独立的 UI 元素、键绑定和生命周期挂钩捆绑包,可将自身安装到图表中 - 核心永远不需要知道它的存在。通过 plugins 选项以声明方式注册插件,或者随时使用 graph.use(plugin) 强制注册插件。
安装时,插件会获得一个 PluginContext 及其所需的一切:addElement() 将 UI 元素放入布局槽中(然后为您管理其生命周期),onPhase() 挂钩 afterMount / graphReady / destroy,addKeybinding() 用于自动清理的快捷方式,以及直接访问 graph, ui 和 layout。
下面的示例安装了一个单元素插件:固定到画布的 stats 覆盖,一旦图表准备好,它就会读取实时节点/边计数。它扩展了 UIComponent,因此它的 mount → afterMount → graphReady → destroy 生命周期(以及它跟踪的任何侦听器)由与内置侧边栏、工具栏和工具提示相同的机制驱动 - 添加新界面只是一个 addElement。
单击图表,然后按 B 切换叠加。
js
// import { UIComponent } from 'pivotick'
// A plugin bundles UI elements, keybindings and lifecycle hooks and installs
// itself through the PluginContext it is handed — the core never needs to know
// it exists.
//
// Our plugin contributes one custom UI element: a small stats overlay pinned to
// the canvas. Because it extends UIComponent, its lifecycle (mount → afterMount
// → graphReady → destroy) and any tracked listeners are driven for free — the
// same machinery the built-in sidebar/toolbar/tooltip use.
class StatsOverlay extends UIComponent {
// Build the DOM and drop it into the slot we were mounted into.
onMount(slot) {
this.el = document.createElement('div')
// Bottom-right corner: the top-left / top / left edges are taken by the
// B3 chrome (top bar, mode rail), so pin the overlay clear of them.
this.el.style.cssText = [
'position:absolute', 'bottom:12px', 'right:12px', 'z-index:5',
'padding:8px 12px', 'border-radius:8px', 'font:12px/1.6 sans-serif',
'background:rgba(20,24,32,0.85)', 'color:#fff',
'box-shadow:0 2px 8px rgba(0,0,0,0.25)', 'pointer-events:none'
].join(';')
slot?.appendChild(this.el)
}
// Runs once the graph data is ready — read live counts off the graph.
onGraphReady() {
this.render()
const graph = this.uiManager.graph
const cb = () => this.render()
graph.on('dataBatchChanged', cb)
// graph.on lives on the Graph, which outlives the UI — so track the
// unsubscribe and destroy() tears it down with everything else.
this.track(() => graph.off('dataBatchChanged', cb))
}
render() {
const graph = this.uiManager.graph
this.el.innerHTML =
'<strong>Graph stats</strong><br>' +
`${graph.getNodes().length} nodes · ${graph.getEdges().length} edges`
}
toggle() {
this.el.style.display = this.el.style.display === 'none' ? '' : 'none'
}
// Only DOM cleanup needed here; children and everything registered via
// track() (including the dataBatchChanged unsubscribe above) are torn down
// by UIComponent.
onDestroy() {
this.el?.remove()
}
}
const statsPlugin = {
name: 'stats-overlay',
install(ctx) {
const overlay = new StatsOverlay(ctx.ui)
// Mount the element into the canvas slot; its lifecycle is then managed.
ctx.addElement(overlay, ctx.layout?.canvas)
// Bonus: click the graph, then press "B" to toggle the overlay.
ctx.addKeybinding({ key: 'b', callback: () => overlay.toggle() })
}
}
// Register plugins declaratively with `plugins`, or imperatively at any time
// with `graph.use(statsPlugin)` (late installs are caught up to the current
// lifecycle phase automatically).
const options = {
// Off: `full` mode brings a minimap and the data dock, and this card is about
// neither (see the Minimap and Data table cards for those).
UI: { mode: 'full', minimap: false, table: false, sidebar: { collapsed: true } },
plugins: [statsPlugin]
}js
const data = {
nodes: [
{ id: 'web', data: { label: 'Web' } },
{ id: 'api', data: { label: 'API' } },
{ id: 'db', data: { label: 'Database' } },
{ id: 'cache', data: { label: 'Cache' } },
{ id: 'worker', data: { label: 'Worker' } }
],
edges: [
{ from: 'web', to: 'api', data: { label: 'calls' } },
{ from: 'api', to: 'db', data: { label: 'reads' } },
{ from: 'api', to: 'cache', data: { label: 'reads' } },
{ from: 'worker', to: 'db', data: { label: 'writes' } }
]
}