关于 neo4j-nvl-skill
面向在浏览器应用中构建 Neo4j 图可视化的开发者,介绍 NVL 的原生 JavaScript 和 React 用法。它提供渲染、交互、布局及驱动结果转换指导,适用于自定义图形呈现与交互。
主要功能
- 01Canvas 和 WebGL 图渲染
- 02React 包装组件与原生 JS API
- 03缩放、拖拽和选择交互
- 04图布局、聚类与动态更新
- 05Neo4j 驱动结果转换
适用场景
- 01在 React 或浏览器应用中展示 Neo4j 图
- 02实现节点点击、拖动或框选操作
- 03探索大型图数据或更新可视化
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ neo4j-contrib/ neo4j-skills --skill neo4j-nvl-skill安装后在 Claude Code 中输入/neo4j-nvl-skill确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.mdreferences/api-surface.mdtroubleshooting.md详细介绍
适用场景
- 在浏览器中使用自定义交互、渲染方式或数据形状来渲染 Neo4j 图(原生 JS、React、Vite)
- 将
driver.executeQuery的结果可视化为交互式图 - 配置缩放、平移、拖动、点击、悬停、套索或框选交互
- 在现有应用中嵌入 NVL 并同步图状态
不适用场景
- 带预设样式的嵌入式图视图,使用默认行为且无需自定义交互 → 使用
@neo4j-ndl/react中的GraphVisualization(Neo4j Needle / NDL 设计系统)——它封装了 NVL 并应用默认的 Neo4j 样式。请参阅下文的使用 NVL 还是 Needle 组件?。 - Python / Jupyter notebook 图可视化 →
neo4j/python-graph-visualization(NVL 的 Python 移植版) - 编写/优化 Cypher →
neo4j-cypher-skill - 驱动程序配置/executeQuery/会话 →
neo4j-driver-javascript-skill - 仅在服务端获取数据,不进行渲染 →
neo4j-driver-javascript-skill - 执行 GDS 算法 →
neo4j-gds-skill或neo4j-aura-graph-analytics-skill - GraphQL API →
neo4j-graphql-skill
使用 NVL 还是 Needle 组件?
| 需求 | 使用 |
|---|---|
| 嵌入带默认 Neo4j 样式的图视图,无需自定义交互或渲染 | 使用 @neo4j-ndl/react 中的 GraphVisualization(Neo4j Needle / NDL 设计系统)——它封装了 NVL,并接受形如 { id, labels, properties: { key: { stringified, type } } }(NeoNode)的记录 |
| 自定义交互、自定义渲染、非标准数据形状或与框架无关的嵌入方式 | 使用本指南——直接使用 NVL |
如果符合第一行的需求,请安装并使用 Needle 组件,而不是 NVL——不要重复实现样式工作。
安装
npm install @neo4j-nvl/base # core (required)
npm install @neo4j-nvl/interaction-handlers # standard interactions (optional, vanilla JS)
npm install @neo4j-nvl/react # React wrappers (optional)
同级依赖要求:@neo4j-nvl/react 需要 React 19。已发布的 peerDependency 范围仍允许 React 18,但不建议混用主版本——请以 19 为目标版本。@neo4j-nvl/layout-workers 是传递依赖——切勿直接安装。仅在使用 nvlResultTransformer 时,neo4j-driver 才是 @neo4j-nvl/base 的同级依赖。
入门模板:https://github.com/neo4j-devtools/nvl-boilerplates — 各框架的官方脚手架;优先使用这些模板,而不是手动搭建。
许可证:NVL 基于 Neo4j Visualization Library License 发布——仅可与 Neo4j 产品搭配使用。不得用于其他图后端。
选择合适的范式
| 需求 | 使用 |
|---|---|
| React 应用,默认交互 | 使用 @neo4j-nvl/react 中的 <InteractiveNvlWrapper> |
| React 应用,自定义交互配置 | 使用 <BasicNvlWrapper> + 通过 ref 编写自己的处理器 |
| 原生 JS,标准交互 | 使用 NVL + @neo4j-nvl/interaction-handlers |
| 原生 JS,完全自定义事件逻辑 | 使用 NVL + container.addEventListener + nvl.getHits() |
| 导出静态 PNG/SVG 图像 | 使用 <StaticPictureWrapper> 或 nvl.saveToFile() / nvl.saveToSvg() |
选择合适的渲染器
| 渲染器 | 最大节点数 | 细节 | 使用场景 |
|---|---|---|---|
'canvas'(默认) | ~1,000 | 标题、图标、箭头、像素级精准命中测试 | 细节探索、小型图 |
'webgl' | 100,000+ | 无标题;2.0 支持节点图标和关系箭头 | 大规模模式探索 |
const nvl = new NVL(container, nodes, rels, { renderer: 'webgl' })
nvl.setRenderer('canvas') // swap at runtime
NVL 2.0 的 GPU 渲染器和力导向物理引擎需要 WebGL2。如果无法使用 GPU 加速,NVL 可以回退到 Canvas 渲染和非 GPU 布局实现;使用 disableWebGL: true 可显式禁用 WebGL。
容器设置
容器必须显式设置 width 和 height。缺少高度 → 容器会收缩为 0 → 图不可见。这是最常见的 NVL 问题。
<div id="viz"></div>
<div id="viz" style="width: 100%; height: 600px;"></div>
原生 JS — 基础库
import { NVL } from '@neo4j-nvl/base'
const container = document.getElementById('viz')
const nodes = [{ id: '1' }, { id: '2' }]
const relationships = [{ id: '12', from: '1', to: '2', type: 'KNOWS' }]
const nvl = new NVL(container, nodes, relationships)
带选项和回调:
import { NVL } from '@neo4j-nvl/base'
const options = {
initialZoom: 1.0,
minZoom: 0.1,
maxZoom: 8,
layout: 'forceDirected',
renderer: 'canvas',
styling: { defaultNodeColor: '#0e86d4', defaultRelationshipColor: '#888' }
}
const callbacks = {
onInitialization: () => console.log('NVL ready'),
onLayoutDone: () => nvl.fit([]),
onError: (err) => console.error('NVL error', err)
}
const nvl = new NVL(container, nodes, relationships, options, callbacks)
// On teardown — always:
nvl.destroy()
NVL 构造函数签名:new NVL(frame, nvlNodes?, nvlRels?, options?, callbacks?)。除 frame 外,所有参数均可选,默认值为空。
Vanilla — 交互处理器
将处理器组合到现有的 NVL 实例上。每个处理器都会通过 .updateCallback(name, fn) 注册回调,并且必须通过 .destroy() 拆除。
import { NVL } from '@neo4j-nvl/base'
import {
ZoomInteraction, PanInteraction, DragNodeInteraction,
ClickInteraction, HoverInteraction, BoxSelectInteraction,
LassoInteraction, KeyboardInteraction
} from '@neo4j-nvl/interaction-handlers'
const nvl = new NVL(container, nodes, relationships)
const zoom = new ZoomInteraction(nvl)
const pan = new PanInteraction(nvl)
const drag = new DragNodeInteraction(nvl)
const click = new ClickInteraction(nvl, { selectOnClick: true })
const hover = new HoverInteraction(nvl, { drawShadowOnHover: true })
click.updateCallback('onNodeClick', (node, hits, evt) => console.log('node', node.id))
click.updateCallback('onRelationshipClick', (rel, hits, evt) => console.log('rel', rel.id))
click.updateCallback('onClusterClick', (cluster, hits, evt) => console.log('cluster', cluster.id))
click.updateCallback('onCanvasClick', (evt) => console.log('canvas'))
hover.updateCallback('onHover', (el, hits, evt) => el && console.log('over', el.id))
drag.updateCallback('onDragEnd', (nodes, evt) => savePositions(nodes))
zoom.updateCallback('onZoomAndPan', (level, panX, panY) => console.log(level, panX, panY))
// Teardown — destroy all handlers, then the NVL instance
function teardown() {
for (const h of [zoom, pan, drag, click, hover]) h.destroy()
nvl.destroy()
}
停用事件但不移除处理器:click.removeCallback('onCanvasClick')。传入 true 而非函数即可启用事件并使用空操作(适用于默认选择行为)。
React — InteractiveNvlWrapper
预先接入所有交互处理器。使用 mouseEventCallbacks 切换事件(函数 = 启用并设置回调;true = 启用并使用空操作;false/省略 = 停用)。
import { InteractiveNvlWrapper } from '@neo4j-nvl/react'
import type { MouseEventCallbacks } from '@neo4j-nvl/react'
import { useRef } from 'react'
import type { NVL, NvlOptions } from '@neo4j-nvl/base'
export function GraphView({ nodes, rels }) {
const nvlRef = useRef<NVL>(null)
const nvlOptions: NvlOptions = { initialZoom: 1, renderer: 'canvas' }
const mouseEventCallbacks: MouseEventCallbacks = {
onNodeClick: (node, hits, evt) => console.log('node', node.id),
onRelationshipClick: (rel, hits, evt) => console.log('rel', rel.id),
onClusterClick: (cluster, hits, evt) => console.log('cluster', cluster.id),
onCanvasClick: (evt) => console.log('canvas'),
onHover: (el, hits, evt) => {
if (el) console.log('hover', el.id)
if (hits.clusters[0]) console.log('cluster hover', hits.clusters[0].data.id)
},
onDragEnd: (nodes, evt) => persist(nodes),
onZoomAndPan: true, // enable, no callback
onPan: true
}
return (
<div style={{ width: '100%', height: 600 }}>
<InteractiveNvlWrapper
ref={nvlRef}
nodes={nodes}
rels={rels}
nvlOptions={nvlOptions}
interactionOptions={{ selectOnClick: true, drawShadowOnHover: true }}
mouseEventCallbacks={mouseEventCallbacks}
onInitializationError={(err) => console.error('NVL init', err)}
/>
</div>
)
}
ref 指向底层的 NVL 实例,可调用其任意方法:nvlRef.current?.fit([])、nvlRef.current?.setRenderer('webgl')、nvlRef.current?.saveToFile()。
NVL 2.0 — 布局聚类
clusterBy 会将回调返回相同字符串或数字的节点分组。返回 undefined 表示节点相互独立。在 React 中使用 useCallback 保持回调引用稳定。
力导向聚类将所有聚类设置嵌套在 layoutOptions.clustering 下:
const clusterBy = (nodeId) => categoryByNodeId[nodeId]
const nvl = new NVL(container, nodes, relationships, {
layout: 'forceDirected',
layoutOptions: {
seedingMethod: 'circle',
clustering: {
clusterBy,
drawOutline: true,
drawHalo: true,
crossClusterRelationshipMultiplier: 0.2
}
},
minFps: 3,
maxFps: 30
})
crossClusterRelationshipMultiplier 是实验性参数,取值范围限制为 0..1;当其接近 1 时,高度连接的图可能变得不稳定。它只影响跨聚类弹簧拉力;聚类内的边仍保持完整强度。
分层聚类将 clusterBy 直接放在 layoutOptions 中:
const nvl = new NVL(container, nodes, relationships, {
layout: 'hierarchical',
layoutOptions: {
direction: 'down',
clusterBy
}
})
聚类装饰和聚类凸包交互适用于力导向的 layoutOptions.clustering。Cluster 不是图元素;其结构为 { id: string | number, nodeIds: string[] }。仅当点击聚类凸包内的空白区域时才会触发聚类回调;节点和关系的命中优先级更高。
从 NVL 1.x 迁移到 2.0
- 将 React 包装器的
layout和layoutOptions属性移入nvlOptions:<InteractiveNvlWrapper nvlOptions={{ layout, layoutOptions }} />。 - 将交互回调
onZoom替换为onZoomAndPan(zoom, panX, panY, event)。 - 移除
intelWorkaround和与 Cytoscape 相关的布局设置;这些内容已不存在。 - 将 WebGL 视为仅支持 WebGL2,并且不要期望 WebGL 渲染器显示标题。
getNodes()和onLayoutStep返回NodeWithClientPosition[]:x/y是布局坐标,clientPosition是相对于画布元素的位置。- 使用
styling.overlayLineColor;styling.minimapViewportBoxColor已弃用。
React — BasicNvlWrapper + Ref
未接入交互。ref 通过 IncludeMethods<NVL> 暴露所有 NVL 方法,可用于在 React 中构建自定义交互逻辑。
import { BasicNvlWrapper } from '@neo4j-nvl/react'
import type { NVL } from '@neo4j-nvl/base'
import { useRef } from 'react'
export function MiniGraph({ nodes, rels }) {
const nvlRef = useRef<NVL>(null)
return (
<div style={{ width: '100%', height: 400 }}>
<BasicNvlWrapper
ref={nvlRef}
nodes={nodes}
rels={rels}
nvlOptions={{ initialZoom: 2 }}
nvlCallbacks={{ onLayoutDone: () => nvlRef.current?.fit([]) }}
/>
<button onClick={() => nvlRef.current?.fit(['1', '2'])}>Zoom to 1,2</button>
</div>
)
}
接入 Neo4j Driver 查询结果
@neo4j-nvl/base 为 JS driver 导出 ResultTransformer,可对任意记录结构中的节点/关系去重。
import neo4j from 'neo4j-driver'
import { NVL, nvlResultTransformer } from '@neo4j-nvl/base'
const driver = neo4j.driver(process.env.NEO4J_URI,
neo4j.auth.basic(process.env.NEO4J_USERNAME, process.env.NEO4J_PASSWORD))
const { nodes, relationships } = await driver.executeQuery(
'MATCH (a)-[r]-(b) RETURN a, r, b LIMIT 25',
{},
{ database: 'neo4j', resultTransformer: nvlResultTransformer }
)
const nvl = new NVL(document.getElementById('viz'), nodes, relationships)
// ❌ raw EagerResult — records are not Node/Relationship objects
const result = await driver.executeQuery('MATCH (a)-[r]-(b) RETURN a, r, b')
new NVL(container, result.records, []) // breaks
// ✅ use the transformer
const { nodes, relationships } = await driver.executeQuery(
'MATCH (a)-[r]-(b) RETURN a, r, b',
{},
{ database: 'neo4j', resultTransformer: nvlResultTransformer }
)
new NVL(container, nodes, relationships)
有关 driver 生命周期、会话管理、Integer 处理和 TypeScript 类型 → neo4j-driver-javascript-skill。
更新图
| 方法 | 行为 |
|---|---|
addAndUpdateElementsInGraph(nodes, rels) | 插入新元素;按 id 更新现有元素(仅更新指定字段) |
updateElementsInGraph(nodes, rels) | 仅更新现有元素;忽略未知 id |
addElementsToGraph(nodes, rels) | 仅插入;遇到已有 id 时抛出异常 |
removeNodesWithIds(ids) | 移除节点;自动移除相邻关系 |
removeRelationshipsWithIds(ids) | 移除关系 |
setNodePositions(nodes, updateLayout?) | 覆盖位置;可选择重新运行布局 |
restart(options?, retainPositions?) | 使用新选项重新启动;可选择保留位置 |
差异更新使用 PartialNode / PartialRelationship,只要求提供 id:
nvl.updateElementsInGraph(
[{ id: '1', color: '#f00', selected: true }], // PartialNode
[{ id: '12', width: 4 }] // PartialRelationship
)
命中测试(手动)
在不使用 interaction-handlers 包时使用。在 2.0 中,getHits() 会解析节点、关系和力导向聚类凸包。其默认目标为 ['node', 'relationship', 'cluster']。
const nvl = new NVL(container, nodes, rels)
container.addEventListener('click', (evt) => {
const { nvlTargets } = nvl.getHits(evt, ['node', 'relationship', 'cluster'], { hitNodeMarginWidth: 4 })
const hitNode = nvlTargets.nodes[0]
const hitRel = nvlTargets.relationships[0]
const hitCluster = nvlTargets.clusters[0]
if (hitNode) console.log('hit node', hitNode.data.id)
else if (hitRel) console.log('hit rel', hitRel.data.id)
else if (hitCluster) console.log('hit cluster', hitCluster.data.id, hitCluster.data.nodeIds)
else console.log('hit canvas')
})
HitTargetNode / HitTargetRelationship 携带元素命中数据。HitTargetCluster 携带 data、pointerCoordinates 和拟合后的凸 hull。请参阅 references/api-surface.md。
常见错误
| 错误 | 修复方法 |
|---|---|
容器没有 height → 图表不可见 | 为容器设置明确的 width 和 height |
直接传入 driver.executeQuery 的结果 | 使用 nvlResultTransformer 并使用 { nodes, relationships } |
| 小型且标签丰富的图表使用 WebGL | 使用 'canvas';它完全支持标签 |
| 10k+ 节点的图表使用 Canvas | 通过 renderer 选项或 setRenderer 切换到 'webgl' |
每次 React 渲染都创建新的 NVL | 使用 <InteractiveNvlWrapper> / <BasicNvlWrapper>,或将其封装在 useEffect + destroy() 中 |
卸载时忘记调用 nvl.destroy() | 卸载时调用 destroy();React 封装组件会自动处理 |
| Vanilla 事件处理器未清理 | 在调用 nvl.destroy() 之前,对每个交互调用 .destroy() |
| 无法构建 Worker(严格的 CSP / 沙盒运行时 / 较旧的打包器) | nvlOptions: { disableWebWorkers: true }(NVL 有非 Worker 回退方案) |
| 在受监管的环境中启用了遥测 | nvlOptions: { disableTelemetry: true } |
| 布局始终无法稳定 | 使用 pinNode(id) 固定锚点节点;调整 layoutTimeLimit |
selectOnClick 触发两次 | 在挂载时切换一次;不要在每次渲染时切换 interactionOptions |
| 命中测试未检测到节点边缘附近的命中 | 向 getHits 传入 { hitNodeMarginWidth: N } |
| WebGL 中缺少标题 | 这是 2.0 中的预期行为;标题仅支持 Canvas,因此标签很重要时请使用 'canvas' |
React 封装组件忽略 layout / layoutOptions | 在 2.0 中,这些选项必须嵌套在 nvlOptions 中 |
onZoom 从未触发 | 它已在 2.0 中移除;请使用 onZoomAndPan |
| 聚类回调未检测到节点上的点击 | 聚类回调的目标是聚类凸包中的空白区域;节点命中优先 |
参考资料
按需加载:
- references/api-surface.md — 完整的 2.0
NVL方法表;聚类、FPS、播种、NodeWithClientPosition、命中目标和交互回调类型;React 属性;具名导出;nvlResultTransformer - references/troubleshooting.md — 容器高度为零、WebGL2 与 Canvas 的权衡、聚类配置、1.x 迁移错误、Worker 回退、上下文丢失、遥测、生命周期、布局和命中测试问题
权威 Web 文档(仅当上述参考资料不足时使用 WebFetch):
- https://neo4j.com/docs/nvl/current/ — 用户指南(安装、基础库、交互处理器、React 封装组件)
- https://neo4j.com/docs/api/nvl/current/ — TypeDoc API 参考
- https://neo4j.com/docs/api/nvl/current/examples.html — 可运行示例
- https://github.com/neo4j-devtools/nvl-boilerplates — 针对每种受支持框架的官方入门模板
- https://github.com/neo4j/python-graph-visualization — NVL 的 Python 移植版(仅在 JavaScript/浏览器路径中使用此技能)
检查清单
- 容器已显式设置
width和heightCSS - 根据决策表选择了正确的范式(vanilla / handlers / React)
- 渲染器与预期节点数量匹配(Canvas ≲1k / WebGL 100k+)
- 需要标题时始终选择 Canvas
- React 的
layout和layoutOptions嵌套在nvlOptions中 - 缩放处理程序使用
onZoomAndPan,而非已移除的onZoom - React 中的
clusterBy保持稳定,并针对所选布局放置在正确位置 - Driver 的
executeQuery结果通过nvlResultTransformer传递 - 每次调用
executeQuery时都指定database(委托给neo4j-driver-javascript-skill) - 在 teardown 时,先对所有交互处理程序调用
.destroy(),再调用nvl.destroy() - 在 React 卸载时调用
nvl.destroy()(仅适用于手动创建的实例——包装器会自行处理) - 在受监管 / 离线环境中设置
disableTelemetry: true - 在打包器 / CSP 阻止创建 worker 时设置
disableWebWorkers: true - 图更新使用
addAndUpdateElementsInGraph/updateElementsInGraph,而非restart - 许可证兼容:目标对象是 Neo4j 产品