@neo4j-nvl/base 2.0.0-8a6da460 → 2.0.0-c8e499dc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/base.mjs +35832 -39698
- package/dist/types/index.d.ts +4 -6
- package/dist/types/modules/state/types.d.ts +16 -5
- package/dist/types/renderers/domrenderer/canvasrenderer/CanvasRenderer.d.ts +1 -1
- package/package.json +6 -6
- package/skills/neo4j-nvl-skill/README.md +39 -0
- package/skills/neo4j-nvl-skill/SKILL.md +456 -0
- package/skills/neo4j-nvl-skill/references/api-surface.md +596 -0
- package/skills/neo4j-nvl-skill/references/troubleshooting.md +317 -0
- /package/dist/types/utils/{hittest.d.ts → hitTest.d.ts} +0 -0
- /package/dist/types/utils/{hittest.test.d.ts → hitTest.test.d.ts} +0 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# NVL — Troubleshooting & Gotchas
|
|
2
|
+
|
|
3
|
+
## Container renders nothing / 0×0
|
|
4
|
+
|
|
5
|
+
**Cause:** Parent `<div>` has no explicit height. NVL inherits `0` → graph invisible.
|
|
6
|
+
|
|
7
|
+
```html
|
|
8
|
+
<!-- ❌ -->
|
|
9
|
+
<div id="viz"></div>
|
|
10
|
+
|
|
11
|
+
<!-- ✅ -->
|
|
12
|
+
<div id="viz" style="width: 100%; height: 600px;"></div>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
React: wrap `<InteractiveNvlWrapper>` or `<BasicNvlWrapper>` in a sized parent OR pass `style={{ width, height }}` directly (the wrappers forward `HTMLProps<HTMLDivElement>`).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Driver `EagerResult` rejected by NVL
|
|
20
|
+
|
|
21
|
+
**Cause:** Default `executeQuery` returns `{ records, summary, keys }` — records are not `Node`/`Relationship` shapes.
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
// ❌
|
|
25
|
+
const res = await driver.executeQuery('MATCH (a)-[r]-(b) RETURN a,r,b LIMIT 25')
|
|
26
|
+
new NVL(container, res.records, []) // ids missing, shape wrong
|
|
27
|
+
|
|
28
|
+
// ✅
|
|
29
|
+
import { nvlResultTransformer } from '@neo4j-nvl/base'
|
|
30
|
+
const { nodes, relationships } = await driver.executeQuery(
|
|
31
|
+
'MATCH (a)-[r]-(b) RETURN a,r,b LIMIT 25', {},
|
|
32
|
+
{ database: 'neo4j', resultTransformer: nvlResultTransformer }
|
|
33
|
+
)
|
|
34
|
+
new NVL(container, nodes, relationships)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
For driver setup → `neo4j-driver-javascript-skill`.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Web Worker construction blocked
|
|
42
|
+
|
|
43
|
+
**Cause:** NVL's `@neo4j-nvl/layout-workers` spawns Web Workers for the force-directed and hierarchical layouts. Some environments block worker construction — strict CSP (`worker-src 'none'`), sandboxed iframes, custom runtimes, or older build tools that don't bundle workers correctly.
|
|
44
|
+
|
|
45
|
+
**Fix:** Disable workers — NVL has a synchronous fallback (slower for large graphs, identical output). Applies to any build tool (Vite, Webpack, Rollup, esbuild, Parcel, etc.):
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
const nvl = new NVL(container, nodes, rels, { disableWebWorkers: true })
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
React:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
<InteractiveNvlWrapper nvlOptions={{ disableWebWorkers: true }} ... />
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Most modern bundlers handle worker construction natively — try the default setup first, fall back to `disableWebWorkers: true` only when worker errors actually appear.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Renderer trade-off (Canvas vs WebGL)
|
|
62
|
+
|
|
63
|
+
| Renderer | Practical ceiling | Captions | Hit-test |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| Canvas (default) | ~1,000 nodes | Full styling | Pixel-perfect |
|
|
66
|
+
| WebGL | 100,000+ nodes | Not supported | Approximate |
|
|
67
|
+
|
|
68
|
+
NVL 2.0's GPU renderer and physics engine require **WebGL2**. WebGL1 is not supported. WebGL rendering now supports node icons and relationship arrowheads, but not captions.
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
// At construction
|
|
72
|
+
new NVL(container, nodes, rels, { renderer: 'webgl' })
|
|
73
|
+
|
|
74
|
+
// At runtime
|
|
75
|
+
nvl.setRenderer('webgl') // or 'canvas'
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Pick based on node count and whether captions are required. Switching at runtime triggers a re-render. Set `disableWebGL: true` to explicitly use non-WebGL rendering and layout implementations.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## WebGL captions/labels are missing
|
|
83
|
+
|
|
84
|
+
**Cause:** Expected NVL 2.0 behavior. The WebGL renderer does not support captions.
|
|
85
|
+
|
|
86
|
+
**Fix:** Use Canvas when captions are required:
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
nvl.setRenderer('canvas')
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## React `layout` or `layoutOptions` has no effect
|
|
95
|
+
|
|
96
|
+
**Cause:** NVL 2.0 removed the deprecated standalone React wrapper props.
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
// ❌ removed in 2.0
|
|
100
|
+
<InteractiveNvlWrapper layout="hierarchical" layoutOptions={{ direction: 'down' }} />
|
|
101
|
+
|
|
102
|
+
// ✅ put both options in nvlOptions
|
|
103
|
+
<InteractiveNvlWrapper
|
|
104
|
+
nvlOptions={{ layout: 'hierarchical', layoutOptions: { direction: 'down' } }}
|
|
105
|
+
/>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## `onZoom` callback no longer fires
|
|
111
|
+
|
|
112
|
+
**Cause:** NVL 2.0 removed `onZoom`.
|
|
113
|
+
|
|
114
|
+
**Fix:** Use `onZoomAndPan`, which includes the complete viewport state:
|
|
115
|
+
|
|
116
|
+
```javascript
|
|
117
|
+
zoom.updateCallback('onZoomAndPan', (zoomLevel, panX, panY, event) => {
|
|
118
|
+
console.log({ zoomLevel, panX, panY })
|
|
119
|
+
})
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
For React, set `mouseEventCallbacks={{ onZoomAndPan: callback }}`.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Force-directed clusters do not appear
|
|
127
|
+
|
|
128
|
+
**Cause:** Force-directed clustering options must be nested under `layoutOptions.clustering`; hierarchical `clusterBy` is placed directly under `layoutOptions`.
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
// Force-directed
|
|
132
|
+
const options = {
|
|
133
|
+
layout: 'forceDirected',
|
|
134
|
+
layoutOptions: {
|
|
135
|
+
clustering: {
|
|
136
|
+
clusterBy: (nodeId) => categoryByNodeId[nodeId],
|
|
137
|
+
drawOutline: true,
|
|
138
|
+
drawHalo: true
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Hierarchical
|
|
144
|
+
const hierarchicalOptions = {
|
|
145
|
+
layout: 'hierarchical',
|
|
146
|
+
layoutOptions: {
|
|
147
|
+
direction: 'down',
|
|
148
|
+
clusterBy: (nodeId) => categoryByNodeId[nodeId]
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The callback must return the same string or number for nodes in one cluster. Return `undefined` for independent nodes. In React, keep the callback stable with `useCallback`; otherwise changing its identity can cause repeated cluster evaluation.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Cluster click callback misses clicks
|
|
158
|
+
|
|
159
|
+
**Cause:** `onClusterClick`, `onClusterDoubleClick`, and `onClusterRightClick` apply to empty space inside a force-directed cluster hull. A node or relationship under the pointer takes priority.
|
|
160
|
+
|
|
161
|
+
**Fix:** Use node/relationship callbacks for direct element hits and cluster callbacks for hull space. For hover, inspect `hitElements.clusters` from `onHover`:
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
hover.updateCallback('onHover', (_element, hitElements) => {
|
|
165
|
+
const cluster = hitElements.clusters[0]?.data
|
|
166
|
+
if (cluster) console.log(cluster.id, cluster.nodeIds)
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## `onWebGLContextLost` fires
|
|
173
|
+
|
|
174
|
+
**Cause:** GPU dropped the context (tab backgrounded too long, driver crash, OS swap).
|
|
175
|
+
|
|
176
|
+
**Fix:** Implement `onWebGLContextLost` callback — call `nvl.setRenderer('canvas')` as recovery, or `nvl.restart(options, true)` to reinitialize while keeping positions:
|
|
177
|
+
|
|
178
|
+
```javascript
|
|
179
|
+
const nvl = new NVL(container, nodes, rels,
|
|
180
|
+
{ renderer: 'webgl' },
|
|
181
|
+
{ onWebGLContextLost: () => nvl.restart({ renderer: 'canvas' }, true) }
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Telemetry — opting out
|
|
188
|
+
|
|
189
|
+
**Cause:** NVL ships Segment Analytics; runs by default.
|
|
190
|
+
|
|
191
|
+
**Fix:**
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
new NVL(container, nodes, rels, { disableTelemetry: true })
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Mandatory in regulated / air-gapped / customer-data-sensitive deployments.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Memory leak after route change (React)
|
|
202
|
+
|
|
203
|
+
**Cause:** Custom `useRef` + manual `new NVL(...)` not torn down on unmount.
|
|
204
|
+
|
|
205
|
+
**Fix:** Always call `nvl.destroy()` in cleanup:
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
const nvl = new NVL(ref.current!, nodes, rels)
|
|
210
|
+
return () => nvl.destroy()
|
|
211
|
+
}, [])
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`<InteractiveNvlWrapper>` / `<BasicNvlWrapper>` handle this automatically — prefer them over manual refs.
|
|
215
|
+
|
|
216
|
+
For vanilla, destroy all interaction handlers BEFORE the NVL instance:
|
|
217
|
+
|
|
218
|
+
```javascript
|
|
219
|
+
for (const h of handlers) h.destroy()
|
|
220
|
+
nvl.destroy()
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Layout never settles / `isLayoutMoving` stays true
|
|
226
|
+
|
|
227
|
+
**Cause:** Graph has competing forces or is unsolvable; or workers disabled and synchronous fallback is throttled.
|
|
228
|
+
|
|
229
|
+
**Fix options:**
|
|
230
|
+
- Pin anchor nodes: `nvl.pinNode(id)` for known fixed positions
|
|
231
|
+
- Cap iterations: `nvlOptions: { layoutTimeLimit: 3000 }` (ms)
|
|
232
|
+
- Switch layout: `nvl.setLayout('hierarchical')` for DAG-like data
|
|
233
|
+
- Reduce nodes (paginate query, increase `LIMIT`)
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Selection callback fires twice
|
|
238
|
+
|
|
239
|
+
**Cause:** `selectOnClick` toggled mid-render OR both base handler + wrapper handling clicks.
|
|
240
|
+
|
|
241
|
+
**Fix:** Set `interactionOptions={{ selectOnClick: true }}` once at mount and leave it; don't flip the value across renders. If using vanilla `ClickInteraction`, do not also register a manual `container.addEventListener('click', ...)`.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Hit test misses near node edge
|
|
246
|
+
|
|
247
|
+
**Cause:** Default `hitNodeMarginWidth: 0` requires pointer strictly inside the node circle.
|
|
248
|
+
|
|
249
|
+
**Fix:** Widen the hit margin:
|
|
250
|
+
|
|
251
|
+
```javascript
|
|
252
|
+
const { nvlTargets } = nvl.getHits(evt, ['node', 'relationship'], { hitNodeMarginWidth: 8 })
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
For pan-on-relationship sensitivity, set `excludeNodeMargin: true` in `PanInteraction` options.
|
|
256
|
+
|
|
257
|
+
To include cluster hulls when supplying an explicit target list, add `'cluster'`. Cluster hits are included by default when the target list is omitted:
|
|
258
|
+
|
|
259
|
+
```javascript
|
|
260
|
+
const { nvlTargets } = nvl.getHits(evt, ['node', 'relationship', 'cluster'])
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Layout / hierarchy looks wrong
|
|
266
|
+
|
|
267
|
+
**Cause:** Default `'forceDirected'` not suited for tree / pipeline / circular data.
|
|
268
|
+
|
|
269
|
+
**Fix:** Pick the right layout up front:
|
|
270
|
+
|
|
271
|
+
| Data shape | Layout |
|
|
272
|
+
|---|---|
|
|
273
|
+
| Generic network | `'forceDirected'` |
|
|
274
|
+
| Tree / DAG | `'hierarchical'` (set `direction` in `layoutOptions`) |
|
|
275
|
+
| Sorted ring | `'circular'` (provide `sortFunction`) |
|
|
276
|
+
| Manual positioning | `'free'` |
|
|
277
|
+
| Snapped grid | `'grid'` |
|
|
278
|
+
| Force-directed via d3-force | `'d3Force'` |
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## License pitfall
|
|
283
|
+
|
|
284
|
+
**Cause:** NVL is licensed under the Neo4j Visualization Library License — for use with Neo4j products only.
|
|
285
|
+
|
|
286
|
+
**Implication:** Cannot ship NVL inside an application that uses a non-Neo4j graph backend (Neptune, JanusGraph, ArangoDB, etc.) as its primary data store. Refer to the LICENSE.txt in `@neo4j-nvl/base` and confirm with legal before redistribution.
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Image export returns blank PNG
|
|
291
|
+
|
|
292
|
+
**Cause:** Called `saveToFile()` before `onLayoutDone` fires — initial positions still settling.
|
|
293
|
+
|
|
294
|
+
**Fix:** Trigger export from the callback:
|
|
295
|
+
|
|
296
|
+
```javascript
|
|
297
|
+
const nvl = new NVL(container, nodes, rels, {},
|
|
298
|
+
{ onLayoutDone: () => nvl.saveToFile({ filename: 'graph.png' }) })
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Or `getImageDataUrl()` for in-memory data URL.
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## `restart()` vs `addAndUpdateElementsInGraph()`
|
|
306
|
+
|
|
307
|
+
**`restart()`** rebuilds internal state; expensive; resets selection unless `retainPositions=true` is passed AND positions are preserved. Use for option changes (renderer, layout, styling).
|
|
308
|
+
|
|
309
|
+
**`addAndUpdateElementsInGraph()`** is the correct API for streaming data changes. Use for live updates.
|
|
310
|
+
|
|
311
|
+
```javascript
|
|
312
|
+
// ❌ data update via restart — full rebuild every tick
|
|
313
|
+
nvl.restart()
|
|
314
|
+
|
|
315
|
+
// ✅ diff update
|
|
316
|
+
nvl.addAndUpdateElementsInGraph(newNodes, newRels)
|
|
317
|
+
```
|
|
File without changes
|
|
File without changes
|