@neo4j-nvl/react 1.2.0-80937bca → 1.2.0-84e1e1ad
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 +16 -0
- package/README.md +1 -1
- package/lib/__tests__/BasicNvlWrapper.test.js +26 -2
- package/lib/__tests__/InteractiveNvlWrapper.test.js +7 -3
- package/lib/basic-wrapper/BasicNvlWrapper.d.ts +1 -5
- package/lib/basic-wrapper/BasicNvlWrapper.js +8 -14
- package/lib/interactive-nvl-wrapper/InteractionHandlers.js +0 -1
- package/lib/interactive-nvl-wrapper/InteractiveNvlWrapper.js +2 -2
- package/lib/static-picture-wrapper/StaticPictureWrapper.js +3 -3
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
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
|
+
## [1.2.1] - 2026-07-28
|
|
6
|
+
|
|
7
|
+
This 1.2.1 patch release contains several fixes and accessibility improvements.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
* Outline when the canvas is focused while using the keyboard interaction handlers.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
* Renamed links in README from Explore to Neo4j Bloom.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
* Aligned padding on svg and png export.
|
|
17
|
+
* Aligned start behaviour of box and lasso select interaction handlers.
|
|
18
|
+
* Fixed static image wrapper React component returning incorrect height when using SVG format.
|
|
19
|
+
* Fixed self-referencing relationship captions being misplaced on downward curves.
|
|
20
|
+
|
|
5
21
|
## [1.2.0] - 2026-05-27
|
|
6
22
|
|
|
7
23
|
This 1.2.0 release introduces a new keyboard interaction handler and SVG support for the static image wrapper React component. It also includes several performance improvements for clustering and text/icon rendering, as well as several bug fixes.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Neo4j Visualization Library
|
|
2
2
|
|
|
3
|
-
Welcome to the Neo4j Visualization Library, NVL for short. NVL is a collection of libraries that can be used to build custom graph visualizations like those used in [Neo4j Bloom
|
|
3
|
+
Welcome to the Neo4j Visualization Library, NVL for short. NVL is a collection of libraries that can be used to build custom graph visualizations like those used in [Neo4j Bloom](https://neo4j.com/product/bloom/). NVL is written in TypeScript and can be used in any JavaScript project. This package contains React components that make it easier to use NVL within a React application. If you want to use NVL with a framework other than React or want to build your own React wrapper, you can do so by using the [NVL base library](https://www.npmjs.com/package/@neo4j-nvl/base).
|
|
4
4
|
|
|
5
5
|
## Consuming the library
|
|
6
6
|
|
|
@@ -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,10 +9,6 @@ export interface BasicReactWrapperProps {
|
|
|
9
9
|
nodes: Node[];
|
|
10
10
|
/** The rels of the graph of type Relationship[] */
|
|
11
11
|
rels: Relationship[];
|
|
12
|
-
/** The layout, can be 'forceDirected' or 'hierarchical' */
|
|
13
|
-
layout?: Layout;
|
|
14
|
-
/** Options for the current layout */
|
|
15
|
-
layoutOptions?: LayoutOptions;
|
|
16
12
|
/** an Object containing functions for callbacks on certain actions */
|
|
17
13
|
nvlCallbacks?: ExternalCallbacks;
|
|
18
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;
|
|
@@ -22,7 +22,6 @@ export const InteractionHandlers = ({ nvlRef, mouseEventCallbacks, keyboardEvent
|
|
|
22
22
|
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onCanvasDoubleClick, 'onCanvasDoubleClick', nvlRef, interactionOptions);
|
|
23
23
|
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onCanvasRightClick, 'onCanvasRightClick', nvlRef, interactionOptions);
|
|
24
24
|
useInteraction(PanInteraction, panInteraction, mouseEventCallbacks.onPan, 'onPan', nvlRef, interactionOptions);
|
|
25
|
-
useInteraction(ZoomInteraction, zoomInteraction, mouseEventCallbacks.onZoom, 'onZoom', nvlRef, interactionOptions);
|
|
26
25
|
useInteraction(ZoomInteraction, zoomInteraction, mouseEventCallbacks.onZoomAndPan, 'onZoomAndPan', nvlRef, interactionOptions);
|
|
27
26
|
useInteraction(DragNodeInteraction, dragNodeInteraction, mouseEventCallbacks.onDrag, 'onDrag', nvlRef, interactionOptions);
|
|
28
27
|
useInteraction(DragNodeInteraction, dragNodeInteraction, mouseEventCallbacks.onDragStart, 'onDragStart', nvlRef, interactionOptions);
|
|
@@ -20,7 +20,7 @@ const options = {
|
|
|
20
20
|
*
|
|
21
21
|
* For examples, head to the {@link https://neo4j.com/docs/nvl/current/react-wrappers/#_interactive_reactive_wrapperr 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
|
}));
|
|
@@ -24,8 +24,8 @@ export const StaticPictureWrapper = ({ nodes, rels, nvlOptions = {}, width = 500
|
|
|
24
24
|
const [imgSrc, setImgSrc] = useState();
|
|
25
25
|
useEffect(() => {
|
|
26
26
|
const div = document.createElement('div');
|
|
27
|
-
div.style.width = `${width
|
|
28
|
-
div.style.height = `${height
|
|
27
|
+
div.style.width = `${width}px`;
|
|
28
|
+
div.style.height = `${height}px`;
|
|
29
29
|
const myNvl = new NVL(div, nodes, rels, nvlOptions, {
|
|
30
30
|
onLayoutDone: () => {
|
|
31
31
|
myNvl.fit(myNvl.getNodes().map((n) => n.id));
|
|
@@ -53,5 +53,5 @@ export const StaticPictureWrapper = ({ nodes, rels, nvlOptions = {}, width = 500
|
|
|
53
53
|
};
|
|
54
54
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
55
55
|
}, [nodes, rels, width, height, format]);
|
|
56
|
-
return imgSrc !== undefined ? _jsx("img", { src: imgSrc, width: width, height: height, alt: "Graph" }) : null;
|
|
56
|
+
return imgSrc !== undefined ? (_jsx("img", { src: imgSrc, width: width, height: height, style: { display: 'block', width, height, objectFit: 'contain' }, alt: "Graph" })) : null;
|
|
57
57
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neo4j-nvl/react",
|
|
3
|
-
"version": "1.2.0-
|
|
3
|
+
"version": "1.2.0-84e1e1ad",
|
|
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": "1.2.0-
|
|
40
|
-
"@neo4j-nvl/interaction-handlers": "1.2.0-
|
|
39
|
+
"@neo4j-nvl/base": "1.2.0-84e1e1ad",
|
|
40
|
+
"@neo4j-nvl/interaction-handlers": "1.2.0-84e1e1ad",
|
|
41
41
|
"lodash": "4.18.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|