@neo4j-nvl/react 1.2.1 → 2.0.0
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/CHANGELOG.md +43 -0
- package/README.md +6 -6
- package/lib/__tests__/BasicNvlWrapper.test.js +26 -2
- package/lib/__tests__/InteractiveNvlWrapper.test.js +7 -3
- package/lib/basic-wrapper/BasicNvlWrapper.d.ts +1 -11
- package/lib/basic-wrapper/BasicNvlWrapper.js +8 -14
- package/lib/interactive-nvl-wrapper/InteractionHandlers.js +3 -1
- package/lib/interactive-nvl-wrapper/InteractiveNvlWrapper.d.ts +1 -1
- package/lib/interactive-nvl-wrapper/InteractiveNvlWrapper.js +3 -3
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to NVL will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
4
4
|
|
|
5
|
+
## [2.0.0] - 2026-09-11
|
|
6
|
+
|
|
7
|
+
This 2.0.0 major release upgrades NVL to WebGL2 for both the renderer and physics engine, unlocking major performance improvements. It also adds new clustering capabilities with visual decorations to the force directed and hierarchical layouts.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
* `clusterBy` layout option for both force-directed and hierarchical layouts.
|
|
12
|
+
* Cluster interaction API for handling events on clustered elements.
|
|
13
|
+
* Support for rendering icons and arrowheads in the WebGL renderer.
|
|
14
|
+
* Configurable minimum and maximum FPS caps for the rendering loop.
|
|
15
|
+
* Circle seeding option for the physics layout.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
* Upgraded the WebGL renderer and physics engine to use WebGL2.
|
|
20
|
+
* Migrated the hierarchical layout engine dependency to `@dagrejs/dagre` 3.1.1.
|
|
21
|
+
* Greatly optimized physics layout performance and stabalisation speed.
|
|
22
|
+
* Improved rendering performance.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
* Added safety guards to harden the renderer against missing X/Y coordinate data.
|
|
27
|
+
|
|
28
|
+
### Removed
|
|
29
|
+
|
|
30
|
+
* **BREAKING:** Removed deprecated Cytoscape support settings.
|
|
31
|
+
* **BREAKING:** Removed deprecated standalone `layout` and `layoutOptions` props from the React wrappers in favour of the same props inside `NvlOptions`.
|
|
32
|
+
* **BREAKING:** Removed deprecated `intelWorkaround` layout setting APIs.
|
|
33
|
+
* **BREAKING:** Removed deprecated `onZoom` callback in favour of `onZoomAndPan`.
|
|
34
|
+
|
|
35
|
+
## [1.2.2] - 2026-09-11
|
|
36
|
+
|
|
37
|
+
This 1.2.2 patch release adds migration warnings for breaking changes planned for the next major release.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
* `styling.overlayLineColor` for overlay UI stroke color (e.g. minimap viewport box).
|
|
41
|
+
* Note in documentation on changes to client/screen coordinates on `NVL.getNodes()` and `onLayoutStep` `x`/`y`, moving screen coordinates to `clientPosition`.
|
|
42
|
+
|
|
43
|
+
### Deprecated
|
|
44
|
+
* `styling.minimapViewportBoxColor` in favour of `styling.overlayLineColor` (old option still works as a fallback).
|
|
45
|
+
* `ForceDirectedOptions.intelWorkaround` (upgrade to WebGL2 in the next major release removes the need for it).
|
|
46
|
+
* Top-level React wrapper `layout` / `layoutOptions` props in favour of `nvlOptions.layout` / `nvlOptions.layoutOptions`.
|
|
47
|
+
|
|
5
48
|
## [1.2.1] - 2026-07-28
|
|
6
49
|
|
|
7
50
|
This 1.2.1 patch release contains several fixes and accessibility improvements.
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ const nvlRef = useRef<NVL>()
|
|
|
47
47
|
rels={[{ from: '0', to: '1', id: '10' }]}
|
|
48
48
|
ref={nvlRef}
|
|
49
49
|
/>
|
|
50
|
-
<button onClick={() => nvlRef.current?.
|
|
50
|
+
<button onClick={() => nvlRef.current?.fit(['0', '1'])}>Zoom to Nodes</button>
|
|
51
51
|
</div>
|
|
52
52
|
```
|
|
53
53
|
|
|
@@ -59,18 +59,18 @@ Events can be turned on and off by passing a callback function or a boolean valu
|
|
|
59
59
|
`mouseEventCallbacks` prop. The callback function will be called with the event's arguments when the event is triggered. If a boolean value is passed, the event will be turned on or off accordingly.
|
|
60
60
|
|
|
61
61
|
```tsx
|
|
62
|
-
const [
|
|
62
|
+
const [boxSelect, setBoxSelect] = useState(false)
|
|
63
63
|
<>
|
|
64
|
-
<button onClick={() =>
|
|
65
|
-
{
|
|
64
|
+
<button onClick={() => setBoxSelect(!boxSelect)}>
|
|
65
|
+
{boxSelect ? 'Disable' : 'Enable'} box select
|
|
66
66
|
</button>
|
|
67
67
|
<InteractiveNvlWrapper
|
|
68
68
|
nodes={nodes}
|
|
69
69
|
rels={relationships}
|
|
70
70
|
mouseEventCallbacks={{
|
|
71
|
-
onHover: (element) => console.log(element),
|
|
71
|
+
onHover: (element, hitElements) => console.log(element, hitElements),
|
|
72
72
|
onNodeClick: (node) => console.log(node),
|
|
73
|
-
|
|
73
|
+
onBoxSelect: boxSelect
|
|
74
74
|
}}
|
|
75
75
|
/>
|
|
76
76
|
</>
|
|
@@ -27,8 +27,12 @@ describe('BasicNvlWrapper', () => {
|
|
|
27
27
|
});
|
|
28
28
|
test('initialises NVL expectedly with a graph object and properties', () => {
|
|
29
29
|
const mockLayoutDoneFunction = jest.fn();
|
|
30
|
-
render(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [{ id: '1' }, { id: '2' }], rels: [{ id: '12', from: '1', to: '2' }],
|
|
31
|
-
|
|
30
|
+
render(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [{ id: '1' }, { id: '2' }], rels: [{ id: '12', from: '1', to: '2' }], nvlOptions: {
|
|
31
|
+
renderer: 'canvas',
|
|
32
|
+
layout: HierarchicalLayoutType,
|
|
33
|
+
layoutOptions: { direction: 'down' }
|
|
34
|
+
}, nvlCallbacks: { onLayoutDone: mockLayoutDoneFunction } }) }));
|
|
35
|
+
expect(NVL).toHaveBeenCalledWith(expect.any(HTMLDivElement), [{ id: '1' }, { id: '2' }], [{ id: '12', from: '1', to: '2' }], { renderer: 'canvas', layout: HierarchicalLayoutType, layoutOptions: { direction: 'down' } }, {
|
|
32
36
|
onLayoutDone: mockLayoutDoneFunction
|
|
33
37
|
});
|
|
34
38
|
});
|
|
@@ -132,4 +136,24 @@ describe('BasicNvlWrapper', () => {
|
|
|
132
136
|
expect(setZoomAndPan).toHaveBeenCalledTimes(1);
|
|
133
137
|
expect(setZoom).not.toHaveBeenCalled();
|
|
134
138
|
});
|
|
139
|
+
test('calls setLayout when nvlOptions.layout changes', () => {
|
|
140
|
+
const setLayout = jest.fn();
|
|
141
|
+
jest.spyOn(NVL.prototype, 'setLayout').mockImplementation(setLayout);
|
|
142
|
+
const { rerender } = render(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], nvlOptions: { layout: HierarchicalLayoutType } }) }));
|
|
143
|
+
expect(setLayout).toHaveBeenCalledWith(HierarchicalLayoutType);
|
|
144
|
+
expect(setLayout).toHaveBeenCalledTimes(1);
|
|
145
|
+
rerender(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], nvlOptions: { layout: 'forceDirected' } }) }));
|
|
146
|
+
expect(setLayout).toHaveBeenCalledWith('forceDirected');
|
|
147
|
+
expect(setLayout).toHaveBeenCalledTimes(2);
|
|
148
|
+
});
|
|
149
|
+
test('calls setLayoutOptions when nvlOptions.layoutOptions changes', () => {
|
|
150
|
+
const setLayoutOptions = jest.fn();
|
|
151
|
+
jest.spyOn(NVL.prototype, 'setLayoutOptions').mockImplementation(setLayoutOptions);
|
|
152
|
+
const { rerender } = render(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], nvlOptions: { layoutOptions: { direction: 'down' } } }) }));
|
|
153
|
+
expect(setLayoutOptions).toHaveBeenCalledWith({ direction: 'down' });
|
|
154
|
+
expect(setLayoutOptions).toHaveBeenCalledTimes(1);
|
|
155
|
+
rerender(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], nvlOptions: { layoutOptions: { direction: 'up' } } }) }));
|
|
156
|
+
expect(setLayoutOptions).toHaveBeenCalledWith({ direction: 'up' });
|
|
157
|
+
expect(setLayoutOptions).toHaveBeenCalledTimes(2);
|
|
158
|
+
});
|
|
135
159
|
});
|
|
@@ -85,11 +85,15 @@ describe('InteractiveNvlWrapper', () => {
|
|
|
85
85
|
});
|
|
86
86
|
test('initialises NVL expectedly with a graph object and properties', () => {
|
|
87
87
|
const mockLayoutDoneFunction = jest.fn();
|
|
88
|
-
render(_jsx(InteractiveNvlWrapper, { nodes: [{ id: '1' }, { id: '2' }], rels: [{ id: '12', from: '1', to: '2' }],
|
|
88
|
+
render(_jsx(InteractiveNvlWrapper, { nodes: [{ id: '1' }, { id: '2' }], rels: [{ id: '12', from: '1', to: '2' }], nvlOptions: {
|
|
89
|
+
renderer: 'canvas',
|
|
90
|
+
layout: HierarchicalLayoutType,
|
|
91
|
+
layoutOptions: { direction: 'down' }
|
|
92
|
+
}, nvlCallbacks: { onLayoutDone: mockLayoutDoneFunction } }));
|
|
89
93
|
act(() => {
|
|
90
94
|
mockOnInitialization?.();
|
|
91
95
|
});
|
|
92
|
-
expect(NVL).toHaveBeenCalledWith(expect.any(HTMLDivElement), [{ id: '1' }, { id: '2' }], [{ id: '12', from: '1', to: '2' }], { renderer: 'canvas', layout: HierarchicalLayoutType, layoutOptions: {
|
|
96
|
+
expect(NVL).toHaveBeenCalledWith(expect.any(HTMLDivElement), [{ id: '1' }, { id: '2' }], [{ id: '12', from: '1', to: '2' }], { renderer: 'canvas', layout: HierarchicalLayoutType, layoutOptions: { direction: 'down' } }, {
|
|
93
97
|
onLayoutDone: mockLayoutDoneFunction,
|
|
94
98
|
onInitialization: mockOnInitialization
|
|
95
99
|
});
|
|
@@ -187,7 +191,7 @@ describe('InteractiveNvlWrapper', () => {
|
|
|
187
191
|
{
|
|
188
192
|
name: 'ZoomInteraction',
|
|
189
193
|
InteractionClass: ZoomInteraction,
|
|
190
|
-
callback: {
|
|
194
|
+
callback: { onZoomAndPan: jest.fn() },
|
|
191
195
|
options: {}
|
|
192
196
|
},
|
|
193
197
|
{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExternalCallbacks,
|
|
1
|
+
import type { ExternalCallbacks, Node, NvlOptions, Relationship } from '@neo4j-nvl/base';
|
|
2
2
|
import NVL from '@neo4j-nvl/base';
|
|
3
3
|
import { type HTMLProps } from 'react';
|
|
4
4
|
/**
|
|
@@ -9,16 +9,6 @@ export interface BasicReactWrapperProps {
|
|
|
9
9
|
nodes: Node[];
|
|
10
10
|
/** The rels of the graph of type Relationship[] */
|
|
11
11
|
rels: Relationship[];
|
|
12
|
-
/**
|
|
13
|
-
* The layout, can be 'forceDirected' or 'hierarchical'
|
|
14
|
-
* @deprecated Use the layout property in nvlOptions instead. This property will be removed in a future version.
|
|
15
|
-
*/
|
|
16
|
-
layout?: Layout;
|
|
17
|
-
/**
|
|
18
|
-
* Options for the current layout
|
|
19
|
-
* @deprecated Use the layoutOptions property in nvlOptions instead. This property will be removed in a future version.
|
|
20
|
-
*/
|
|
21
|
-
layoutOptions?: LayoutOptions;
|
|
22
12
|
/** an Object containing functions for callbacks on certain actions */
|
|
23
13
|
nvlCallbacks?: ExternalCallbacks;
|
|
24
14
|
/** An object containing options for the Nvl instance */
|
|
@@ -12,7 +12,7 @@ import { useDeepCompareEffect } from '../utils/hooks';
|
|
|
12
12
|
*
|
|
13
13
|
* For examples, head to the {@link https://neo4j.com/docs/nvl/current/react-wrappers/#_basic_react_wrapper Basic React wrapper documentation page}.
|
|
14
14
|
*/
|
|
15
|
-
export const BasicNvlWrapper = memo(forwardRef(({ nodes, rels,
|
|
15
|
+
export const BasicNvlWrapper = memo(forwardRef(({ nodes, rels, nvlCallbacks = {}, nvlOptions = {}, positions = [], zoom, pan, onInitializationError, ...nvlEvents }, ref) => {
|
|
16
16
|
const nvlRef = useRef(null);
|
|
17
17
|
const prevZoomRef = useRef(undefined);
|
|
18
18
|
const prevPanRef = useRef(undefined);
|
|
@@ -47,12 +47,8 @@ export const BasicNvlWrapper = memo(forwardRef(({ nodes, rels, layout, layoutOpt
|
|
|
47
47
|
const mainContainerIsReady = containerRef.current !== null;
|
|
48
48
|
if (mainContainerIsReady && minimapContainerIsReady) {
|
|
49
49
|
if (nvlRef.current === null) {
|
|
50
|
-
const combinedOptions = { ...nvlOptions, layoutOptions };
|
|
51
|
-
if (layout !== undefined) {
|
|
52
|
-
combinedOptions.layout = layout;
|
|
53
|
-
}
|
|
54
50
|
try {
|
|
55
|
-
newNvl = new NVL(containerRef.current, currentNodes, currentRels,
|
|
51
|
+
newNvl = new NVL(containerRef.current, currentNodes, currentRels, nvlOptions, nvlCallbacks);
|
|
56
52
|
nvlRef.current = newNvl;
|
|
57
53
|
setCurrentRels(rels);
|
|
58
54
|
setCurrentNodes(nodes);
|
|
@@ -96,19 +92,17 @@ export const BasicNvlWrapper = memo(forwardRef(({ nodes, rels, layout, layoutOpt
|
|
|
96
92
|
nvlRef.current.removeNodesWithIds(nodesToRemove);
|
|
97
93
|
}, [currentNodes, currentRels, nodes, rels]);
|
|
98
94
|
useEffect(() => {
|
|
99
|
-
|
|
100
|
-
if (nvlRef.current === null || updatedLayout === undefined) {
|
|
95
|
+
if (nvlRef.current === null || nvlOptions.layout === undefined) {
|
|
101
96
|
return;
|
|
102
97
|
}
|
|
103
|
-
nvlRef.current.setLayout(
|
|
104
|
-
}, [
|
|
98
|
+
nvlRef.current.setLayout(nvlOptions.layout);
|
|
99
|
+
}, [nvlOptions.layout]);
|
|
105
100
|
useDeepCompareEffect(() => {
|
|
106
|
-
|
|
107
|
-
if (nvlRef.current === null || updatedLayoutOptions === undefined) {
|
|
101
|
+
if (nvlRef.current === null || nvlOptions.layoutOptions === undefined) {
|
|
108
102
|
return;
|
|
109
103
|
}
|
|
110
|
-
nvlRef.current.setLayoutOptions(
|
|
111
|
-
}, [
|
|
104
|
+
nvlRef.current.setLayoutOptions(nvlOptions.layoutOptions);
|
|
105
|
+
}, [nvlOptions.layoutOptions]);
|
|
112
106
|
useEffect(() => {
|
|
113
107
|
if (nvlRef.current === null || nvlOptions.renderer === undefined) {
|
|
114
108
|
return;
|
|
@@ -21,8 +21,10 @@ export const InteractionHandlers = ({ nvlRef, mouseEventCallbacks, keyboardEvent
|
|
|
21
21
|
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onCanvasClick, 'onCanvasClick', nvlRef, interactionOptions);
|
|
22
22
|
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onCanvasDoubleClick, 'onCanvasDoubleClick', nvlRef, interactionOptions);
|
|
23
23
|
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onCanvasRightClick, 'onCanvasRightClick', nvlRef, interactionOptions);
|
|
24
|
+
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onClusterClick, 'onClusterClick', nvlRef, interactionOptions);
|
|
25
|
+
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onClusterDoubleClick, 'onClusterDoubleClick', nvlRef, interactionOptions);
|
|
26
|
+
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onClusterRightClick, 'onClusterRightClick', nvlRef, interactionOptions);
|
|
24
27
|
useInteraction(PanInteraction, panInteraction, mouseEventCallbacks.onPan, 'onPan', nvlRef, interactionOptions);
|
|
25
|
-
useInteraction(ZoomInteraction, zoomInteraction, mouseEventCallbacks.onZoom, 'onZoom', nvlRef, interactionOptions);
|
|
26
28
|
useInteraction(ZoomInteraction, zoomInteraction, mouseEventCallbacks.onZoomAndPan, 'onZoomAndPan', nvlRef, interactionOptions);
|
|
27
29
|
useInteraction(DragNodeInteraction, dragNodeInteraction, mouseEventCallbacks.onDrag, 'onDrag', nvlRef, interactionOptions);
|
|
28
30
|
useInteraction(DragNodeInteraction, dragNodeInteraction, mouseEventCallbacks.onDragStart, 'onDragStart', nvlRef, interactionOptions);
|
|
@@ -10,6 +10,6 @@ import type { InteractiveNvlWrapperProps } from './types';
|
|
|
10
10
|
* The mouseEventCallbacks property takes an object where various callbacks can be defined
|
|
11
11
|
* and behavior can be toggled on and off.
|
|
12
12
|
*
|
|
13
|
-
* For examples, head to the {@link https://neo4j.com/docs/nvl/current/react-wrappers/#
|
|
13
|
+
* For examples, head to the {@link https://neo4j.com/docs/nvl/current/react-wrappers/#_interactive_react_wrapper Interactive React wrapper documentation page}.
|
|
14
14
|
*/
|
|
15
15
|
export declare const InteractiveNvlWrapper: import("react").MemoExoticComponent<import("react").ForwardRefExoticComponent<Omit<InteractiveNvlWrapperProps & HTMLProps<HTMLDivElement>, "ref"> & import("react").RefAttributes<NVL>>>;
|
|
@@ -18,9 +18,9 @@ const options = {
|
|
|
18
18
|
* The mouseEventCallbacks property takes an object where various callbacks can be defined
|
|
19
19
|
* and behavior can be toggled on and off.
|
|
20
20
|
*
|
|
21
|
-
* For examples, head to the {@link https://neo4j.com/docs/nvl/current/react-wrappers/#
|
|
21
|
+
* For examples, head to the {@link https://neo4j.com/docs/nvl/current/react-wrappers/#_interactive_react_wrapper Interactive React wrapper documentation page}.
|
|
22
22
|
*/
|
|
23
|
-
export const InteractiveNvlWrapper = memo(forwardRef(({ nodes, rels,
|
|
23
|
+
export const InteractiveNvlWrapper = memo(forwardRef(({ nodes, rels, onInitializationError, mouseEventCallbacks = {}, keyboardEventCallbacks = {}, nvlCallbacks = {}, nvlOptions = {}, interactionOptions = options, ...nvlEvents }, nvlRef) => {
|
|
24
24
|
const newNvlRef = useRef(null);
|
|
25
25
|
const myNvlRef = nvlRef ?? newNvlRef;
|
|
26
26
|
const [isNvlInitialized, setIsNvlInitialized] = useState(false);
|
|
@@ -42,5 +42,5 @@ export const InteractiveNvlWrapper = memo(forwardRef(({ nodes, rels, layout, lay
|
|
|
42
42
|
}
|
|
43
43
|
handleInitialization();
|
|
44
44
|
}
|
|
45
|
-
},
|
|
45
|
+
}, onInitializationError: handleInitializationError, ...nvlEvents }), setupInteractions && (_jsx(InteractionHandlers, { nvlRef: myNvlRef, mouseEventCallbacks: mouseEventCallbacks, keyboardEventCallbacks: keyboardEventCallbacks, interactionOptions: interactionOptions }))] }));
|
|
46
46
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neo4j-nvl/react",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"homepage": "https://neo4j.com/docs/nvl/current/",
|
|
6
6
|
"license": "SEE LICENSE IN 'LICENSE.txt'",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"react-dom": "19.2.1"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@neo4j-nvl/base": "
|
|
40
|
-
"@neo4j-nvl/interaction-handlers": "
|
|
39
|
+
"@neo4j-nvl/base": "2.0.0",
|
|
40
|
+
"@neo4j-nvl/interaction-handlers": "2.0.0",
|
|
41
41
|
"lodash": "4.18.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|