Loading...
Loading...
Neo4j Visualization Library (NVL) — framework-agnostic graph rendering for the browser. Covers @neo4j-nvl/base (NVL class, nodes/relationships, Canvas vs WebGL renderer), @neo4j-nvl/interaction-handlers (ZoomInteraction, PanInteraction, DragNodeInteraction, ClickInteraction, HoverInteraction, BoxSelectInteraction, LassoInteraction, KeyboardInteraction), and @neo4j-nvl/react (InteractiveNvlWrapper, BasicNvlWrapper, StaticPictureWrapper). Use when rendering a Neo4j graph in a browser, feeding driver results through nvlResultTransformer, choosing Canvas vs WebGL, wiring node/relationship click/hover/drag handlers, or embedding NVL in React, Vite, or vanilla JS apps. Does NOT handle Cypher query authoring — use neo4j-cypher-skill. Does NOT handle driver lifecycle, sessions, or executeQuery setup — use neo4j-driver-javascript-skill. Does NOT handle GraphVisualization/Needle default embed — use @neo4j-ndl/react.
npx skill4agent add neo4j-contrib/neo4j-skills neo4j-nvl-skilldriver.executeQueryGraphVisualization@neo4j-ndl/reactneo4j/python-graph-visualizationneo4j-cypher-skillneo4j-driver-javascript-skillneo4j-driver-javascript-skillneo4j-gds-skillneo4j-aura-graph-analytics-skillneo4j-graphql-skill| Need | Use |
|---|---|
| Embed a graph view with default Neo4j styling, no custom interactions or rendering | |
| Custom interactions, custom rendering, non-standard data shapes, or framework-agnostic embedding | This skill — use NVL directly |
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@neo4j-nvl/layout-workersneo4j-driver@neo4j-nvl/basenvlResultTransformer| Need | Use |
|---|---|
| React app, default interactions | |
| React app, custom interaction wiring | |
| Vanilla JS, standard interactions | |
| Vanilla JS, fully custom event logic | |
| Static PNG/SVG image export | |
| Renderer | Max nodes | Detail | Use case |
|---|---|---|---|
| ~1,000 | Full captions, icons, arrows, pixel-perfect hit-testing | Detail investigation, small graphs |
| 100,000+ | Reduced label fidelity (bound by GPU max texture size) | Large-scale pattern exploration |
const nvl = new NVL(container, nodes, rels, { renderer: 'webgl' })
nvl.setRenderer('canvas') // swap at runtimewidthheight0<!-- ❌ height defaults to 0; graph invisible -->
<div id="viz"></div>
<!-- ✅ explicit dimensions -->
<div id="viz" style="width: 100%; height: 600px;"></div>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()NVLnew NVL(frame, nvlNodes?, nvlRels?, options?, callbacks?)frameNVL.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('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('onZoom', (level) => console.log('zoom', level))
// 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')truemouseEventCallbackstruefalseimport { InteractiveNvlWrapper } from '@neo4j-nvl/react'
import type { MouseEventCallbacks, NvlOptions } from '@neo4j-nvl/react'
import { useRef } from 'react'
import type { NVL } 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),
onCanvasClick: (evt) => console.log('canvas'),
onHover: (el, hits, evt) => el && console.log('hover', el.id),
onDragEnd: (nodes, evt) => persist(nodes),
onZoom: 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>
)
}refNVLnvlRef.current?.fit([])nvlRef.current?.setRenderer('webgl')nvlRef.current?.saveToFile()IncludeMethods<NVL>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-nvl/baseResultTransformerimport 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)neo4j-driver-javascript-skill| Method | Behavior |
|---|---|
| Insert new; update existing by id (only specified fields) |
| Update existing only; ignores unknown ids |
| Insert only; throws on existing id |
| Remove nodes; adjacent relationships auto-removed |
| Remove relationships |
| Override positions; optionally re-run layout |
| Restart with new options; positions optional |
PartialNodePartialRelationshipidnvl.updateElementsInGraph(
[{ id: '1', color: '#f00', selected: true }], // PartialNode
[{ id: '12', width: 4 }] // PartialRelationship
)getHits()const nvl = new NVL(container, nodes, rels)
container.addEventListener('click', (evt) => {
const { nvlTargets } = nvl.getHits(evt, ['node', 'relationship'], { hitNodeMarginWidth: 4 })
const hitNode = nvlTargets.nodes[0]
const hitRel = nvlTargets.relationships[0]
if (hitNode) console.log('hit node', hitNode.data.id)
else if (hitRel) console.log('hit rel', hitRel.data.id)
else console.log('hit canvas')
})HitTargetNodeHitTargetRelationshipdatapointerCoordinatesdistanceinsideNode| Mistake | Fix |
|---|---|
Container with no | Set explicit |
Pass | Use |
| WebGL for small label-rich graphs | Use |
| Canvas for 10k+ nodes | Switch to |
New | Use |
Forgetting | Call |
| Vanilla handlers not torn down | Call |
| Worker construction blocked (strict CSP / sandboxed runtime / older bundler) | |
| Telemetry enabled in regulated env | |
| Layout never settles | Pin anchor nodes with |
| Toggle once at mount; don't flip |
| Hit test misses near node edge | Pass |
| Captions missing on WebGL | GPU max texture size exceeded; fall back to Canvas or shrink captions |
NVLNodeRelationshipNvlOptionsLayoutOptionsExternalCallbacksHitTargetsNvlMouseEventStyledCaptionPoint<InteractiveNvlWrapper><BasicNvlWrapper><StaticPictureWrapper>MouseEventCallbacksKeyboardEventCallbacksnvlResultTransformerdisableWebWorkersonWebGLContextLostWebFetchwidthheightexecuteQuerynvlResultTransformerdatabaseexecuteQueryneo4j-driver-javascript-skill.destroy()nvl.destroy()nvl.destroy()disableTelemetry: truedisableWebWorkers: trueaddAndUpdateElementsInGraphupdateElementsInGraphrestart