@neo4j-nvl/base 2.0.0-8a6da460 → 2.0.0-d21c8bbe

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.
@@ -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