@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 ADDED
@@ -0,0 +1,3 @@
1
+ ### react
2
+
3
+ A React wrapper that provides the NVL class as a React component.
@@ -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
@@ -0,0 +1,2 @@
1
+ import { BasicNvlWrapper } from './basic-wrapper/BasicNvlWrapper';
2
+ export { BasicNvlWrapper };
package/lib/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import { BasicNvlWrapper } from './basic-wrapper/BasicNvlWrapper';
2
+ export { BasicNvlWrapper };
@@ -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
+ }