@neo4j-nvl/react 0.1.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/README.md +3 -0
- package/lib/basic-wrapper/BasicNvlWrapper.d.ts +71 -0
- package/lib/basic-wrapper/BasicNvlWrapper.js +109 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/utils/graphComparison.d.ts +14 -0
- package/lib/utils/graphComparison.js +59 -0
- package/lib/utils/hooks.d.ts +1 -0
- package/lib/utils/hooks.js +15 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { NamedExoticComponent } from 'react';
|
|
2
|
+
import { NvlOptions, Node, Relationship, ExternalCallbacks, LayoutOptions, Layout } from '@neo4j-nvl/core';
|
|
3
|
+
/**
|
|
4
|
+
* A basic React wrapper for the NVL class.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* This is the most basic way of using the wrapper. It will create a new NVL instance with the given nodes and relationships.
|
|
8
|
+
* ```tsx
|
|
9
|
+
* <BasicNvlWrapper
|
|
10
|
+
* nodes={[{ id: 0 }, { id: 1 }, { id: 2 }]}
|
|
11
|
+
* rels={[{ from: 0, to: 1, id: 10 }, { from: 0, to: 2, id: 11 }]}
|
|
12
|
+
* />
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* This is a more advanced example, where the nodes and relationships are updated dynamically.
|
|
17
|
+
* ```tsx
|
|
18
|
+
* const [nodes, setNodes] = useState<Node[]>([{ id: 0 }, { id: 1 }, { id: 2 }])
|
|
19
|
+
* const [rels, setRels] = useState<Relationship[]>([{ from: 0, to: 1, id: 10 }, { from: 0, to: 2, id: 11 }])
|
|
20
|
+
*
|
|
21
|
+
* const addNode = () => {
|
|
22
|
+
* const newNodes = [...nodes, { id: nodes.length }]
|
|
23
|
+
* setNodes(newNodes)
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* const addRel = () => {
|
|
27
|
+
* const newRels = [...rels, { from: 0, to: nodes.length - 1, id: rels.length }]
|
|
28
|
+
* setRels(newRels)
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* <div>
|
|
32
|
+
* <BasicNvlWrapper
|
|
33
|
+
* nodes={nodes}
|
|
34
|
+
* rels={rels}
|
|
35
|
+
* />
|
|
36
|
+
* <button onClick={addNode}>Add Node</button>
|
|
37
|
+
* <button onClick={addRel}>Add Relationship</button>
|
|
38
|
+
* </div>
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* This is an example of how to use a reference of NVL to call
|
|
43
|
+
* NVL methods from inside the React wrapper.
|
|
44
|
+
* ```tsx
|
|
45
|
+
* const nvlRef = useRef()
|
|
46
|
+
*
|
|
47
|
+
* <div>
|
|
48
|
+
* <BasicNvlWrapper
|
|
49
|
+
* nodes={[{ id: 0 }, { id: 1 }, { id: 2 }]}
|
|
50
|
+
* rels={[{ from: 0, to: 1, id: 10 }, { from: 0, to: 2, id: 11 }]}
|
|
51
|
+
* ref={nvlRef}
|
|
52
|
+
* />
|
|
53
|
+
* <button onClick={() => nvlRef.current?.zoomToNodes([0, 1])}>Zoom to Nodes</button>
|
|
54
|
+
* </div>
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
declare const BasicNvlWrapper: NamedExoticComponent<{
|
|
58
|
+
/** The nodes of the graph of type Node[] */
|
|
59
|
+
nodes: Node[];
|
|
60
|
+
/** The rels of the graph of type Relationship[] */
|
|
61
|
+
rels: Relationship[];
|
|
62
|
+
/** The layout, can be 'forceDirected' or 'hierarchical' */
|
|
63
|
+
layout?: Layout;
|
|
64
|
+
/** Options for the current layout */
|
|
65
|
+
layoutOptions?: LayoutOptions;
|
|
66
|
+
/** an Object containing functions for callbacks on certain actions */
|
|
67
|
+
nvlCallbacks?: ExternalCallbacks;
|
|
68
|
+
/** An object containing options for the NVL instance */
|
|
69
|
+
nvlOptions?: NvlOptions;
|
|
70
|
+
}>;
|
|
71
|
+
export { BasicNvlWrapper };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React, { useEffect, useState, useRef, forwardRef, useImperativeHandle, memo } from 'react';
|
|
2
|
+
import NVL from '@neo4j-nvl/core';
|
|
3
|
+
import { getMapDifferences, getNodeAttributeDifferences } from '../utils/graphComparison';
|
|
4
|
+
import { useDeepCompareEffect } from '../utils/hooks';
|
|
5
|
+
/**
|
|
6
|
+
* A basic React wrapper for the NVL class.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* This is the most basic way of using the wrapper. It will create a new NVL instance with the given nodes and relationships.
|
|
10
|
+
* ```tsx
|
|
11
|
+
* <BasicNvlWrapper
|
|
12
|
+
* nodes={[{ id: 0 }, { id: 1 }, { id: 2 }]}
|
|
13
|
+
* rels={[{ from: 0, to: 1, id: 10 }, { from: 0, to: 2, id: 11 }]}
|
|
14
|
+
* />
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* This is a more advanced example, where the nodes and relationships are updated dynamically.
|
|
19
|
+
* ```tsx
|
|
20
|
+
* const [nodes, setNodes] = useState<Node[]>([{ id: 0 }, { id: 1 }, { id: 2 }])
|
|
21
|
+
* const [rels, setRels] = useState<Relationship[]>([{ from: 0, to: 1, id: 10 }, { from: 0, to: 2, id: 11 }])
|
|
22
|
+
*
|
|
23
|
+
* const addNode = () => {
|
|
24
|
+
* const newNodes = [...nodes, { id: nodes.length }]
|
|
25
|
+
* setNodes(newNodes)
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* const addRel = () => {
|
|
29
|
+
* const newRels = [...rels, { from: 0, to: nodes.length - 1, id: rels.length }]
|
|
30
|
+
* setRels(newRels)
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* <div>
|
|
34
|
+
* <BasicNvlWrapper
|
|
35
|
+
* nodes={nodes}
|
|
36
|
+
* rels={rels}
|
|
37
|
+
* />
|
|
38
|
+
* <button onClick={addNode}>Add Node</button>
|
|
39
|
+
* <button onClick={addRel}>Add Relationship</button>
|
|
40
|
+
* </div>
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* This is an example of how to use a reference of NVL to call
|
|
45
|
+
* NVL methods from inside the React wrapper.
|
|
46
|
+
* ```tsx
|
|
47
|
+
* const nvlRef = useRef()
|
|
48
|
+
*
|
|
49
|
+
* <div>
|
|
50
|
+
* <BasicNvlWrapper
|
|
51
|
+
* nodes={[{ id: 0 }, { id: 1 }, { id: 2 }]}
|
|
52
|
+
* rels={[{ from: 0, to: 1, id: 10 }, { from: 0, to: 2, id: 11 }]}
|
|
53
|
+
* ref={nvlRef}
|
|
54
|
+
* />
|
|
55
|
+
* <button onClick={() => nvlRef.current?.zoomToNodes([0, 1])}>Zoom to Nodes</button>
|
|
56
|
+
* </div>
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
const BasicNvlWrapper = memo(forwardRef(({ nodes, rels, layout, layoutOptions, nvlCallbacks = {}, nvlOptions }, ref) => {
|
|
60
|
+
useImperativeHandle(ref, () => {
|
|
61
|
+
const nvlMethods = Object.getOwnPropertyNames(NVL.prototype);
|
|
62
|
+
return nvlMethods.reduce((current, method) => (Object.assign(Object.assign({}, current), { [method]: (...args) => nvl && nvl[method](...args) })), {});
|
|
63
|
+
});
|
|
64
|
+
const containerRef = useRef();
|
|
65
|
+
const [nvl, setNvl] = useState();
|
|
66
|
+
const [currentNodes, setCurrentNodes] = useState(nodes);
|
|
67
|
+
const [currentRels, setCurrentRels] = useState(rels);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!nvl) {
|
|
70
|
+
const combinedOptions = Object.assign(Object.assign({}, nvlOptions), { layoutOptions });
|
|
71
|
+
if (layout) {
|
|
72
|
+
combinedOptions.layout = layout;
|
|
73
|
+
}
|
|
74
|
+
const newNvl = new NVL(containerRef.current, currentNodes, currentRels, combinedOptions, nvlCallbacks);
|
|
75
|
+
setNvl(newNvl);
|
|
76
|
+
setCurrentRels(rels);
|
|
77
|
+
setCurrentNodes(nodes);
|
|
78
|
+
return () => {
|
|
79
|
+
newNvl.destroy();
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}, []);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!nvl)
|
|
85
|
+
return;
|
|
86
|
+
const nodeChanges = getMapDifferences(currentNodes, nodes);
|
|
87
|
+
const nodeDiff = getNodeAttributeDifferences(currentNodes, nodes);
|
|
88
|
+
const relChanges = getMapDifferences(currentRels, rels);
|
|
89
|
+
setCurrentRels(rels);
|
|
90
|
+
setCurrentNodes(nodes);
|
|
91
|
+
nvl.updateGraph([...nodeChanges.added, ...nodeDiff], [...relChanges.added, ...relChanges.updated]);
|
|
92
|
+
nvl.removeRelationshipsWithIds(relChanges.removed.map((r) => r.id));
|
|
93
|
+
nvl.removeNodesWithIds(nodeChanges.removed.map((n) => n.id));
|
|
94
|
+
}, [nodes, rels]);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
nvl === null || nvl === void 0 ? void 0 : nvl.setLayout(layout);
|
|
97
|
+
}, [layout]);
|
|
98
|
+
useDeepCompareEffect(() => {
|
|
99
|
+
nvl === null || nvl === void 0 ? void 0 : nvl.setLayoutOptions(layoutOptions);
|
|
100
|
+
}, [layoutOptions]);
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
nvl === null || nvl === void 0 ? void 0 : nvl.setUseWebGLRenderer(nvlOptions === null || nvlOptions === void 0 ? void 0 : nvlOptions.useWebGL);
|
|
103
|
+
}, [nvlOptions === null || nvlOptions === void 0 ? void 0 : nvlOptions.useWebGL]);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
nvl === null || nvl === void 0 ? void 0 : nvl.setDisableWebGL(nvlOptions === null || nvlOptions === void 0 ? void 0 : nvlOptions.disableWebGL);
|
|
106
|
+
}, [nvlOptions === null || nvlOptions === void 0 ? void 0 : nvlOptions.disableWebGL]);
|
|
107
|
+
return React.createElement("div", { ref: containerRef, style: { height: '100%', outline: '0' } });
|
|
108
|
+
}));
|
|
109
|
+
export { BasicNvlWrapper };
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Node, Relationship } from '@neo4j-nvl/core';
|
|
2
|
+
interface NodeChanges {
|
|
3
|
+
added: Node[];
|
|
4
|
+
removed: Node[];
|
|
5
|
+
updated: Node[];
|
|
6
|
+
}
|
|
7
|
+
interface RelChanges {
|
|
8
|
+
added: Relationship[];
|
|
9
|
+
removed: Relationship[];
|
|
10
|
+
updated: Relationship[];
|
|
11
|
+
}
|
|
12
|
+
declare const getMapDifferences: (prevGraphElements: Node[] | Relationship[], newGraphElements: Node[] | Relationship[]) => any;
|
|
13
|
+
declare const getNodeAttributeDifferences: (prevNodes: Node[], newNodes: Node[]) => any[];
|
|
14
|
+
export { getNodeAttributeDifferences, getMapDifferences, NodeChanges, RelChanges };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { isEqual, keyBy, transform, sortBy, keys } from 'lodash';
|
|
2
|
+
const getMapDifferences = (prevGraphElements, newGraphElements) => {
|
|
3
|
+
const prevMap = keyBy(prevGraphElements, 'id');
|
|
4
|
+
const currentMap = keyBy(newGraphElements, 'id');
|
|
5
|
+
const prevIds = sortBy(keys(prevMap));
|
|
6
|
+
const currentIds = sortBy(keys(currentMap));
|
|
7
|
+
const added = [];
|
|
8
|
+
const removed = [];
|
|
9
|
+
const updated = [];
|
|
10
|
+
let i = 0;
|
|
11
|
+
let j = 0;
|
|
12
|
+
while (i < prevIds.length && j < currentIds.length) {
|
|
13
|
+
const prevId = prevIds[i];
|
|
14
|
+
const currId = currentIds[j];
|
|
15
|
+
if (prevId === currId) {
|
|
16
|
+
if (!isEqual(prevMap[prevId], currentMap[currId])) {
|
|
17
|
+
updated.push(currId);
|
|
18
|
+
}
|
|
19
|
+
i++;
|
|
20
|
+
j++;
|
|
21
|
+
}
|
|
22
|
+
else if (prevId < currId) {
|
|
23
|
+
removed.push(prevId);
|
|
24
|
+
i++;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
added.push(currId);
|
|
28
|
+
j++;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
while (i < prevIds.length) {
|
|
32
|
+
removed.push(prevIds[i++]);
|
|
33
|
+
}
|
|
34
|
+
while (j < currentIds.length) {
|
|
35
|
+
added.push(currentIds[j++]);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
added: added.map(id => currentMap[id]),
|
|
39
|
+
removed: removed.map(id => prevMap[id]),
|
|
40
|
+
updated: updated.map(id => currentMap[id])
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const getNodeAttributeDifferences = (prevNodes, newNodes) => {
|
|
44
|
+
const prevNodeMap = keyBy(prevNodes, 'id');
|
|
45
|
+
return newNodes
|
|
46
|
+
.map(nodeToUpdate => {
|
|
47
|
+
const previousNode = prevNodeMap[nodeToUpdate.id];
|
|
48
|
+
if (!previousNode) {
|
|
49
|
+
return nodeToUpdate;
|
|
50
|
+
}
|
|
51
|
+
return transform(nodeToUpdate, (result, value, key) => {
|
|
52
|
+
if (key === 'id' || value !== previousNode[key]) {
|
|
53
|
+
result[key] = value;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
})
|
|
57
|
+
.filter(n => Object.keys(n).length > 1);
|
|
58
|
+
};
|
|
59
|
+
export { getNodeAttributeDifferences, getMapDifferences };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function useDeepCompareEffect(callback: any, dependencies: any): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { isEqual } from 'lodash';
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
function deepCompareEquals(a, b) {
|
|
4
|
+
return isEqual(a, b);
|
|
5
|
+
}
|
|
6
|
+
function useDeepCompareMemoize(value) {
|
|
7
|
+
const ref = useRef();
|
|
8
|
+
if (!deepCompareEquals(value, ref.current)) {
|
|
9
|
+
ref.current = value;
|
|
10
|
+
}
|
|
11
|
+
return ref.current;
|
|
12
|
+
}
|
|
13
|
+
export const useDeepCompareEffect = (callback, dependencies) => {
|
|
14
|
+
useEffect(callback, dependencies.map(useDeepCompareMemoize));
|
|
15
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neo4j-nvl/react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "lib/index.js",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"prebuild": "rm -rf lib/",
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"test": "jest"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"lib"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"yarn": "^1.10.1"
|
|
15
|
+
},
|
|
16
|
+
"typedoc": {
|
|
17
|
+
"entryPoint": "./src/index.ts",
|
|
18
|
+
"readmeFile": "./README.md",
|
|
19
|
+
"displayName": "React",
|
|
20
|
+
"tsconfig": "./tsconfig.json"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@testing-library/jest-dom": "^5.16.5",
|
|
24
|
+
"@testing-library/react": "^13.4.0",
|
|
25
|
+
"@types/lodash": "^4.14.184",
|
|
26
|
+
"@types/react": "^18.0.18",
|
|
27
|
+
"babel-eslint": "^10.1.0",
|
|
28
|
+
"typedoc": "^0.23.15"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"react": "^18.2.0",
|
|
32
|
+
"react-dom": "^18.2.0"
|
|
33
|
+
}
|
|
34
|
+
}
|