@neo4j-nvl/react 0.3.9 → 1.0.0-093d9930
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 +20 -0
- package/lib/__tests__/BasicNvlWrapper.test.js +30 -0
- package/lib/__tests__/InteractiveNvlWrapper.test.js +204 -8
- package/lib/basic-wrapper/BasicNvlWrapper.d.ts +18 -10
- package/lib/basic-wrapper/BasicNvlWrapper.js +30 -21
- package/lib/index.d.ts +4 -2
- package/lib/index.js +2 -1
- package/lib/interactive-nvl-wrapper/InteractionHandlers.js +1 -0
- package/lib/interactive-nvl-wrapper/hooks.d.ts +1 -1
- package/lib/static-picture-wrapper/StaticPictureWrapper.d.ts +22 -0
- package/lib/static-picture-wrapper/StaticPictureWrapper.js +32 -0
- package/lib/utils/hooks.js +4 -4
- package/package.json +17 -12
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
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.0.0] - 2025-09-30
|
|
6
|
+
This 1.0.0 release is our first major release, with NVL's release strategy fully adopting semantic versioning going forward. Updates in this release include React 19 support for the React wrappers, a UI overhaul of the [examples app](https://neo4j.com/docs/api/nvl/current/examples.html).
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
* support for React 19 in Nvl React wrappers.
|
|
10
|
+
* new prop to the React components to provde zoom and pan values.
|
|
11
|
+
* extend return object of `.getZoomTargetForNodePositions` function to include pan values.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
* updated example application UI.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
* display remaining captions when one of the values in the caption array is null or undefined.
|
|
18
|
+
* ensure the computing state for the d3 layout is set correctly.
|
|
19
|
+
* correctly process Right-to-Left (RTL) text for node captions.
|
|
20
|
+
|
|
21
|
+
### Removed
|
|
22
|
+
* `useWebGL` / `setUseWebGLRenderer`, please use [renderer](https://neo4j.com/docs/api/nvl/current/interfaces/_neo4j_nvl_base.NvlOptions.html#renderer) & [setRenderer](https://neo4j.com/docs/api/nvl/current/classes/_neo4j_nvl_base.NVL.html#setRenderer) instead going forward.
|
|
23
|
+
* `NvlOption.nodeDefaultBorderColor`, `NvlOption.selectedBorderColor`, `NvlOption.disabledItemColor`, `NvlOption.disabledItemFontColor`, please use properties with the same name in [NvlOptions.styling](https://neo4j.com/docs/api/nvl/current/interfaces/_neo4j_nvl_base.NvlOptions.html#styling) object going forward.
|
|
24
|
+
|
|
5
25
|
## [0.3.9] - 2025-07-23
|
|
6
26
|
|
|
7
27
|
The 0.3.9 release contains several fixes for NVL's interaction handler and adds a new method to NVL to directly extract image data from an NVL instance:
|
|
@@ -36,4 +36,34 @@ describe('BasicNvlWrapper', () => {
|
|
|
36
36
|
expect(NVL).toHaveBeenCalledTimes(2);
|
|
37
37
|
expect(destroy).toHaveBeenCalledTimes(1);
|
|
38
38
|
});
|
|
39
|
+
test('calls setZoom when zoom property is provided', () => {
|
|
40
|
+
const setZoom = jest.fn();
|
|
41
|
+
jest.spyOn(NVL.prototype, 'setZoom').mockImplementation(setZoom);
|
|
42
|
+
render(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], zoom: 1.5 }) }));
|
|
43
|
+
expect(setZoom).toHaveBeenCalledWith(1.5);
|
|
44
|
+
});
|
|
45
|
+
test('calls setPan when pan property is provided', () => {
|
|
46
|
+
const setPan = jest.fn();
|
|
47
|
+
jest.spyOn(NVL.prototype, 'setPan').mockImplementation(setPan);
|
|
48
|
+
render(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], pan: { x: 10, y: 20 } }) }));
|
|
49
|
+
expect(setPan).toHaveBeenCalledWith(10, 20);
|
|
50
|
+
});
|
|
51
|
+
test('calls setZoomAndPan when both zoom and pan properties are provided', () => {
|
|
52
|
+
const setZoomAndPan = jest.fn();
|
|
53
|
+
jest.spyOn(NVL.prototype, 'setZoomAndPan').mockImplementation(setZoomAndPan);
|
|
54
|
+
render(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], zoom: 1.5, pan: { x: 10, y: 20 } }) }));
|
|
55
|
+
expect(setZoomAndPan).toHaveBeenCalledWith(1.5, 10, 20);
|
|
56
|
+
});
|
|
57
|
+
test('does not call zoom/pan methods when properties are undefined', () => {
|
|
58
|
+
const setZoom = jest.fn();
|
|
59
|
+
jest.spyOn(NVL.prototype, 'setZoom').mockImplementation(setZoom);
|
|
60
|
+
const setPan = jest.fn();
|
|
61
|
+
jest.spyOn(NVL.prototype, 'setPan').mockImplementation(setPan);
|
|
62
|
+
const setZoomAndPan = jest.fn();
|
|
63
|
+
jest.spyOn(NVL.prototype, 'setZoomAndPan').mockImplementation(setZoomAndPan);
|
|
64
|
+
render(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [] }) }));
|
|
65
|
+
expect(setZoom).not.toHaveBeenCalled();
|
|
66
|
+
expect(setPan).not.toHaveBeenCalled();
|
|
67
|
+
expect(setZoomAndPan).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
39
69
|
});
|
|
@@ -2,25 +2,53 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import NVL, { HierarchicalLayoutType } from '@neo4j-nvl/base';
|
|
3
3
|
import { BoxSelectInteraction, ClickInteraction, DragNodeInteraction, DrawInteraction, HoverInteraction, LassoInteraction, PanInteraction, ZoomInteraction } from '@neo4j-nvl/interaction-handlers';
|
|
4
4
|
import '@testing-library/jest-dom';
|
|
5
|
-
import {
|
|
6
|
-
import React, { createRef } from 'react';
|
|
5
|
+
import { render } from '@testing-library/react';
|
|
6
|
+
import React, { act, createRef } from 'react';
|
|
7
7
|
import { InteractiveNvlWrapper } from '../interactive-nvl-wrapper/InteractiveNvlWrapper';
|
|
8
8
|
jest.mock('@neo4j-nvl/base');
|
|
9
9
|
jest.mock('@neo4j-nvl/layout-workers');
|
|
10
10
|
jest.mock('@neo4j-nvl/interaction-handlers');
|
|
11
|
+
const MockedNVL = NVL;
|
|
11
12
|
let mockOnInitialization;
|
|
12
13
|
HoverInteraction.prototype.callbackMap = new Map();
|
|
14
|
+
HoverInteraction.prototype.updateCallback = jest.fn();
|
|
15
|
+
HoverInteraction.prototype.removeCallback = jest.fn();
|
|
16
|
+
HoverInteraction.prototype.destroy = jest.fn();
|
|
13
17
|
PanInteraction.prototype.callbackMap = new Map();
|
|
18
|
+
PanInteraction.prototype.updateCallback = jest.fn();
|
|
19
|
+
PanInteraction.prototype.removeCallback = jest.fn();
|
|
20
|
+
PanInteraction.prototype.destroy = jest.fn();
|
|
21
|
+
ClickInteraction.prototype.callbackMap = new Map();
|
|
22
|
+
ClickInteraction.prototype.updateCallback = jest.fn();
|
|
23
|
+
ClickInteraction.prototype.removeCallback = jest.fn();
|
|
24
|
+
ClickInteraction.prototype.destroy = jest.fn();
|
|
25
|
+
ZoomInteraction.prototype.callbackMap = new Map();
|
|
26
|
+
ZoomInteraction.prototype.updateCallback = jest.fn();
|
|
27
|
+
ZoomInteraction.prototype.removeCallback = jest.fn();
|
|
28
|
+
ZoomInteraction.prototype.destroy = jest.fn();
|
|
29
|
+
DragNodeInteraction.prototype.callbackMap = new Map();
|
|
30
|
+
DragNodeInteraction.prototype.updateCallback = jest.fn();
|
|
31
|
+
DragNodeInteraction.prototype.removeCallback = jest.fn();
|
|
32
|
+
DragNodeInteraction.prototype.destroy = jest.fn();
|
|
33
|
+
DrawInteraction.prototype.callbackMap = new Map();
|
|
34
|
+
DrawInteraction.prototype.updateCallback = jest.fn();
|
|
35
|
+
DrawInteraction.prototype.removeCallback = jest.fn();
|
|
36
|
+
DrawInteraction.prototype.destroy = jest.fn();
|
|
37
|
+
BoxSelectInteraction.prototype.callbackMap = new Map();
|
|
38
|
+
BoxSelectInteraction.prototype.updateCallback = jest.fn();
|
|
39
|
+
BoxSelectInteraction.prototype.removeCallback = jest.fn();
|
|
40
|
+
BoxSelectInteraction.prototype.destroy = jest.fn();
|
|
41
|
+
LassoInteraction.prototype.callbackMap = new Map();
|
|
42
|
+
LassoInteraction.prototype.updateCallback = jest.fn();
|
|
43
|
+
LassoInteraction.prototype.removeCallback = jest.fn();
|
|
44
|
+
LassoInteraction.prototype.destroy = jest.fn();
|
|
14
45
|
let mockDestroy;
|
|
15
46
|
let destroyHoverInteraction;
|
|
16
47
|
let destroyPanInteraction;
|
|
17
48
|
describe('InteractiveNvlWrapper', () => {
|
|
18
49
|
beforeEach(() => {
|
|
19
50
|
jest.spyOn(NVL.prototype, 'getContainer').mockImplementation(() => document.createElement('div'));
|
|
20
|
-
|
|
21
|
-
// @ts-expect-error
|
|
22
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
23
|
-
NVL.mockImplementation((_container, _nodes, _rels, _options, callbacks) => {
|
|
51
|
+
MockedNVL.mockImplementation((_container, _nodes, _rels, _options, callbacks) => {
|
|
24
52
|
mockOnInitialization = callbacks.onInitialization;
|
|
25
53
|
return {
|
|
26
54
|
getContainer: () => document.createElement('div'),
|
|
@@ -36,13 +64,19 @@ describe('InteractiveNvlWrapper', () => {
|
|
|
36
64
|
jest.spyOn(NVL.prototype, 'destroy').mockImplementation(mockDestroy);
|
|
37
65
|
jest.spyOn(HoverInteraction.prototype, 'destroy').mockImplementation(destroyHoverInteraction);
|
|
38
66
|
jest.spyOn(PanInteraction.prototype, 'destroy').mockImplementation(destroyPanInteraction);
|
|
67
|
+
jest.spyOn(ClickInteraction.prototype, 'destroy').mockImplementation(jest.fn());
|
|
68
|
+
jest.spyOn(ZoomInteraction.prototype, 'destroy').mockImplementation(jest.fn());
|
|
69
|
+
jest.spyOn(DragNodeInteraction.prototype, 'destroy').mockImplementation(jest.fn());
|
|
70
|
+
jest.spyOn(DrawInteraction.prototype, 'destroy').mockImplementation(jest.fn());
|
|
71
|
+
jest.spyOn(BoxSelectInteraction.prototype, 'destroy').mockImplementation(jest.fn());
|
|
72
|
+
jest.spyOn(LassoInteraction.prototype, 'destroy').mockImplementation(jest.fn());
|
|
39
73
|
});
|
|
40
74
|
afterEach(() => {
|
|
41
75
|
jest.clearAllMocks();
|
|
42
76
|
});
|
|
43
77
|
test('initialises NVL expectedly with an empty graph object and no other properties', () => {
|
|
44
78
|
render(_jsx("div", { children: _jsx(InteractiveNvlWrapper, { nodes: [], rels: [] }) }));
|
|
45
|
-
expect(NVL).toHaveBeenCalledWith(expect.any(HTMLDivElement), [], [], {}, { onInitialization:
|
|
79
|
+
expect(NVL).toHaveBeenCalledWith(expect.any(HTMLDivElement), [], [], {}, { onInitialization: mockOnInitialization });
|
|
46
80
|
});
|
|
47
81
|
test('initialises NVL expectedly with a graph object and properties', () => {
|
|
48
82
|
const mockLayoutDoneFunction = jest.fn();
|
|
@@ -52,7 +86,7 @@ describe('InteractiveNvlWrapper', () => {
|
|
|
52
86
|
});
|
|
53
87
|
expect(NVL).toHaveBeenCalledWith(expect.any(HTMLDivElement), [{ id: '1' }, { id: '2' }], [{ id: '12', from: '1', to: '2' }], { renderer: 'canvas', layout: HierarchicalLayoutType, layoutOptions: { enableCytoscape: true } }, {
|
|
54
88
|
onLayoutDone: mockLayoutDoneFunction,
|
|
55
|
-
onInitialization:
|
|
89
|
+
onInitialization: mockOnInitialization
|
|
56
90
|
});
|
|
57
91
|
});
|
|
58
92
|
test('initialises expected interaction handlers with correct options', () => {
|
|
@@ -77,6 +111,148 @@ describe('InteractiveNvlWrapper', () => {
|
|
|
77
111
|
expect(BoxSelectInteraction).not.toHaveBeenCalled();
|
|
78
112
|
expect(LassoInteraction).not.toHaveBeenCalled();
|
|
79
113
|
});
|
|
114
|
+
test.each([
|
|
115
|
+
{
|
|
116
|
+
name: 'HoverInteraction',
|
|
117
|
+
InteractionClass: HoverInteraction,
|
|
118
|
+
callback: { onHover: jest.fn() },
|
|
119
|
+
options: { drawShadowOnHover: true }
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'ClickInteraction with onCanvasClick',
|
|
123
|
+
InteractionClass: ClickInteraction,
|
|
124
|
+
callback: { onCanvasClick: jest.fn() },
|
|
125
|
+
options: { selectOnClick: false }
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'ClickInteraction with onCanvasDoubleClick',
|
|
129
|
+
InteractionClass: ClickInteraction,
|
|
130
|
+
callback: { onCanvasDoubleClick: jest.fn() },
|
|
131
|
+
options: { selectOnClick: false }
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'ClickInteraction with onCanvasRightClick',
|
|
135
|
+
InteractionClass: ClickInteraction,
|
|
136
|
+
callback: { onCanvasRightClick: jest.fn() },
|
|
137
|
+
options: { selectOnClick: false }
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'ClickInteraction with onNodeClick',
|
|
141
|
+
InteractionClass: ClickInteraction,
|
|
142
|
+
callback: { onNodeClick: jest.fn() },
|
|
143
|
+
options: { selectOnClick: true }
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'ClickInteraction with onNodeDoubleClick',
|
|
147
|
+
InteractionClass: ClickInteraction,
|
|
148
|
+
callback: { onNodeDoubleClick: jest.fn() },
|
|
149
|
+
options: { selectOnClick: false }
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'ClickInteraction with onNodeRightClick',
|
|
153
|
+
InteractionClass: ClickInteraction,
|
|
154
|
+
callback: { onNodeRightClick: jest.fn() },
|
|
155
|
+
options: { selectOnClick: false }
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: 'ClickInteraction with onRelationshipClick',
|
|
159
|
+
InteractionClass: ClickInteraction,
|
|
160
|
+
callback: { onRelationshipClick: jest.fn() },
|
|
161
|
+
options: { selectOnClick: false }
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'ClickInteraction with onRelationshipDoubleClick',
|
|
165
|
+
InteractionClass: ClickInteraction,
|
|
166
|
+
callback: { onRelationshipDoubleClick: jest.fn() },
|
|
167
|
+
options: { selectOnClick: false }
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'ClickInteraction with onRelationshipRightClick',
|
|
171
|
+
InteractionClass: ClickInteraction,
|
|
172
|
+
callback: { onRelationshipRightClick: jest.fn() },
|
|
173
|
+
options: { selectOnClick: false }
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'PanInteraction',
|
|
177
|
+
InteractionClass: PanInteraction,
|
|
178
|
+
callback: { onPan: jest.fn() },
|
|
179
|
+
options: {}
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'ZoomInteraction',
|
|
183
|
+
InteractionClass: ZoomInteraction,
|
|
184
|
+
callback: { onZoom: jest.fn() },
|
|
185
|
+
options: {}
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: 'DragNodeInteraction with onDrag',
|
|
189
|
+
InteractionClass: DragNodeInteraction,
|
|
190
|
+
callback: { onDrag: jest.fn() },
|
|
191
|
+
options: { selectOnRelease: false }
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: 'DragNodeInteraction with onDragStart',
|
|
195
|
+
InteractionClass: DragNodeInteraction,
|
|
196
|
+
callback: { onDragStart: jest.fn() },
|
|
197
|
+
options: {}
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'DragNodeInteraction with onDragEnd',
|
|
201
|
+
InteractionClass: DragNodeInteraction,
|
|
202
|
+
callback: { onDragEnd: jest.fn() },
|
|
203
|
+
options: {}
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'DrawInteraction with onHoverNodeMargin',
|
|
207
|
+
InteractionClass: DrawInteraction,
|
|
208
|
+
callback: { onHoverNodeMargin: jest.fn() },
|
|
209
|
+
options: { excludeNodeMargin: false }
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'DrawInteraction with onDrawStarted',
|
|
213
|
+
InteractionClass: DrawInteraction,
|
|
214
|
+
callback: { onDrawStarted: jest.fn() },
|
|
215
|
+
options: {}
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: 'DrawInteraction with onDrawEnded',
|
|
219
|
+
InteractionClass: DrawInteraction,
|
|
220
|
+
callback: { onDrawEnded: jest.fn() },
|
|
221
|
+
options: {}
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'BoxSelectInteraction with onBoxStarted',
|
|
225
|
+
InteractionClass: BoxSelectInteraction,
|
|
226
|
+
callback: { onBoxStarted: jest.fn() },
|
|
227
|
+
options: {}
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: 'BoxSelectInteraction with onBoxSelect',
|
|
231
|
+
InteractionClass: BoxSelectInteraction,
|
|
232
|
+
callback: { onBoxSelect: jest.fn() },
|
|
233
|
+
options: {}
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'LassoInteraction with onLassoStarted',
|
|
237
|
+
InteractionClass: LassoInteraction,
|
|
238
|
+
callback: { onLassoStarted: jest.fn() },
|
|
239
|
+
options: {}
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: 'LassoInteraction with onLassoSelect',
|
|
243
|
+
InteractionClass: LassoInteraction,
|
|
244
|
+
callback: { onLassoSelect: jest.fn() },
|
|
245
|
+
options: {}
|
|
246
|
+
}
|
|
247
|
+
])('initialises $name when its callback is provided', ({ InteractionClass, callback, options }) => {
|
|
248
|
+
const myNvlRef = createRef();
|
|
249
|
+
render(_jsx(InteractiveNvlWrapper, { nodes: [], rels: [], ref: myNvlRef, mouseEventCallbacks: callback, interactionOptions: options }));
|
|
250
|
+
act(() => {
|
|
251
|
+
mockOnInitialization?.();
|
|
252
|
+
});
|
|
253
|
+
expect(InteractionClass).toHaveBeenCalledWith(myNvlRef.current, options);
|
|
254
|
+
expect(InteractionClass).toHaveBeenCalledTimes(1);
|
|
255
|
+
});
|
|
80
256
|
test('does not attempt to initialize interaction handlers if NVL container is not available', () => {
|
|
81
257
|
const myNvlRef = createRef();
|
|
82
258
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
@@ -132,4 +308,24 @@ describe('InteractiveNvlWrapper', () => {
|
|
|
132
308
|
});
|
|
133
309
|
expect(customOnInitializationCallback).toHaveBeenCalledTimes(1);
|
|
134
310
|
});
|
|
311
|
+
test('passes zoom and pan properties to BasicNvlWrapper', () => {
|
|
312
|
+
const setZoom = jest.fn();
|
|
313
|
+
const setPan = jest.fn();
|
|
314
|
+
const setZoomAndPan = jest.fn();
|
|
315
|
+
MockedNVL.mockImplementation(() => ({
|
|
316
|
+
getContainer: () => document.createElement('div'),
|
|
317
|
+
destroy: jest.fn(),
|
|
318
|
+
setLayout: jest.fn(),
|
|
319
|
+
setLayoutOptions: jest.fn(),
|
|
320
|
+
setNodePositions: jest.fn(),
|
|
321
|
+
setZoom,
|
|
322
|
+
setPan,
|
|
323
|
+
setZoomAndPan
|
|
324
|
+
}));
|
|
325
|
+
render(_jsx(InteractiveNvlWrapper, { nodes: [], rels: [], zoom: 1.5, pan: { x: 10, y: 20 } }));
|
|
326
|
+
act(() => {
|
|
327
|
+
mockOnInitialization?.();
|
|
328
|
+
});
|
|
329
|
+
expect(setZoomAndPan).toHaveBeenCalledWith(1.5, 10, 20);
|
|
330
|
+
});
|
|
135
331
|
});
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import type { ExternalCallbacks, Layout, LayoutOptions, Node, NvlOptions, Relationship } from '@neo4j-nvl/base';
|
|
2
2
|
import NVL from '@neo4j-nvl/base';
|
|
3
3
|
import { type HTMLProps } from 'react';
|
|
4
|
-
/**
|
|
5
|
-
* @hidden
|
|
6
|
-
*/
|
|
7
|
-
type IncludeMethods<T> = Pick<T, {
|
|
8
|
-
[K in keyof T]: T[K] extends (_: unknown) => unknown ? K : never;
|
|
9
|
-
}[keyof T]>;
|
|
10
4
|
/**
|
|
11
5
|
* The properties that can be passed to the {@link BasicNvlWrapper} component.
|
|
12
6
|
*/
|
|
@@ -23,18 +17,32 @@ export interface BasicReactWrapperProps {
|
|
|
23
17
|
nvlCallbacks?: ExternalCallbacks;
|
|
24
18
|
/** An object containing options for the Nvl instance */
|
|
25
19
|
nvlOptions?: NvlOptions;
|
|
26
|
-
/** Sets the positions of the nodes in the graph using the
|
|
20
|
+
/** Sets the positions of the nodes in the graph using the setNodePositions method. */
|
|
27
21
|
positions?: Node[];
|
|
22
|
+
/**
|
|
23
|
+
* Sets the zoom level of the viewport using the setZoom method.
|
|
24
|
+
* @remarks If both zoom and pan are provided, the setZoomAndPan method will be used.
|
|
25
|
+
*/
|
|
26
|
+
zoom?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Sets the pan coordinates of the viewport using the setPan method.
|
|
29
|
+
* @remarks If both zoom and pan are provided, the setZoomAndPan method will be used.
|
|
30
|
+
*/
|
|
31
|
+
pan?: {
|
|
32
|
+
/** The x coordinate of the pan. */
|
|
33
|
+
x: number;
|
|
34
|
+
/** The y coordinate of the pan. */
|
|
35
|
+
y: number;
|
|
36
|
+
};
|
|
28
37
|
/** A callback to handle any errors that happen during NVL initialization */
|
|
29
38
|
onInitializationError?: (error: unknown) => void;
|
|
30
39
|
}
|
|
31
40
|
/**
|
|
32
41
|
*
|
|
33
|
-
* A basic React wrapper that wraps the base
|
|
42
|
+
* A basic React wrapper that wraps the NVL base library within a React component.
|
|
34
43
|
* It takes the class' arguments as properties, which are passed to the NVL constructor.
|
|
35
44
|
* Any changes in properties will be reflected in the NVL instance by calling the corresponding methods.
|
|
36
45
|
*
|
|
37
46
|
* For examples, head to the {@link https://neo4j.com/docs/nvl/current/react-wrappers/#_basic_react_wrapper Basic React wrapper documentation page}.
|
|
38
47
|
*/
|
|
39
|
-
export declare const BasicNvlWrapper: import("react").MemoExoticComponent<import("react").ForwardRefExoticComponent<Omit<BasicReactWrapperProps & HTMLProps<HTMLDivElement>, "ref"> & import("react").RefAttributes<
|
|
40
|
-
export {};
|
|
48
|
+
export declare const BasicNvlWrapper: import("react").MemoExoticComponent<import("react").ForwardRefExoticComponent<Omit<BasicReactWrapperProps & HTMLProps<HTMLDivElement>, "ref"> & import("react").RefAttributes<Partial<Pick<NVL, "restart" | "destroy" | "addAndUpdateElementsInGraph" | "getSelectedNodes" | "getSelectedRelationships" | "removeNodesWithIds" | "removeRelationshipsWithIds" | "getNodes" | "getRelationships" | "getNodeById" | "getRelationshipById" | "getPositionById" | "getCurrentOptions" | "deselectAll" | "fit" | "resetZoom" | "setRenderer" | "setDisableWebGL" | "pinNode" | "unPinNode" | "setLayout" | "setLayoutOptions" | "getNodesOnScreen" | "getNodePositions" | "setNodePositions" | "isLayoutMoving" | "saveToFile" | "getImageDataUrl" | "saveFullGraphToLargeFile" | "setZoom" | "getScale" | "getPan" | "getHits" | "getContainer">>>>>;
|
|
@@ -6,16 +6,15 @@ import { getMapDifferences, getNodeAttributeDifferences } from '../utils/graph-c
|
|
|
6
6
|
import { useDeepCompareEffect } from '../utils/hooks';
|
|
7
7
|
/**
|
|
8
8
|
*
|
|
9
|
-
* A basic React wrapper that wraps the base
|
|
9
|
+
* A basic React wrapper that wraps the NVL base library within a React component.
|
|
10
10
|
* It takes the class' arguments as properties, which are passed to the NVL constructor.
|
|
11
11
|
* Any changes in properties will be reflected in the NVL instance by calling the corresponding methods.
|
|
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, layout, layoutOptions, nvlCallbacks = {}, nvlOptions = {}, positions = [], onInitializationError, ...nvlEvents }, ref) => {
|
|
15
|
+
export const BasicNvlWrapper = memo(forwardRef(({ nodes, rels, layout, layoutOptions, nvlCallbacks = {}, nvlOptions = {}, positions = [], zoom, pan, onInitializationError, ...nvlEvents }, ref) => {
|
|
16
16
|
const nvlRef = useRef(null);
|
|
17
17
|
useImperativeHandle(ref, () => {
|
|
18
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
19
18
|
const nvlMethods = Object.getOwnPropertyNames(NVL.prototype);
|
|
20
19
|
return nvlMethods.reduce((current, method) => ({
|
|
21
20
|
...current,
|
|
@@ -23,20 +22,26 @@ export const BasicNvlWrapper = memo(forwardRef(({ nodes, rels, layout, layoutOpt
|
|
|
23
22
|
if (nvlRef.current === null) {
|
|
24
23
|
return null;
|
|
25
24
|
}
|
|
26
|
-
// @
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
27
26
|
return nvlRef.current[method](...args);
|
|
28
27
|
}
|
|
29
|
-
}),
|
|
30
|
-
// eslint-disable-next-line max-len
|
|
31
|
-
// eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter, @typescript-eslint/consistent-type-assertions
|
|
32
|
-
{});
|
|
28
|
+
}), {});
|
|
33
29
|
});
|
|
34
30
|
const containerRef = useRef(null);
|
|
35
31
|
const [currentNodes, setCurrentNodes] = useState(nodes);
|
|
36
32
|
const [currentRels, setCurrentRels] = useState(rels);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
return () => {
|
|
35
|
+
nvlRef.current?.destroy();
|
|
36
|
+
nvlRef.current = null;
|
|
37
|
+
};
|
|
38
|
+
}, []);
|
|
37
39
|
useEffect(() => {
|
|
38
40
|
let newNvl = null;
|
|
39
|
-
|
|
41
|
+
const hasMinimap = 'minimapContainer' in nvlOptions;
|
|
42
|
+
const minimapContainerIsReady = hasMinimap ? nvlOptions.minimapContainer !== null : true;
|
|
43
|
+
const mainContainerIsReady = containerRef.current !== null;
|
|
44
|
+
if (mainContainerIsReady && minimapContainerIsReady) {
|
|
40
45
|
if (nvlRef.current === null) {
|
|
41
46
|
const combinedOptions = { ...nvlOptions, layoutOptions };
|
|
42
47
|
if (layout !== undefined) {
|
|
@@ -58,12 +63,8 @@ export const BasicNvlWrapper = memo(forwardRef(({ nodes, rels, layout, layoutOpt
|
|
|
58
63
|
}
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
|
-
return () => {
|
|
62
|
-
newNvl?.destroy();
|
|
63
|
-
nvlRef.current = null;
|
|
64
|
-
};
|
|
65
66
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
|
-
}, []);
|
|
67
|
+
}, [containerRef.current, nvlOptions.minimapContainer]);
|
|
67
68
|
useEffect(() => {
|
|
68
69
|
if (nvlRef.current === null) {
|
|
69
70
|
return;
|
|
@@ -104,18 +105,12 @@ export const BasicNvlWrapper = memo(forwardRef(({ nodes, rels, layout, layoutOpt
|
|
|
104
105
|
}
|
|
105
106
|
nvlRef.current.setLayoutOptions(updatedLayoutOptions);
|
|
106
107
|
}, [layoutOptions, nvlOptions.layoutOptions]);
|
|
107
|
-
useEffect(() => {
|
|
108
|
-
if (nvlRef.current === null || nvlOptions.useWebGL === undefined) {
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
nvlRef.current.setUseWebGLRenderer(nvlOptions.useWebGL);
|
|
112
|
-
}, [nvlOptions.useWebGL]);
|
|
113
108
|
useEffect(() => {
|
|
114
109
|
if (nvlRef.current === null || nvlOptions.renderer === undefined) {
|
|
115
110
|
return;
|
|
116
111
|
}
|
|
117
112
|
nvlRef.current.setRenderer(nvlOptions.renderer);
|
|
118
|
-
});
|
|
113
|
+
}, [nvlOptions.renderer]);
|
|
119
114
|
useEffect(() => {
|
|
120
115
|
if (nvlRef.current === null || nvlOptions.disableWebGL === undefined) {
|
|
121
116
|
return;
|
|
@@ -128,5 +123,19 @@ export const BasicNvlWrapper = memo(forwardRef(({ nodes, rels, layout, layoutOpt
|
|
|
128
123
|
}
|
|
129
124
|
nvlRef.current.setNodePositions(positions);
|
|
130
125
|
}, [positions]);
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (nvlRef.current === null) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (zoom !== undefined && pan !== undefined) {
|
|
131
|
+
nvlRef.current.setZoomAndPan(zoom, pan.x, pan.y);
|
|
132
|
+
}
|
|
133
|
+
else if (zoom !== undefined) {
|
|
134
|
+
nvlRef.current.setZoom(zoom);
|
|
135
|
+
}
|
|
136
|
+
else if (pan !== undefined) {
|
|
137
|
+
nvlRef.current.setPan(pan.x, pan.y);
|
|
138
|
+
}
|
|
139
|
+
}, [zoom, pan]);
|
|
131
140
|
return _jsx("div", { id: BASIC_WRAPPER_ID, ref: containerRef, style: { height: '100%', outline: '0' }, ...nvlEvents });
|
|
132
141
|
}));
|
package/lib/index.d.ts
CHANGED
|
@@ -2,5 +2,7 @@ import { BasicNvlWrapper } from './basic-wrapper/BasicNvlWrapper';
|
|
|
2
2
|
import type { BasicReactWrapperProps } from './basic-wrapper/BasicNvlWrapper';
|
|
3
3
|
import { InteractiveNvlWrapper } from './interactive-nvl-wrapper/InteractiveNvlWrapper';
|
|
4
4
|
import type { InteractionOptions, InteractiveNvlWrapperProps, MouseEvent, MouseEventCallbacks } from './interactive-nvl-wrapper/types';
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import type { StaticPictureWrapperProps } from './static-picture-wrapper/StaticPictureWrapper';
|
|
6
|
+
import { StaticPictureWrapper } from './static-picture-wrapper/StaticPictureWrapper';
|
|
7
|
+
export { BasicNvlWrapper, InteractiveNvlWrapper, StaticPictureWrapper };
|
|
8
|
+
export type { MouseEventCallbacks, MouseEvent, BasicReactWrapperProps, InteractionOptions, InteractiveNvlWrapperProps, StaticPictureWrapperProps };
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { BasicNvlWrapper } from './basic-wrapper/BasicNvlWrapper';
|
|
2
2
|
import { InteractiveNvlWrapper } from './interactive-nvl-wrapper/InteractiveNvlWrapper';
|
|
3
|
-
|
|
3
|
+
import { StaticPictureWrapper } from './static-picture-wrapper/StaticPictureWrapper';
|
|
4
|
+
export { BasicNvlWrapper, InteractiveNvlWrapper, StaticPictureWrapper };
|
|
@@ -18,6 +18,7 @@ export const InteractionHandlers = ({ nvlRef, mouseEventCallbacks, interactionOp
|
|
|
18
18
|
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onRelationshipDoubleClick, 'onRelationshipDoubleClick', nvlRef, interactionOptions);
|
|
19
19
|
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onRelationshipRightClick, 'onRelationshipRightClick', nvlRef, interactionOptions);
|
|
20
20
|
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onCanvasClick, 'onCanvasClick', nvlRef, interactionOptions);
|
|
21
|
+
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onCanvasDoubleClick, 'onCanvasDoubleClick', nvlRef, interactionOptions);
|
|
21
22
|
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onCanvasRightClick, 'onCanvasRightClick', nvlRef, interactionOptions);
|
|
22
23
|
useInteraction(PanInteraction, panInteraction, mouseEventCallbacks.onPan, 'onPan', nvlRef, interactionOptions);
|
|
23
24
|
useInteraction(ZoomInteraction, zoomInteraction, mouseEventCallbacks.onZoom, 'onZoom', nvlRef, interactionOptions);
|
|
@@ -2,4 +2,4 @@ import type NVL from '@neo4j-nvl/base';
|
|
|
2
2
|
import type { MutableRefObject } from 'react';
|
|
3
3
|
import type { InteractionOptions, MouseEvent, MouseInteraction, MouseInteractionModule } from './types';
|
|
4
4
|
export declare const destroyInteraction: (interactionRef: MutableRefObject<MouseInteraction | null>) => void;
|
|
5
|
-
export declare const useInteraction: (Interaction: MouseInteractionModule, interactionRef: MutableRefObject<MouseInteraction | null>, callback:
|
|
5
|
+
export declare const useInteraction: (Interaction: MouseInteractionModule, interactionRef: MutableRefObject<MouseInteraction | null>, callback: ((...args: unknown[]) => void) | boolean | undefined, eventName: MouseEvent, nvlRef: MutableRefObject<NVL | null>, interactionOptions: InteractionOptions) => void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Node, NvlOptions, Relationship } from '@neo4j-nvl/base';
|
|
2
|
+
/**
|
|
3
|
+
* The properties that can be passed to the StaticPictureWrapper component.
|
|
4
|
+
*/
|
|
5
|
+
export type StaticPictureWrapperProps = {
|
|
6
|
+
/** The nodes to be displayed in the graph. */
|
|
7
|
+
nodes: Node[];
|
|
8
|
+
/** The relationships to be displayed in the graph. */
|
|
9
|
+
rels: Relationship[];
|
|
10
|
+
/** Options to customize the NVL instance. */
|
|
11
|
+
nvlOptions?: NvlOptions;
|
|
12
|
+
/** The width of the static picture. */
|
|
13
|
+
width?: number;
|
|
14
|
+
/** The height of the static picture. */
|
|
15
|
+
height?: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* A React component that creates a static picture of a graph using NVL.
|
|
19
|
+
* This component is useful for generating static visualizations of graphs without requiring user interaction.
|
|
20
|
+
* NVL is destroyed after capturing the image to free up resources.
|
|
21
|
+
*/
|
|
22
|
+
export declare const StaticPictureWrapper: ({ nodes, rels, nvlOptions, width, height }: StaticPictureWrapperProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import NVL from '@neo4j-nvl/base';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
/**
|
|
5
|
+
* A React component that creates a static picture of a graph using NVL.
|
|
6
|
+
* This component is useful for generating static visualizations of graphs without requiring user interaction.
|
|
7
|
+
* NVL is destroyed after capturing the image to free up resources.
|
|
8
|
+
*/
|
|
9
|
+
export const StaticPictureWrapper = ({ nodes, rels, nvlOptions = {}, width = 500, height = 500 }) => {
|
|
10
|
+
const [imgSrc, setImgSrc] = useState();
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const div = document.createElement('div');
|
|
13
|
+
div.style.width = `${width / window.devicePixelRatio}px`;
|
|
14
|
+
div.style.height = `${height / window.devicePixelRatio}px`;
|
|
15
|
+
const myNvl = new NVL(div, nodes, rels, nvlOptions, {
|
|
16
|
+
onLayoutDone: () => {
|
|
17
|
+
myNvl.fit(myNvl.getNodes().map((n) => n.id));
|
|
18
|
+
},
|
|
19
|
+
onZoomTransitionDone: () => {
|
|
20
|
+
setImgSrc(myNvl.getImageDataUrl());
|
|
21
|
+
setTimeout(() => {
|
|
22
|
+
myNvl.destroy();
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
return () => {
|
|
27
|
+
myNvl?.destroy();
|
|
28
|
+
};
|
|
29
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
30
|
+
}, [nodes, rels, width, height]);
|
|
31
|
+
return imgSrc !== undefined ? _jsx("img", { src: imgSrc, width: width, height: height, alt: "Graph" }) : null;
|
|
32
|
+
};
|
package/lib/utils/hooks.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { isEqual } from 'lodash';
|
|
2
2
|
import { useEffect, useRef } from 'react';
|
|
3
|
-
|
|
3
|
+
const deepCompareEquals = (a, b) => {
|
|
4
4
|
return isEqual(a, b);
|
|
5
|
-
}
|
|
6
|
-
|
|
5
|
+
};
|
|
6
|
+
const useDeepCompareMemoize = (value) => {
|
|
7
7
|
const ref = useRef();
|
|
8
8
|
if (!deepCompareEquals(value, ref.current)) {
|
|
9
9
|
ref.current = value;
|
|
10
10
|
}
|
|
11
11
|
return ref.current;
|
|
12
|
-
}
|
|
12
|
+
};
|
|
13
13
|
export const useDeepCompareEffect = (callback, dependencies) => {
|
|
14
14
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
15
15
|
useEffect(callback, dependencies.map(useDeepCompareMemoize));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neo4j-nvl/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-093d9930",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"homepage": "https://neo4j.com/docs/nvl/current/",
|
|
6
6
|
"license": "SEE LICENSE IN 'LICENSE.txt'",
|
|
@@ -27,17 +27,22 @@
|
|
|
27
27
|
"lib"
|
|
28
28
|
],
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@testing-library/
|
|
31
|
-
"@testing-library/
|
|
32
|
-
"@
|
|
33
|
-
"@types/
|
|
34
|
-
"
|
|
30
|
+
"@testing-library/dom": "10.4.1",
|
|
31
|
+
"@testing-library/jest-dom": "5.17.0",
|
|
32
|
+
"@testing-library/react": "16.3.0",
|
|
33
|
+
"@types/lodash": "4.17.21",
|
|
34
|
+
"@types/react": "18.3.27",
|
|
35
|
+
"react": "19.2.1",
|
|
36
|
+
"react-dom": "19.2.1"
|
|
35
37
|
},
|
|
36
38
|
"dependencies": {
|
|
37
|
-
"@neo4j-nvl/base": "0.
|
|
38
|
-
"@neo4j-nvl/interaction-handlers": "0.
|
|
39
|
-
"lodash": "4.17.21"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
"@neo4j-nvl/base": "1.0.0-093d9930",
|
|
40
|
+
"@neo4j-nvl/interaction-handlers": "1.0.0-093d9930",
|
|
41
|
+
"lodash": "4.17.21"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"react": "18.0.0 || ^19.0.0",
|
|
45
|
+
"react-dom": "18.0.0 || ^19.0.0"
|
|
46
|
+
},
|
|
47
|
+
"stableVersion": "1.0.0"
|
|
43
48
|
}
|