@truedat/lm 5.13.1 → 5.13.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/lm",
3
- "version": "5.13.1",
3
+ "version": "5.13.3",
4
4
  "description": "Truedat Link Manager",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -86,7 +86,7 @@
86
86
  ]
87
87
  },
88
88
  "dependencies": {
89
- "@truedat/core": "5.13.1",
89
+ "@truedat/core": "5.13.3",
90
90
  "path-to-regexp": "^1.7.0",
91
91
  "prop-types": "^15.8.1",
92
92
  "react-graph-vis": "1.0.6",
@@ -107,5 +107,5 @@
107
107
  "react-dom": ">= 16.8.6 < 17",
108
108
  "semantic-ui-react": ">= 2.0.3 < 2.2"
109
109
  },
110
- "gitHead": "59eb6f428c3489a2ae689381d2f239378284be8c"
110
+ "gitHead": "93c1576f82fedd1d9b607cd0c9053279763877bc"
111
111
  }
@@ -8,7 +8,7 @@ import { FormattedMessage } from "react-intl";
8
8
 
9
9
  export const RelationGraphDepth = ({ onChange, depth, maxDepth }) => {
10
10
  return _.isUndefined(maxDepth) ? null : (
11
- <Segment className="graph-depth">
11
+ <Segment className={`graph-depth ${maxDepth == 0 ? "disabled" : ""}`}>
12
12
  <FormattedMessage id="relationGraph.depth" />
13
13
  <div>{depth}</div>
14
14
  <Slider
@@ -16,7 +16,7 @@ export const RelationGraphDepth = ({ onChange, depth, maxDepth }) => {
16
16
  orientation="vertical"
17
17
  reverse
18
18
  onChange={onChange}
19
- min={1}
19
+ min={0}
20
20
  max={maxDepth}
21
21
  tooltip={false}
22
22
  />
@@ -80,9 +80,12 @@ describe("<ImplementationRelationForm />", () => {
80
80
  renderOpts
81
81
  );
82
82
 
83
- await waitFor(() => {
84
- expect(getByText(/relates_to/)).toBeInTheDocument();
85
- });
83
+ await waitFor(
84
+ () => {
85
+ expect(getByText(/relates_to/)).toBeInTheDocument();
86
+ },
87
+ { timeout: 60000 }
88
+ );
86
89
  expect(container).toMatchSnapshot();
87
90
  });
88
91
 
@@ -85,9 +85,12 @@ describe("<StructureRelationForm />", () => {
85
85
  renderOpts
86
86
  );
87
87
 
88
- await waitFor(() => {
89
- expect(getByText(/relates_to/)).toBeInTheDocument();
90
- });
88
+ await waitFor(
89
+ () => {
90
+ expect(getByText(/relates_to/)).toBeInTheDocument();
91
+ },
92
+ { timeout: 60000 }
93
+ );
91
94
 
92
95
  expect(container).toMatchSnapshot();
93
96
  });
@@ -1,3 +1,3 @@
1
1
  export * from "./getStructureLinks";
2
2
  export * from "./getImplementationToConceptLinks";
3
- export * from "./relationGraphTraversal";
3
+ export * from "../services/relationGraphTraversal";
@@ -0,0 +1,388 @@
1
+ import _ from "lodash/fp";
2
+ import { findMaxDepth, EMPTY, pruneGraph } from "../relationGraphTraversal";
3
+
4
+ const nodes = [
5
+ {
6
+ id: "character:taiga",
7
+ name: "Taiga",
8
+ },
9
+ {
10
+ id: "character:ryuuji",
11
+ name: "Ryuuji",
12
+ },
13
+ {
14
+ id: "character:minori",
15
+ name: "Minori",
16
+ },
17
+ {
18
+ id: "character:ami",
19
+ name: "Ami",
20
+ },
21
+ {
22
+ id: "character:yuusaku",
23
+ name: "Yuusaku",
24
+ },
25
+ {
26
+ id: "character:yasuko",
27
+ name: "Yasuko",
28
+ },
29
+ {
30
+ id: "character:sumire",
31
+ name: "Sumire",
32
+ },
33
+ {
34
+ id: "character:maya",
35
+ name: "Maya",
36
+ },
37
+ {
38
+ id: "character:non_connected",
39
+ name: "unrelated",
40
+ },
41
+ ];
42
+
43
+ const tag_suki = {
44
+ id: "suki",
45
+ value: {
46
+ target_type: "character",
47
+ type: "loves",
48
+ },
49
+ };
50
+
51
+ const tag_shinyuu = {
52
+ id: "shinyuu",
53
+ value: {
54
+ target_type: "character",
55
+ type: "close_friend",
56
+ },
57
+ };
58
+
59
+ const tag_seitokai = {
60
+ id: "seitokai",
61
+ value: {
62
+ target_type: "character",
63
+ type: "student_council",
64
+ },
65
+ };
66
+
67
+ const tag_sonkei = {
68
+ id: "sonkei",
69
+ value: {
70
+ target_type: "character",
71
+ type: "respect",
72
+ },
73
+ };
74
+
75
+ const tag_haha = {
76
+ id: "haha",
77
+ value: {
78
+ target_type: "character",
79
+ type: "mother",
80
+ },
81
+ };
82
+
83
+ const tag_musuko = {
84
+ id: "musuko",
85
+ value: {
86
+ target_type: "character",
87
+ type: "son",
88
+ },
89
+ };
90
+
91
+ const tag_osananajimi = {
92
+ id: "osananajimi",
93
+ value: {
94
+ target_type: "character",
95
+ type: "childhood_friend",
96
+ },
97
+ };
98
+
99
+ const edges = [
100
+ {
101
+ id: "taiga_to_yuusaku",
102
+ source_id: "character:taiga",
103
+ target_id: "character:yuusaku",
104
+ tags: [tag_suki],
105
+ },
106
+ {
107
+ id: "yuusaku_to_taiga",
108
+ source_id: "character:yuusaku",
109
+ target_id: "character:taiga",
110
+ tags: [],
111
+ },
112
+ {
113
+ id: "taiga_to_minori",
114
+ source_id: "character:taiga",
115
+ target_id: "character:minori",
116
+ tags: [tag_shinyuu],
117
+ },
118
+ {
119
+ id: "minori_to_taiga",
120
+ source_id: "character:minori",
121
+ target_id: "character:taiga",
122
+ tags: [tag_shinyuu],
123
+ },
124
+ {
125
+ id: "taiga_to_ryuuji",
126
+ source_id: "character:taiga",
127
+ target_id: "character:ryuuji",
128
+ tags: [],
129
+ },
130
+ {
131
+ id: "ryuuji_to_taiga",
132
+ source_id: "character:ryuuji",
133
+ target_id: "character:taiga",
134
+ tags: [],
135
+ },
136
+ {
137
+ id: "taiga_to_ami",
138
+ source_id: "character:taiga",
139
+ target_id: "character:ami",
140
+ tags: [],
141
+ },
142
+ {
143
+ id: "ami_to_taiga",
144
+ source_id: "character:ami",
145
+ target_id: "character:taiga",
146
+ tags: [],
147
+ },
148
+ {
149
+ id: "ryuuji_to_minori",
150
+ source_id: "character:ryuuji",
151
+ target_id: "character:minori",
152
+ tags: [tag_suki],
153
+ },
154
+ {
155
+ id: "minori_to_ryuuji",
156
+ source_id: "character:minori",
157
+ target_id: "character:ryuuji",
158
+ tags: [],
159
+ },
160
+ {
161
+ id: "ryuuji_to_yuusaku",
162
+ source_id: "character:ryuuji",
163
+ target_id: "character:yuusaku",
164
+ tags: [tag_shinyuu],
165
+ },
166
+ {
167
+ id: "yuusaku_to_ryuuji",
168
+ source_id: "character:yuusaku",
169
+ target_id: "character:ryuuji",
170
+ tags: [tag_shinyuu],
171
+ },
172
+ {
173
+ id: "ryuuji_to_yasuko",
174
+ source_id: "character:ryuuji",
175
+ target_id: "character:yasuko",
176
+ tags: [tag_haha],
177
+ },
178
+ {
179
+ id: "yasuko_to_ryuuji",
180
+ source_id: "character:yasuko",
181
+ target_id: "character:ryuuji",
182
+ tags: [tag_musuko],
183
+ },
184
+ {
185
+ id: "yuusaku_to_sumire",
186
+ source_id: "character:yuusaku",
187
+ target_id: "character:sumire",
188
+ tags: [tag_seitokai, tag_sonkei],
189
+ },
190
+ {
191
+ id: "sumire_to_yuusaku",
192
+ source_id: "character:sumire",
193
+ target_id: "character:yuusaku",
194
+ tags: [tag_seitokai],
195
+ },
196
+ {
197
+ id: "yuusaku_to_maya",
198
+ source_id: "character:yuusaku",
199
+ target_id: "character:maya",
200
+ tags: [],
201
+ },
202
+ {
203
+ id: "maya_to_yuusaku",
204
+ source_id: "character:maya",
205
+ target_id: "character:yuusaku",
206
+ tags: [tag_suki],
207
+ },
208
+ {
209
+ id: "yuusaku_to_ami",
210
+ source_id: "character:yuusaku",
211
+ target_id: "character:ami",
212
+ tags: [tag_osananajimi],
213
+ },
214
+ {
215
+ id: "ami_to_yuusaku",
216
+ source_id: "character:ami",
217
+ target_id: "character:yuusaku",
218
+ tags: [tag_osananajimi],
219
+ },
220
+ ];
221
+
222
+ const relationsGraph = {
223
+ nodes,
224
+ edges,
225
+ };
226
+
227
+ describe("relationGraphTraversal", () => {
228
+ const traversalTags = [];
229
+
230
+ it("get graph max depth, starting from taiga, no limitations (all tags)", () => {
231
+ const currentId = "character:taiga";
232
+ expect(findMaxDepth(relationsGraph, currentId, traversalTags)).toBe(2);
233
+ });
234
+
235
+ it("get graph max depth, starting from ami, no limitations (all tags)", () => {
236
+ const currentId = "character:ami";
237
+ expect(findMaxDepth(relationsGraph, currentId, traversalTags)).toBe(3);
238
+ });
239
+
240
+ it("empty input graph", () => {
241
+ const currentId = "character:taiga";
242
+ const limitedGraph = pruneGraph({}, currentId, 3, traversalTags);
243
+ expect(limitedGraph).toStrictEqual({});
244
+ });
245
+
246
+ it("get graph, no limitations (max depth, all tags)", () => {
247
+ const currentId = "character:taiga";
248
+ const limitedGraph = pruneGraph(
249
+ relationsGraph,
250
+ currentId,
251
+ 2,
252
+ traversalTags
253
+ );
254
+ const { nodes: outputNodes, edges: outputEdges } = limitedGraph;
255
+
256
+ const inputNodeIds = _.map((node) => node.id, nodes);
257
+ const outputNodeIds = _.map((node) => node.id, outputNodes);
258
+
259
+ // excludes non connected nodes
260
+ expect(_.difference(inputNodeIds, outputNodeIds)).toStrictEqual([
261
+ "character:non_connected",
262
+ ]);
263
+
264
+ const inputEdgeIds = _.map((edge) => edge.id, edges);
265
+ const outputEdgeIds = _.map((edge) => edge.id, outputEdges);
266
+
267
+ expect(_.difference(inputEdgeIds, outputEdgeIds)).toStrictEqual([]);
268
+ });
269
+
270
+ it("get graph, limit depth to 1, all tags", () => {
271
+ const currentId = "character:taiga";
272
+ const limitedGraph = pruneGraph(
273
+ relationsGraph,
274
+ currentId,
275
+ 1,
276
+ traversalTags
277
+ );
278
+ const { nodes: outputNodes, edges: outputEdges } = limitedGraph;
279
+ const outputNodeIds = new Set(_.map((node) => node.id, outputNodes));
280
+ const expectedNodeIds = new Set([
281
+ "character:taiga",
282
+ "character:ryuuji",
283
+ "character:minori",
284
+ "character:ami",
285
+ "character:yuusaku",
286
+ ]);
287
+
288
+ expect(_.isEqual(outputNodeIds, expectedNodeIds)).toBeTruthy();
289
+
290
+ const outputEdgeIds = new Set(_.map((edge) => edge.id, outputEdges));
291
+ const expectedEdgeIds = new Set([
292
+ // Directly reachable edges
293
+ "taiga_to_yuusaku",
294
+ "yuusaku_to_taiga",
295
+ "taiga_to_minori",
296
+ "minori_to_taiga",
297
+ "taiga_to_ryuuji",
298
+ "ryuuji_to_taiga",
299
+ "taiga_to_ami",
300
+ "ami_to_taiga",
301
+ /* Indirectly reachable edges
302
+ * (are shown even though depth first search has not actually traversed
303
+ * through them, to avoid possible user confusion thinking they
304
+ * do not exist).
305
+ */
306
+ "ryuuji_to_minori",
307
+ "minori_to_ryuuji",
308
+ "ami_to_yuusaku",
309
+ "yuusaku_to_ami",
310
+ "ryuuji_to_yuusaku",
311
+ "yuusaku_to_ryuuji",
312
+ ]);
313
+
314
+ expect(_.isEqual(outputEdgeIds, expectedEdgeIds)).toBeTruthy();
315
+ });
316
+
317
+ it("get graph, limit depth to 2, with traversal tags", () => {
318
+ const currentId = "character:taiga";
319
+ const limitedGraph = pruneGraph(relationsGraph, currentId, 2, [
320
+ "suki",
321
+ "sonkei",
322
+ ]);
323
+ const { nodes: outputNodes, edges: outputEdges } = limitedGraph;
324
+ const outputNodeIds = new Set(_.map((node) => node.id, outputNodes));
325
+ const expectedNodeIds = new Set([
326
+ "character:taiga",
327
+ "character:yuusaku",
328
+ "character:sumire",
329
+ "character:maya",
330
+ ]);
331
+
332
+ expect(_.isEqual(outputNodeIds, expectedNodeIds)).toBeTruthy();
333
+
334
+ const outputEdgeIds = new Set(_.map((edge) => edge.id, outputEdges));
335
+ const expectedEdgeIds = new Set([
336
+ "taiga_to_yuusaku",
337
+ "yuusaku_to_sumire",
338
+ "maya_to_yuusaku",
339
+ ]);
340
+
341
+ expect(_.isEqual(outputEdgeIds, expectedEdgeIds)).toBeTruthy();
342
+ });
343
+
344
+ it("EMPTY tag", () => {
345
+ const currentId = "character:taiga";
346
+ const limitedGraph = pruneGraph(relationsGraph, currentId, 2, [EMPTY]);
347
+ const { nodes: outputNodes, edges: outputEdges } = limitedGraph;
348
+ const outputNodeIds = new Set(_.map((node) => node.id, outputNodes));
349
+ const expectedNodeIds = new Set([
350
+ "character:taiga",
351
+ "character:ryuuji",
352
+ "character:ami",
353
+ "character:maya",
354
+ "character:minori",
355
+ "character:yuusaku",
356
+ ]);
357
+
358
+ expect(_.isEqual(outputNodeIds, expectedNodeIds)).toBeTruthy();
359
+
360
+ const outputEdgeIds = new Set(_.map((edge) => edge.id, outputEdges));
361
+ const expectedEdgeIds = new Set([
362
+ "taiga_to_ryuuji",
363
+ "ryuuji_to_taiga",
364
+ "taiga_to_ami",
365
+ "ami_to_taiga",
366
+ "yuusaku_to_taiga",
367
+ "minori_to_ryuuji",
368
+ "yuusaku_to_maya",
369
+ ]);
370
+
371
+ expect(_.isEqual(outputEdgeIds, expectedEdgeIds)).toBeTruthy();
372
+ });
373
+
374
+ it("unreachable tag", () => {
375
+ const currentId = "character:taiga";
376
+ const limitedGraph = pruneGraph(relationsGraph, currentId, 2, ["sonkei"]);
377
+ const { nodes: outputNodes, edges: outputEdges } = limitedGraph;
378
+ const outputNodeIds = new Set(_.map((node) => node.id, outputNodes));
379
+ const expectedNodeIds = new Set(["character:taiga"]);
380
+
381
+ expect(_.isEqual(outputNodeIds, expectedNodeIds)).toBeTruthy();
382
+
383
+ const outputEdgeIds = new Set(_.map((edge) => edge.id, outputEdges));
384
+ const expectedEdgeIds = new Set([]);
385
+
386
+ expect(_.isEqual(outputEdgeIds, expectedEdgeIds)).toBeTruthy();
387
+ });
388
+ });
@@ -1,31 +1,117 @@
1
+ /* eslint-disable fp/no-mutation */
2
+ /* eslint-disable fp/no-let */
1
3
  import _ from "lodash/fp";
4
+ import { defaultMemoize } from "reselect";
2
5
 
3
- const getNeighbours = (relationsGraph, start) => {
4
- return _.concat(
5
- // Up
6
- _.flow(
7
- _.filter((edge) => edge.target_id === start.id),
8
- _.flatMap((edge) => _.find({ id: edge.source_id }, relationsGraph.nodes))
9
- )(relationsGraph.edges),
10
- // Down
11
- _.flow(
12
- _.filter((edge) => edge.source_id === start.id),
13
- _.flatMap((edge) => _.find({ id: edge.target_id }, relationsGraph.nodes))
14
- )(relationsGraph.edges)
6
+ export const EMPTY = "_EMPTY";
7
+
8
+ const currentIdToNode = (graph, currentId) =>
9
+ _.flow(_.getOr([], "nodes"), _.find({ id: currentId }))(graph) || {};
10
+
11
+ const equalityCheck = (previousVal, currentVal) =>
12
+ _.isArray(previousVal) && _.isArray(currentVal) // traversalTags uses arrays
13
+ ? _.isEqual(previousVal.sort(), currentVal.sort())
14
+ : previousVal === currentVal;
15
+
16
+ export const findMaxDepth = defaultMemoize(
17
+ (graph, currentId, traversalTags) => {
18
+ const start = currentIdToNode(graph, currentId);
19
+ return bfsMaxDepth(graph, start, traversalTags);
20
+ },
21
+ { maxSize: 10, equalityCheck }
22
+ );
23
+
24
+ export const pruneGraph = defaultMemoize(
25
+ (graph, currentId, depth, traversalTags) => {
26
+ const start = currentIdToNode(graph, currentId);
27
+ const maxDepth = findMaxDepth(graph, currentId, traversalTags);
28
+
29
+ if (!_.isEmpty(graph) && !_.isEmpty(start)) {
30
+ const nodes = dfs(
31
+ graph,
32
+ start,
33
+ {},
34
+ depth < maxDepth ? depth : maxDepth,
35
+ traversalTags
36
+ );
37
+ return {
38
+ nodes,
39
+ /* This will show both directly and indirectly reachable edges
40
+ * Indirectly reachable edges are shown even though depth first search
41
+ * has not actually traversed through them. Could avoid by accumulating
42
+ * edges while traversing. However, I prefer to display them to avoid
43
+ * possible user confusion thinking they do not exist.
44
+ * See test for more details
45
+ */
46
+ edges: filterEdges(graph, nodes, traversalTags),
47
+ };
48
+ } else {
49
+ return graph;
50
+ }
51
+ },
52
+ { maxSize: 10, equalityCheck }
53
+ );
54
+
55
+ export const anyTraversalTags = (traversalTagIds, tags) => {
56
+ const tagIds = _.map((tag) => tag.id, tags);
57
+ return (
58
+ _.isEmpty(traversalTagIds) || // If no tags specified, any tag is accepted
59
+ (traversalTagIds.includes(EMPTY) && _.isEmpty(tagIds)) ||
60
+ !_.isEmpty(_.intersection(traversalTagIds, tagIds))
61
+ );
62
+ };
63
+
64
+ const findNeighbours = (relationsGraph, start, traversalTags) => {
65
+ const inNeighbours = _.flow(
66
+ _.filter(
67
+ (edge) =>
68
+ edge.target_id === start.id &&
69
+ anyTraversalTags(traversalTags, edge.tags)
70
+ ),
71
+ _.flatMap((edge) => _.find({ id: edge.source_id }, relationsGraph.nodes))
72
+ );
73
+ const outNeighbours = _.flow(
74
+ _.filter(
75
+ (edge) =>
76
+ edge.source_id === start.id &&
77
+ anyTraversalTags(traversalTags, edge.tags)
78
+ ),
79
+ _.flatMap((edge) => _.find({ id: edge.target_id }, relationsGraph.nodes))
15
80
  );
81
+
82
+ return _.flow(
83
+ (edges) => [...inNeighbours(edges), ...outNeighbours(edges)],
84
+ _.uniq
85
+ )(relationsGraph.edges);
16
86
  };
17
87
 
18
- export const getEdges = (relationsGraph, nodes) => {
88
+ export const filterEdges = (relationsGraph, nodes, traversalTags) => {
19
89
  const nodeIds = Object.keys(nodes);
20
- return _.filter(
90
+ const edges = _.filter(
21
91
  (edge) =>
22
- nodeIds.includes(edge.source_id) && nodeIds.includes(edge.target_id),
92
+ nodeIds.includes(edge.source_id) &&
93
+ nodeIds.includes(edge.target_id) &&
94
+ anyTraversalTags(traversalTags, edge.tags),
23
95
  relationsGraph.edges
24
96
  );
97
+ return edges;
25
98
  };
26
99
 
27
- export const dfs = (relationsGraph, start, visited, maxDepth) => {
28
- if (Object.keys(visited).includes(start.id) || maxDepth === 0) return {};
100
+ export const graphDistinctTags = ({ edges }) =>
101
+ _.flow(
102
+ _.flatMap((edge) => edge?.tags),
103
+ _.uniqBy((tag) => tag?.id)
104
+ )(edges);
105
+
106
+ export const dfs = (
107
+ relationsGraph,
108
+ start,
109
+ visited,
110
+ maxDepth,
111
+ traversalTags
112
+ ) => {
113
+ // Depth range: [0, maxDepth]
114
+ if (Object.keys(visited).includes(start.id) || maxDepth === -1) return {};
29
115
 
30
116
  return _.reduce(
31
117
  (acc, neighbour) => ({
@@ -34,27 +120,41 @@ export const dfs = (relationsGraph, start, visited, maxDepth) => {
34
120
  relationsGraph,
35
121
  neighbour,
36
122
  { ...visited, [start.id]: start },
37
- maxDepth - 1
123
+ maxDepth - 1,
124
+ traversalTags
38
125
  ),
39
126
  }),
40
127
  { [start.id]: start }
41
- )(getNeighbours(relationsGraph, start));
128
+ )(findNeighbours(relationsGraph, start, traversalTags));
42
129
  };
43
130
 
44
- export const dfsMaxDepth = (relationsGraph, start, visited, currDepth) => {
45
- if (Object.keys(visited).includes(start.id)) return currDepth;
131
+ /*
132
+ * Shortest Path (Unweighted Graph)
133
+ * If the graph is unweighed, then finding the shortest path is easy: we can
134
+ * use the breadth-first search algorithm. For a weighted graph, we can use Dijkstra's algorithm.
135
+ * https://aquarchitect.github.io/swift-algorithm-club/Shortest%20Path%20%28Unweighted%29/
136
+ */
137
+ const bfsMaxDepth = (relationsGraph, start, traversalTags) => {
138
+ let queue = [];
139
+ let nodesDistance = {};
46
140
 
47
- return _.reduce(
48
- (currMax, neighbour) =>
49
- Math.max(
50
- currMax,
51
- dfsMaxDepth(
52
- relationsGraph,
53
- neighbour,
54
- { ...visited, [start.id]: start },
55
- currDepth + 1
56
- )
57
- ),
58
- 0
59
- )(getNeighbours(relationsGraph, start));
141
+ queue.push(start);
142
+ nodesDistance[start.id] = 0; // Depth range: [0, maxDepth]
143
+
144
+ let current = start;
145
+ while (!_.isEmpty(current)) {
146
+ current = queue.shift() || {};
147
+ for (let neighbour of findNeighbours(
148
+ relationsGraph,
149
+ current,
150
+ traversalTags
151
+ )) {
152
+ if (!(neighbour.id in nodesDistance)) {
153
+ queue.push(neighbour);
154
+ nodesDistance[neighbour.id] = nodesDistance[current.id] + 1;
155
+ }
156
+ }
157
+ }
158
+
159
+ return Math.max(...Object.values(nodesDistance));
60
160
  };
@@ -2,20 +2,30 @@
2
2
  position: absolute !important;
3
3
  z-index: 1;
4
4
  margin-right: 0px;
5
+ right: 14px;
5
6
 
6
7
  div {
7
8
  text-align: center;
8
9
  font-weight: bold;
9
10
  }
10
- }
11
11
 
12
- .graph-depth .rangeslider-vertical {
13
- .rangeslider__fill {
14
- background-color: #ed5c17;
15
- border: 1px solid gray;
12
+ &.disabled {
13
+ pointer-events: none;
14
+ opacity: 0.7;
15
+
16
+ .rangeslider-vertical .rangeslider__fill {
17
+ background-color: gray;
18
+ }
16
19
  }
17
20
 
18
- .rangeslider__handle {
19
- border-radius: 4px;
21
+ .rangeslider-vertical {
22
+ .rangeslider__fill {
23
+ background-color: #ed5c17;
24
+ border: 1px solid gray;
25
+ }
26
+
27
+ .rangeslider__handle {
28
+ border-radius: 4px;
29
+ }
20
30
  }
21
31
  }
@@ -1,188 +0,0 @@
1
- import _ from "lodash/fp";
2
- import { selectLimitedGraph } from "..";
3
- import { selectMaxDepth } from "../relationGraphTraversal";
4
-
5
- const nodes = [
6
- {
7
- id: "business_concept:92",
8
- name: "My Awesome Concept",
9
- },
10
- {
11
- id: "business_concept:5",
12
- name: "New Concept 4",
13
- },
14
- {
15
- id: "business_concept:18",
16
- name: "Float Concept",
17
- },
18
- ];
19
-
20
- const edges = [
21
- {
22
- id: 1,
23
- source_id: "business_concept:92",
24
- target_id: "business_concept:5",
25
- tags: [
26
- {
27
- id: 3,
28
- value: {
29
- target_type: "business_concept",
30
- type: "bc_padre",
31
- },
32
- },
33
- ],
34
- },
35
- {
36
- id: 2,
37
- source_id: "business_concept:5",
38
- target_id: "business_concept:18",
39
- tags: [
40
- {
41
- id: 3,
42
- value: {
43
- target_type: "business_concept",
44
- type: "bc_padre",
45
- },
46
- },
47
- ],
48
- },
49
- {
50
- id: 3,
51
- source_id: "business_concept:18",
52
- target_id: "business_concept:5",
53
- tags: [
54
- {
55
- id: 3,
56
- value: {
57
- target_type: "business_concept",
58
- type: "bc_padre",
59
- },
60
- },
61
- ],
62
- },
63
- ];
64
-
65
- const relationsGraph = {
66
- nodes,
67
- edges,
68
- };
69
-
70
- describe("selectors: relationGraphTraversal", () => {
71
- const currentId = "business_concept:92";
72
-
73
- it("gets Graph on non-empty relationsGraph", () => {
74
- const limitedGraph = selectLimitedGraph({ relationsGraph }, currentId, 3);
75
-
76
- expect(
77
- _.isUndefined(
78
- _.find({ id: nodes[0].id, name: nodes[0].name }, limitedGraph.nodes)
79
- ) &&
80
- _.isUndefined(
81
- _.find({ id: nodes[1].id, name: nodes[1].name }, limitedGraph.nodes)
82
- ) &&
83
- _.isUndefined(
84
- _.find({ id: nodes[2].id, name: nodes[2].name }, limitedGraph.nodes)
85
- )
86
- ).toBeFalsy();
87
-
88
- expect(
89
- _.isUndefined(
90
- _.find(
91
- {
92
- source_id: edges[0].source_id,
93
- target_id: edges[0].target_id,
94
- },
95
- limitedGraph.edges
96
- )
97
- ) &&
98
- _.isUndefined(
99
- _.find(
100
- {
101
- source_id: edges[1].source_id,
102
- target_id: edges[1].target_id,
103
- },
104
- limitedGraph.edges
105
- )
106
- ) &&
107
- _.isUndefined(
108
- _.find(
109
- {
110
- source_id: edges[2].source_id,
111
- target_id: edges[2].target_id,
112
- },
113
- limitedGraph.edges
114
- )
115
- )
116
- ).toBeFalsy();
117
- });
118
-
119
- it("gets Graph on empty relationsGraph", () => {
120
- const limitedGraph = selectLimitedGraph(
121
- { relationsGraph: {} },
122
- currentId,
123
- 3
124
- );
125
- expect(limitedGraph).toStrictEqual({});
126
- });
127
-
128
- it("gets depth limited graph", () => {
129
- const limitedGraph = selectLimitedGraph({ relationsGraph }, currentId, 2);
130
-
131
- expect(
132
- _.isUndefined(
133
- _.find({ id: nodes[0].id, name: nodes[0].name }, limitedGraph.nodes)
134
- ) &&
135
- _.isUndefined(
136
- _.find({ id: nodes[1].id, name: nodes[1].name }, limitedGraph.nodes)
137
- )
138
- ).toBeFalsy();
139
-
140
- expect(
141
- _.isUndefined(
142
- _.find({ id: nodes[2].id, name: nodes[2].name }, limitedGraph.nodes)
143
- )
144
- ).toBeTruthy();
145
-
146
- expect(
147
- _.isUndefined(
148
- _.find(
149
- {
150
- source_id: edges[0].source_id,
151
- target_id: edges[0].target_id,
152
- },
153
- limitedGraph.edges
154
- )
155
- )
156
- ).toBeFalsy();
157
-
158
- expect(
159
- _.isUndefined(
160
- _.find(
161
- {
162
- source_id: edges[1].source_id,
163
- target_id: edges[1].target_id,
164
- },
165
- limitedGraph.edges
166
- )
167
- ) &&
168
- _.isUndefined(
169
- _.find(
170
- {
171
- source_id: edges[2].source_id,
172
- target_id: edges[2].target_id,
173
- },
174
- limitedGraph.edges
175
- )
176
- )
177
- ).toBeTruthy();
178
- });
179
-
180
- it("get graph max depth", () => {
181
- const start = {
182
- id: "business_concept:92",
183
- name: "My Awesome Concept",
184
- };
185
-
186
- expect(selectMaxDepth({ relationsGraph }, currentId, start)).toBe(3);
187
- });
188
- });
@@ -1,113 +0,0 @@
1
- /* eslint-disable fp/no-mutation */
2
- /* eslint-disable fp/no-let */
3
- import _ from "lodash/fp";
4
- import { createSelector } from "reselect";
5
-
6
- const selectStart = createSelector(
7
- (state) => state.relationsGraph,
8
- (state, currentId) => currentId,
9
- (relationsGraph, currentId) => {
10
- return (
11
- _.flow(_.getOr([], "nodes"), _.find({ id: currentId }))(relationsGraph) ||
12
- {}
13
- );
14
- }
15
- );
16
-
17
- export const selectMaxDepth = createSelector(
18
- (state) => state.relationsGraph,
19
- (state, currentId) => selectStart(state, currentId),
20
- (relationsGraph, start) => bfsMaxDepth(relationsGraph, start)
21
- );
22
-
23
- export const selectLimitedGraph = createSelector(
24
- (state) => state.relationsGraph,
25
- (state, currentId) => selectStart(state, currentId),
26
- (state, currentId) => selectMaxDepth(state, currentId),
27
- (state, currentId, depth) => depth,
28
- (relationsGraph, start, maxDepth, depth) =>
29
- getLimitedGraph(relationsGraph, start, maxDepth, depth)
30
- );
31
-
32
- const getLimitedGraph = (relationsGraph, start, maxDepth, depth) => {
33
- if (!_.isEmpty(relationsGraph) && !_.isEmpty(start) && depth !== Infinity) {
34
- const nodes = dfs(relationsGraph, start, {}, depth);
35
- return depth < maxDepth
36
- ? {
37
- nodes,
38
- edges: getEdges(relationsGraph, nodes),
39
- }
40
- : relationsGraph;
41
- } else {
42
- return relationsGraph;
43
- }
44
- };
45
-
46
- const getNeighbours = (relationsGraph, start) => {
47
- const neighbours = _.concat(
48
- // Up
49
- _.flow(
50
- _.filter((edge) => edge.target_id === start.id),
51
- _.flatMap((edge) => _.find({ id: edge.source_id }, relationsGraph.nodes))
52
- )(relationsGraph.edges),
53
- // Down
54
- _.flow(
55
- _.filter((edge) => edge.source_id === start.id),
56
- _.flatMap((edge) => _.find({ id: edge.target_id }, relationsGraph.nodes))
57
- )(relationsGraph.edges)
58
- );
59
- return neighbours;
60
- };
61
-
62
- export const getEdges = (relationsGraph, nodes) => {
63
- const nodeIds = Object.keys(nodes);
64
- return _.filter(
65
- (edge) =>
66
- nodeIds.includes(edge.source_id) && nodeIds.includes(edge.target_id),
67
- relationsGraph.edges
68
- );
69
- };
70
-
71
- export const dfs = (relationsGraph, start, visited, maxDepth) => {
72
- if (Object.keys(visited).includes(start.id) || maxDepth === 0) return {};
73
-
74
- return _.reduce(
75
- (acc, neighbour) => ({
76
- ...acc,
77
- ...dfs(
78
- relationsGraph,
79
- neighbour,
80
- { ...visited, [start.id]: start },
81
- maxDepth - 1
82
- ),
83
- }),
84
- { [start.id]: start }
85
- )(getNeighbours(relationsGraph, start));
86
- };
87
-
88
- /*
89
- * Shortest Path (Unweighted Graph)
90
- * If the graph is unweighed, then finding the shortest path is easy: we can
91
- * use the breadth-first search algorithm. For a weighted graph, we can use Dijkstra's algorithm.
92
- * https://aquarchitect.github.io/swift-algorithm-club/Shortest%20Path%20%28Unweighted%29/
93
- */
94
- const bfsMaxDepth = (relationsGraph, start) => {
95
- let queue = [];
96
- let nodesDistance = {};
97
-
98
- queue.push(start);
99
- nodesDistance[start.id] = 1;
100
-
101
- let current = start;
102
- while (!_.isEmpty(current)) {
103
- current = queue.shift() || {};
104
- for (let neighbour of getNeighbours(relationsGraph, current)) {
105
- if (!nodesDistance[neighbour.id]) {
106
- queue.push(neighbour);
107
- nodesDistance[neighbour.id] = nodesDistance[current.id] + 1;
108
- }
109
- }
110
- }
111
-
112
- return Math.max(...Object.values(nodesDistance));
113
- };