@neo4j-nvl/interaction-handlers 0.2.53 → 0.2.55

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.
@@ -19,6 +19,9 @@ const shapeShowTime = 500;
19
19
  * })
20
20
  */
21
21
  export class LassoInteraction extends BaseInteraction {
22
+ active;
23
+ points;
24
+ overlayRenderer;
22
25
  /**
23
26
  * Creates a new instance of the lasso interaction handler.
24
27
  * @param nvl - The NVL instance to attach the interaction handler to
@@ -26,141 +29,103 @@ export class LassoInteraction extends BaseInteraction {
26
29
  */
27
30
  constructor(nvl, options = { selectOnRelease: false }) {
28
31
  super(nvl, options);
29
- Object.defineProperty(this, "active", {
30
- enumerable: true,
31
- configurable: true,
32
- writable: true,
33
- value: void 0
34
- });
35
- Object.defineProperty(this, "points", {
36
- enumerable: true,
37
- configurable: true,
38
- writable: true,
39
- value: void 0
40
- });
41
- Object.defineProperty(this, "overlayRenderer", {
42
- enumerable: true,
43
- configurable: true,
44
- writable: true,
45
- value: void 0
46
- });
47
- Object.defineProperty(this, "startLasso", {
48
- enumerable: true,
49
- configurable: true,
50
- writable: true,
51
- value: (event) => {
52
- this.active = true;
53
- this.points = [getCanvasPosition(this.containerInstance, event)];
54
- this.callCallbackIfRegistered('onLassoStarted', event);
32
+ this.overlayRenderer = new OverlayRenderer(this.containerInstance);
33
+ this.active = false;
34
+ this.addEventListener('mousedown', this.handleMouseDown, true);
35
+ this.addEventListener('mousemove', this.handleDrag, true);
36
+ this.addEventListener('mouseup', this.handleMouseUp, true);
37
+ this.addEventListener('mouseleave', this.endLasso, true);
38
+ }
39
+ startLasso = (event) => {
40
+ this.active = true;
41
+ this.points = [getCanvasPosition(this.containerInstance, event)];
42
+ this.callCallbackIfRegistered('onLassoStarted', event);
43
+ };
44
+ handleMouseDown = (event) => {
45
+ if (event.button === 0 && !this.active) {
46
+ this.startLasso(event);
47
+ }
48
+ };
49
+ handleDrag = (event) => {
50
+ if (this.active) {
51
+ const lastPoint = this.points[this.points.length - 1];
52
+ if (lastPoint === undefined) {
53
+ return;
55
54
  }
56
- });
57
- Object.defineProperty(this, "handleMouseDown", {
58
- enumerable: true,
59
- configurable: true,
60
- writable: true,
61
- value: (event) => {
62
- if (event.button === 0 && !this.active) {
63
- this.startLasso(event);
64
- }
55
+ const pos = getCanvasPosition(this.containerInstance, event);
56
+ const dx = Math.abs(lastPoint.x - pos.x);
57
+ const dy = Math.abs(lastPoint.y - pos.y);
58
+ if (dx > pointDist || dy > pointDist) {
59
+ this.points.push(pos);
60
+ this.overlayRenderer.drawLasso(this.points, true, false);
65
61
  }
66
- });
67
- Object.defineProperty(this, "handleDrag", {
68
- enumerable: true,
69
- configurable: true,
70
- writable: true,
71
- value: (event) => {
72
- if (this.active) {
73
- const lastPoint = this.points[this.points.length - 1];
74
- const pos = getCanvasPosition(this.containerInstance, event);
75
- const dx = Math.abs(lastPoint.x - pos.x);
76
- const dy = Math.abs(lastPoint.y - pos.y);
77
- if (dx > pointDist || dy > pointDist) {
78
- this.points.push(pos);
79
- this.overlayRenderer.drawLasso(this.points, true, false);
80
- }
62
+ }
63
+ };
64
+ handleMouseUp = (event) => {
65
+ this.points.push(getCanvasPosition(this.containerInstance, event));
66
+ this.endLasso(event);
67
+ };
68
+ getLassoItems = (points) => {
69
+ const inside = (x, y, vs) => {
70
+ // ray-casting algorithm based on
71
+ // https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html
72
+ let isInside = false;
73
+ for (let i = 0, j = vs.length - 1; i < vs.length; j = i, i += 1) {
74
+ const vsI = vs[i];
75
+ const vsJ = vs[j];
76
+ if (vsI === undefined || vsJ === undefined) {
77
+ continue;
78
+ }
79
+ const { x: xi, y: yi } = vsI;
80
+ const { x: xj, y: yj } = vsJ;
81
+ const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
82
+ if (intersect) {
83
+ isInside = !isInside;
81
84
  }
82
85
  }
83
- });
84
- Object.defineProperty(this, "handleMouseUp", {
85
- enumerable: true,
86
- configurable: true,
87
- writable: true,
88
- value: (event) => {
89
- this.points.push(getCanvasPosition(this.containerInstance, event));
90
- this.endLasso(event);
86
+ return isInside;
87
+ };
88
+ const worldPoints = points.map((p) => getWorldPosition(this.nvlInstance, p));
89
+ const nodePositions = this.nvlInstance.getNodePositions();
90
+ const hitNodes = new Set();
91
+ for (const pos of nodePositions) {
92
+ if (pos.x === undefined || pos.y === undefined || pos.id === undefined) {
93
+ continue;
91
94
  }
92
- });
93
- Object.defineProperty(this, "getLassoItems", {
94
- enumerable: true,
95
- configurable: true,
96
- writable: true,
97
- value: (points) => {
98
- const inside = (x, y, vs) => {
99
- // ray-casting algorithm based on
100
- // https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html
101
- let isInside = false;
102
- for (let i = 0, j = vs.length - 1; i < vs.length; j = i, i += 1) {
103
- const { x: xi, y: yi } = vs[i];
104
- const { x: xj, y: yj } = vs[j];
105
- const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
106
- if (intersect) {
107
- isInside = !isInside;
108
- }
109
- }
110
- return isInside;
111
- };
112
- const worldPoints = points.map((p) => getWorldPosition(this.nvlInstance, p));
113
- const nodePositions = this.nvlInstance.getNodePositions();
114
- const hitNodes = new Set();
115
- for (const pos of nodePositions) {
116
- if (pos.x === undefined || pos.y === undefined || pos.id === undefined) {
117
- continue;
118
- }
119
- if (inside(pos.x, pos.y, worldPoints)) {
120
- hitNodes.add(pos.id);
121
- }
122
- }
123
- const rels = this.nvlInstance.getRelationships();
124
- const hitRels = [];
125
- for (const rel of rels) {
126
- if (hitNodes.has(rel.from) && hitNodes.has(rel.to)) {
127
- hitRels.push(rel);
128
- }
129
- }
130
- const hitNodeArray = Array.from(hitNodes).map((id) => this.nvlInstance.getNodeById(id));
131
- return {
132
- nodes: hitNodeArray,
133
- rels: hitRels
134
- };
95
+ if (inside(pos.x, pos.y, worldPoints)) {
96
+ hitNodes.add(pos.id);
135
97
  }
136
- });
137
- Object.defineProperty(this, "endLasso", {
138
- enumerable: true,
139
- configurable: true,
140
- writable: true,
141
- value: (event) => {
142
- if (!this.active) {
143
- return;
144
- }
145
- this.active = false;
146
- const pointArrays = this.points.map((p) => [p.x, p.y]);
147
- const hull = concaveman(pointArrays, 3).map((p) => ({ x: p[0], y: p[1] }));
148
- this.overlayRenderer.drawLasso(hull, false, true);
149
- setTimeout(() => this.overlayRenderer.clear(), shapeShowTime);
150
- const hitItems = this.getLassoItems(hull);
151
- if (this.currentOptions.selectOnRelease) {
152
- this.nvlInstance.updateElementsInGraph(hitItems.nodes.map((node) => ({ id: node.id, selected: true })), hitItems.rels.map((rel) => ({ id: rel.id, selected: true })));
153
- }
154
- this.callCallbackIfRegistered('onLassoSelect', hitItems, event);
98
+ }
99
+ const rels = this.nvlInstance.getRelationships();
100
+ const hitRels = [];
101
+ for (const rel of rels) {
102
+ if (hitNodes.has(rel.from) && hitNodes.has(rel.to)) {
103
+ hitRels.push(rel);
155
104
  }
156
- });
157
- this.overlayRenderer = new OverlayRenderer(this.containerInstance);
105
+ }
106
+ const hitNodeArray = Array.from(hitNodes).map((id) => this.nvlInstance.getNodeById(id));
107
+ return {
108
+ nodes: hitNodeArray,
109
+ rels: hitRels
110
+ };
111
+ };
112
+ endLasso = (event) => {
113
+ if (!this.active) {
114
+ return;
115
+ }
158
116
  this.active = false;
159
- this.addEventListener('mousedown', this.handleMouseDown, true);
160
- this.addEventListener('mousemove', this.handleDrag, true);
161
- this.addEventListener('mouseup', this.handleMouseUp, true);
162
- this.addEventListener('mouseleave', this.endLasso, true);
163
- }
117
+ const pointArrays = this.points.map((p) => [p.x, p.y]);
118
+ const hull = concaveman(pointArrays, 3)
119
+ .map((p) => ({ x: p[0], y: p[1] }))
120
+ .filter((point) => point.x !== undefined && point.y !== undefined);
121
+ this.overlayRenderer.drawLasso(hull, false, true);
122
+ setTimeout(() => this.overlayRenderer.clear(), shapeShowTime);
123
+ const hitItems = this.getLassoItems(hull);
124
+ if (this.currentOptions.selectOnRelease === true) {
125
+ this.nvlInstance.updateElementsInGraph(hitItems.nodes.map((node) => ({ id: node.id, selected: true })), hitItems.rels.map((rel) => ({ id: rel.id, selected: true })));
126
+ }
127
+ this.callCallbackIfRegistered('onLassoSelect', hitItems, event);
128
+ };
164
129
  /**
165
130
  * Removes all related event listeners and the overlay renderer for the box.
166
131
  */
@@ -15,93 +15,15 @@ import { BaseInteraction } from './base';
15
15
  * })
16
16
  */
17
17
  export class PanInteraction extends BaseInteraction {
18
+ mousePosition;
19
+ targets;
20
+ shouldPan;
18
21
  /**
19
22
  * Creates a new instance of the pan interaction handler.
20
23
  * @param nvl - The NVL instance to attach the interaction handler to
21
24
  */
22
25
  constructor(nvl, options = { excludeNodeMargin: false }) {
23
26
  super(nvl, options);
24
- Object.defineProperty(this, "mousePosition", {
25
- enumerable: true,
26
- configurable: true,
27
- writable: true,
28
- value: void 0
29
- });
30
- Object.defineProperty(this, "targets", {
31
- enumerable: true,
32
- configurable: true,
33
- writable: true,
34
- value: void 0
35
- });
36
- Object.defineProperty(this, "shouldPan", {
37
- enumerable: true,
38
- configurable: true,
39
- writable: true,
40
- value: void 0
41
- });
42
- /**
43
- * Updates which type of graph elements should hinder panning.
44
- * @param targets - The graph elements that should hinder panning
45
- * @param excludeNodeMargin - If true, the node margin will not hinder panning
46
- * @note By default, panning is hindered by nodes and relationships.
47
- *
48
- * @example
49
- * ```js
50
- * // Pan canvas even when dragging on nodes and relationships
51
- * panInteraction.updateTargets([], true)
52
- * ```
53
- */
54
- Object.defineProperty(this, "updateTargets", {
55
- enumerable: true,
56
- configurable: true,
57
- writable: true,
58
- value: (targets, excludeNodeMargin) => {
59
- this.targets = targets;
60
- this.currentOptions.excludeNodeMargin = excludeNodeMargin;
61
- }
62
- });
63
- Object.defineProperty(this, "handleMouseDown", {
64
- enumerable: true,
65
- configurable: true,
66
- writable: true,
67
- value: (event) => {
68
- const hits = this.nvlInstance.getHits(event, difference(['node', 'relationship'], this.targets), {
69
- hitNodeMarginWidth: this.currentOptions.excludeNodeMargin ? NODE_EDGE_WIDTH : 0
70
- });
71
- if (hits.nvlTargets.nodes.length > 0 || hits.nvlTargets.relationships.length > 0) {
72
- this.shouldPan = false;
73
- }
74
- else {
75
- this.mousePosition = { x: event.clientX, y: event.clientY };
76
- this.shouldPan = true;
77
- }
78
- }
79
- });
80
- Object.defineProperty(this, "handleMouseMove", {
81
- enumerable: true,
82
- configurable: true,
83
- writable: true,
84
- value: (evt) => {
85
- if (!this.shouldPan || evt.buttons !== 1) {
86
- return;
87
- }
88
- const zoom = this.nvlInstance.getScale();
89
- const { x, y } = this.nvlInstance.getPan();
90
- const dx = ((evt.clientX - this.mousePosition.x) / zoom) * window.devicePixelRatio;
91
- const dy = ((evt.clientY - this.mousePosition.y) / zoom) * window.devicePixelRatio;
92
- this.nvlInstance.setPan(x - dx, y - dy);
93
- this.callCallbackIfRegistered('onPan', { x: x - dx, y: y - dy }, evt);
94
- this.mousePosition = { x: evt.clientX, y: evt.clientY };
95
- }
96
- });
97
- Object.defineProperty(this, "handleMouseUp", {
98
- enumerable: true,
99
- configurable: true,
100
- writable: true,
101
- value: () => {
102
- this.shouldPan = false;
103
- }
104
- });
105
27
  this.mousePosition = { x: 0, y: 0 };
106
28
  this.targets = [];
107
29
  this.shouldPan = false;
@@ -109,6 +31,49 @@ export class PanInteraction extends BaseInteraction {
109
31
  this.addEventListener('mousemove', this.handleMouseMove, true);
110
32
  this.addEventListener('mouseup', this.handleMouseUp, true);
111
33
  }
34
+ /**
35
+ * Updates which type of graph elements should hinder panning.
36
+ * @param targets - The graph elements that should hinder panning
37
+ * @param excludeNodeMargin - If true, the node margin will not hinder panning
38
+ * @note By default, panning is hindered by nodes and relationships.
39
+ *
40
+ * @example
41
+ * ```js
42
+ * // Pan canvas even when dragging on nodes and relationships
43
+ * panInteraction.updateTargets([], true)
44
+ * ```
45
+ */
46
+ updateTargets = (targets, excludeNodeMargin) => {
47
+ this.targets = targets;
48
+ this.currentOptions.excludeNodeMargin = excludeNodeMargin;
49
+ };
50
+ handleMouseDown = (event) => {
51
+ const hits = this.nvlInstance.getHits(event, difference(['node', 'relationship'], this.targets), {
52
+ hitNodeMarginWidth: this.currentOptions.excludeNodeMargin ? NODE_EDGE_WIDTH : 0
53
+ });
54
+ if (hits.nvlTargets.nodes.length > 0 || hits.nvlTargets.relationships.length > 0) {
55
+ this.shouldPan = false;
56
+ }
57
+ else {
58
+ this.mousePosition = { x: event.clientX, y: event.clientY };
59
+ this.shouldPan = true;
60
+ }
61
+ };
62
+ handleMouseMove = (evt) => {
63
+ if (!this.shouldPan || evt.buttons !== 1) {
64
+ return;
65
+ }
66
+ const zoom = this.nvlInstance.getScale();
67
+ const { x, y } = this.nvlInstance.getPan();
68
+ const dx = ((evt.clientX - this.mousePosition.x) / zoom) * window.devicePixelRatio;
69
+ const dy = ((evt.clientY - this.mousePosition.y) / zoom) * window.devicePixelRatio;
70
+ this.nvlInstance.setPan(x - dx, y - dy);
71
+ this.callCallbackIfRegistered('onPan', { x: x - dx, y: y - dy }, evt);
72
+ this.mousePosition = { x: evt.clientX, y: evt.clientY };
73
+ };
74
+ handleMouseUp = () => {
75
+ this.shouldPan = false;
76
+ };
112
77
  /**
113
78
  * Removes the related event listeners from the canvas.
114
79
  */
@@ -22,53 +22,38 @@ export class ZoomInteraction extends BaseInteraction {
22
22
  */
23
23
  constructor(nvl, options = {}) {
24
24
  super(nvl, options);
25
- /**
26
- * Throttled zoom function to avoid events happening too fast.
27
- * @param event - The original mouse wheel event
28
- * @note "Wheel" with a touchpad, the wheel is triggered event a lot,
29
- * especially a lot of very small values.
30
- * However, updating values in NVL instance takes time.
31
- * Sometimes it lost the track of multiple events happening too soon.
32
- * As a result, the zoom might lose its anchor point under touch pad.
33
- * Therefore, the throttle is needed to avoid events happening too fast.
34
- * @note The throttle is set to 25ms.
35
- */
36
- Object.defineProperty(this, "throttledZoom", {
37
- enumerable: true,
38
- configurable: true,
39
- writable: true,
40
- value: throttle((event) => {
41
- const zoom = this.nvlInstance.getScale();
42
- const { x, y } = this.nvlInstance.getPan();
43
- const factor = event.deltaY / 500;
44
- const newZoomTarget = zoom - factor * Math.min(1, zoom);
45
- const offs = getCanvasCenterOffset(this.containerInstance, event);
46
- const newPanX = x + (offs.x / zoom - offs.x / newZoomTarget);
47
- const newPanY = y + (offs.y / zoom - offs.y / newZoomTarget);
48
- this.nvlInstance.setZoomAndPan(newZoomTarget, newPanX, newPanY);
49
- this.callCallbackIfRegistered('onZoom', newZoomTarget, event);
50
- }, 25, { leading: true })
51
- });
52
- Object.defineProperty(this, "handleWheel", {
53
- enumerable: true,
54
- configurable: true,
55
- writable: true,
56
- value: (evt) => {
57
- evt.preventDefault();
58
- this.throttledZoom(evt);
59
- }
60
- });
61
- /**
62
- * Removes the relevant event listeners from the canvas.
63
- */
64
- Object.defineProperty(this, "destroy", {
65
- enumerable: true,
66
- configurable: true,
67
- writable: true,
68
- value: () => {
69
- this.removeEventListener('wheel', this.handleWheel);
70
- }
71
- });
72
25
  this.addEventListener('wheel', this.handleWheel);
73
26
  }
27
+ /**
28
+ * Throttled zoom function to avoid events happening too fast.
29
+ * @param event - The original mouse wheel event
30
+ * @note "Wheel" with a touchpad, the wheel is triggered event a lot,
31
+ * especially a lot of very small values.
32
+ * However, updating values in NVL instance takes time.
33
+ * Sometimes it lost the track of multiple events happening too soon.
34
+ * As a result, the zoom might lose its anchor point under touch pad.
35
+ * Therefore, the throttle is needed to avoid events happening too fast.
36
+ * @note The throttle is set to 25ms.
37
+ */
38
+ throttledZoom = throttle((event) => {
39
+ const zoom = this.nvlInstance.getScale();
40
+ const { x, y } = this.nvlInstance.getPan();
41
+ const factor = event.deltaY / 500;
42
+ const newZoomTarget = zoom - factor * Math.min(1, zoom);
43
+ const offs = getCanvasCenterOffset(this.containerInstance, event);
44
+ const newPanX = x + (offs.x / zoom - offs.x / newZoomTarget);
45
+ const newPanY = y + (offs.y / zoom - offs.y / newZoomTarget);
46
+ this.nvlInstance.setZoomAndPan(newZoomTarget, newPanX, newPanY);
47
+ this.callCallbackIfRegistered('onZoom', newZoomTarget, event);
48
+ }, 25, { leading: true });
49
+ handleWheel = (evt) => {
50
+ evt.preventDefault();
51
+ this.throttledZoom(evt);
52
+ };
53
+ /**
54
+ * Removes the relevant event listeners from the canvas.
55
+ */
56
+ destroy = () => {
57
+ this.removeEventListener('wheel', this.handleWheel);
58
+ };
74
59
  }
@@ -1,4 +1,3 @@
1
- import onResize from 'resizelistener';
2
1
  const marquee = {
3
2
  frameWidth: 2,
4
3
  frameColor: '#a9a9a9',
@@ -6,27 +5,11 @@ const marquee = {
6
5
  opacity: 0.5
7
6
  };
8
7
  export class OverlayRenderer {
8
+ ctx;
9
+ canvas;
10
+ removeResizeListener;
9
11
  constructor(canvasParent) {
10
- Object.defineProperty(this, "ctx", {
11
- enumerable: true,
12
- configurable: true,
13
- writable: true,
14
- value: void 0
15
- });
16
- Object.defineProperty(this, "canvas", {
17
- enumerable: true,
18
- configurable: true,
19
- writable: true,
20
- value: void 0
21
- });
22
- Object.defineProperty(this, "removeResizeListener", {
23
- enumerable: true,
24
- configurable: true,
25
- writable: true,
26
- value: void 0
27
- });
28
12
  const canvas = document.createElement('canvas');
29
- canvas.id = 'OVERLAY';
30
13
  canvas.style.position = 'absolute';
31
14
  canvas.style.top = '0';
32
15
  canvas.style.bottom = '0';
@@ -38,13 +21,18 @@ export class OverlayRenderer {
38
21
  const context = canvas.getContext('2d');
39
22
  this.ctx = context;
40
23
  this.canvas = canvas;
41
- this.removeResizeListener = onResize(canvasParent, () => {
24
+ const handleResize = () => {
42
25
  this.fixCanvasSize(canvas);
43
- });
26
+ };
27
+ canvasParent.addEventListener('resize', handleResize);
28
+ this.removeResizeListener = () => canvasParent.removeEventListener('resize', handleResize);
44
29
  this.fixCanvasSize(canvas);
45
30
  }
46
31
  fixCanvasSize(canvas) {
47
32
  const parent = canvas.parentElement;
33
+ if (!parent) {
34
+ return;
35
+ }
48
36
  const rect = parent.getBoundingClientRect();
49
37
  const { width } = rect;
50
38
  const { height } = rect;
@@ -80,14 +68,16 @@ export class OverlayRenderer {
80
68
  ctx.save();
81
69
  this.clear();
82
70
  ctx.beginPath();
83
- for (let i = 0; i < points.length; i++) {
84
- const { x, y } = points[i];
71
+ let i = 0;
72
+ for (const point of points) {
73
+ const { x, y } = point;
85
74
  if (i === 0) {
86
75
  ctx.moveTo(x, y);
87
76
  }
88
77
  else {
89
78
  ctx.lineTo(x, y);
90
79
  }
80
+ i += 1;
91
81
  }
92
82
  const devicePixelRatio = window.devicePixelRatio || 1;
93
83
  ctx.strokeStyle = marquee.frameColor;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neo4j-nvl/interaction-handlers",
3
- "version": "0.2.53",
3
+ "version": "0.2.55",
4
4
  "license": "SEE LICENSE IN 'Neo4j Early Access Agreement - Visualization Library.pdf'",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -26,12 +26,12 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "concaveman": "^1.2.1",
29
- "lodash": "^4.17.21",
30
- "resizelistener": "^1.1.0"
29
+ "lodash": "4.17.21"
31
30
  },
32
31
  "devDependencies": {
33
32
  "@testing-library/jest-dom": "^5.16.5",
34
33
  "@testing-library/react": "^13.4.0",
35
- "@types/lodash": "^4.14.184"
34
+ "@types/concaveman": "1.1.6",
35
+ "@types/lodash": "4.14.202"
36
36
  }
37
37
  }