neo4j-viz 0.2.5__py3-none-any.whl → 0.3.0__py3-none-any.whl
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.
- neo4j_viz/colors.py +282 -1
- neo4j_viz/gds.py +11 -2
- neo4j_viz/gql_create.py +356 -0
- neo4j_viz/neo4j.py +44 -49
- neo4j_viz/node.py +7 -1
- neo4j_viz/nvl.py +64 -5
- neo4j_viz/pandas.py +53 -19
- neo4j_viz/relationship.py +7 -1
- neo4j_viz/resources/icons/screenshot.svg +3 -0
- neo4j_viz/resources/icons/zoom-in.svg +3 -0
- neo4j_viz/resources/icons/zoom-out.svg +3 -0
- neo4j_viz/resources/nvl_entrypoint/base.js +1 -1
- neo4j_viz/resources/nvl_entrypoint/styles.css +53 -0
- neo4j_viz/visualization_graph.py +127 -31
- {neo4j_viz-0.2.5.dist-info → neo4j_viz-0.3.0.dist-info}/METADATA +13 -10
- neo4j_viz-0.3.0.dist-info/RECORD +22 -0
- {neo4j_viz-0.2.5.dist-info → neo4j_viz-0.3.0.dist-info}/WHEEL +1 -1
- neo4j_viz-0.2.5.dist-info/RECORD +0 -17
- {neo4j_viz-0.2.5.dist-info → neo4j_viz-0.3.0.dist-info}/top_level.txt +0 -0
neo4j_viz/colors.py
CHANGED
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
from collections.abc import Iterable
|
|
2
|
+
from enum import Enum
|
|
2
3
|
from typing import Any, Union
|
|
3
4
|
|
|
5
|
+
import enum_tools
|
|
4
6
|
from pydantic_extra_types.color import ColorType
|
|
5
7
|
|
|
6
8
|
ColorsType = Union[dict[Any, ColorType], Iterable[ColorType]]
|
|
7
9
|
|
|
10
|
+
|
|
11
|
+
@enum_tools.documentation.document_enum
|
|
12
|
+
class ColorSpace(Enum):
|
|
13
|
+
"""
|
|
14
|
+
Describes the type of color space used by a color palette.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
DISCRETE = "discrete"
|
|
18
|
+
"""
|
|
19
|
+
This category describes a color palette that is a collection of different colors that are not necessarily related to
|
|
20
|
+
each other. Discrete color spaces are suitable for categorical data, where each unique category is represented by a
|
|
21
|
+
different color.
|
|
22
|
+
"""
|
|
23
|
+
CONTINUOUS = "continuous"
|
|
24
|
+
"""
|
|
25
|
+
This category describes a color palette that is a range/gradient of colors between two or more colors. Continuous
|
|
26
|
+
color spaces are suitable for continuous data (typically floats), where values can change smoothly.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
8
30
|
# Comes from https://neo4j.design/40a8cff71/p/5639c0-color/t/page-5639c0-79109681-33
|
|
9
|
-
|
|
31
|
+
NEO4J_COLORS_DISCRETE = [
|
|
10
32
|
"#FFDF81",
|
|
11
33
|
"#C990C0",
|
|
12
34
|
"#F79767",
|
|
@@ -20,3 +42,262 @@ neo4j_colors = [
|
|
|
20
42
|
"#DA7294",
|
|
21
43
|
"#579380",
|
|
22
44
|
]
|
|
45
|
+
_NEO4J_COLORS_CONTINUOUS_BASE = ["#FFDF81", "#C990C0"]
|
|
46
|
+
NEO4J_COLORS_CONTINUOUS = [
|
|
47
|
+
(255, 223, 129),
|
|
48
|
+
(255, 223, 129),
|
|
49
|
+
(255, 222, 129),
|
|
50
|
+
(254, 222, 130),
|
|
51
|
+
(254, 222, 130),
|
|
52
|
+
(254, 221, 130),
|
|
53
|
+
(254, 221, 130),
|
|
54
|
+
(254, 221, 131),
|
|
55
|
+
(253, 221, 131),
|
|
56
|
+
(253, 220, 131),
|
|
57
|
+
(253, 220, 131),
|
|
58
|
+
(253, 220, 132),
|
|
59
|
+
(252, 219, 132),
|
|
60
|
+
(252, 219, 132),
|
|
61
|
+
(252, 219, 132),
|
|
62
|
+
(252, 218, 133),
|
|
63
|
+
(252, 218, 133),
|
|
64
|
+
(251, 218, 133),
|
|
65
|
+
(251, 217, 133),
|
|
66
|
+
(251, 217, 134),
|
|
67
|
+
(251, 217, 134),
|
|
68
|
+
(251, 216, 134),
|
|
69
|
+
(250, 216, 134),
|
|
70
|
+
(250, 216, 135),
|
|
71
|
+
(250, 216, 135),
|
|
72
|
+
(250, 215, 135),
|
|
73
|
+
(249, 215, 135),
|
|
74
|
+
(249, 215, 136),
|
|
75
|
+
(249, 214, 136),
|
|
76
|
+
(249, 214, 136),
|
|
77
|
+
(249, 214, 136),
|
|
78
|
+
(248, 213, 137),
|
|
79
|
+
(248, 213, 137),
|
|
80
|
+
(248, 213, 137),
|
|
81
|
+
(248, 212, 137),
|
|
82
|
+
(248, 212, 138),
|
|
83
|
+
(247, 212, 138),
|
|
84
|
+
(247, 212, 138),
|
|
85
|
+
(247, 211, 138),
|
|
86
|
+
(247, 211, 139),
|
|
87
|
+
(247, 211, 139),
|
|
88
|
+
(246, 210, 139),
|
|
89
|
+
(246, 210, 139),
|
|
90
|
+
(246, 210, 140),
|
|
91
|
+
(246, 209, 140),
|
|
92
|
+
(245, 209, 140),
|
|
93
|
+
(245, 209, 140),
|
|
94
|
+
(245, 208, 141),
|
|
95
|
+
(245, 208, 141),
|
|
96
|
+
(245, 208, 141),
|
|
97
|
+
(244, 208, 141),
|
|
98
|
+
(244, 207, 142),
|
|
99
|
+
(244, 207, 142),
|
|
100
|
+
(244, 207, 142),
|
|
101
|
+
(244, 206, 142),
|
|
102
|
+
(243, 206, 143),
|
|
103
|
+
(243, 206, 143),
|
|
104
|
+
(243, 205, 143),
|
|
105
|
+
(243, 205, 143),
|
|
106
|
+
(243, 205, 144),
|
|
107
|
+
(242, 204, 144),
|
|
108
|
+
(242, 204, 144),
|
|
109
|
+
(242, 204, 144),
|
|
110
|
+
(242, 203, 145),
|
|
111
|
+
(241, 203, 145),
|
|
112
|
+
(241, 203, 145),
|
|
113
|
+
(241, 203, 145),
|
|
114
|
+
(241, 202, 146),
|
|
115
|
+
(241, 202, 146),
|
|
116
|
+
(240, 202, 146),
|
|
117
|
+
(240, 201, 146),
|
|
118
|
+
(240, 201, 147),
|
|
119
|
+
(240, 201, 147),
|
|
120
|
+
(240, 200, 147),
|
|
121
|
+
(239, 200, 147),
|
|
122
|
+
(239, 200, 148),
|
|
123
|
+
(239, 199, 148),
|
|
124
|
+
(239, 199, 148),
|
|
125
|
+
(238, 199, 148),
|
|
126
|
+
(238, 199, 149),
|
|
127
|
+
(238, 198, 149),
|
|
128
|
+
(238, 198, 149),
|
|
129
|
+
(238, 198, 149),
|
|
130
|
+
(237, 197, 150),
|
|
131
|
+
(237, 197, 150),
|
|
132
|
+
(237, 197, 150),
|
|
133
|
+
(237, 196, 150),
|
|
134
|
+
(237, 196, 150),
|
|
135
|
+
(236, 196, 151),
|
|
136
|
+
(236, 195, 151),
|
|
137
|
+
(236, 195, 151),
|
|
138
|
+
(236, 195, 151),
|
|
139
|
+
(236, 194, 152),
|
|
140
|
+
(235, 194, 152),
|
|
141
|
+
(235, 194, 152),
|
|
142
|
+
(235, 194, 152),
|
|
143
|
+
(235, 193, 153),
|
|
144
|
+
(234, 193, 153),
|
|
145
|
+
(234, 193, 153),
|
|
146
|
+
(234, 192, 153),
|
|
147
|
+
(234, 192, 154),
|
|
148
|
+
(234, 192, 154),
|
|
149
|
+
(233, 191, 154),
|
|
150
|
+
(233, 191, 154),
|
|
151
|
+
(233, 191, 155),
|
|
152
|
+
(233, 190, 155),
|
|
153
|
+
(233, 190, 155),
|
|
154
|
+
(232, 190, 155),
|
|
155
|
+
(232, 190, 156),
|
|
156
|
+
(232, 189, 156),
|
|
157
|
+
(232, 189, 156),
|
|
158
|
+
(231, 189, 156),
|
|
159
|
+
(231, 188, 157),
|
|
160
|
+
(231, 188, 157),
|
|
161
|
+
(231, 188, 157),
|
|
162
|
+
(231, 187, 157),
|
|
163
|
+
(230, 187, 158),
|
|
164
|
+
(230, 187, 158),
|
|
165
|
+
(230, 186, 158),
|
|
166
|
+
(230, 186, 158),
|
|
167
|
+
(230, 186, 159),
|
|
168
|
+
(229, 186, 159),
|
|
169
|
+
(229, 185, 159),
|
|
170
|
+
(229, 185, 159),
|
|
171
|
+
(229, 185, 160),
|
|
172
|
+
(229, 184, 160),
|
|
173
|
+
(228, 184, 160),
|
|
174
|
+
(228, 184, 160),
|
|
175
|
+
(228, 183, 161),
|
|
176
|
+
(228, 183, 161),
|
|
177
|
+
(227, 183, 161),
|
|
178
|
+
(227, 182, 161),
|
|
179
|
+
(227, 182, 162),
|
|
180
|
+
(227, 182, 162),
|
|
181
|
+
(227, 181, 162),
|
|
182
|
+
(226, 181, 162),
|
|
183
|
+
(226, 181, 163),
|
|
184
|
+
(226, 181, 163),
|
|
185
|
+
(226, 180, 163),
|
|
186
|
+
(226, 180, 163),
|
|
187
|
+
(225, 180, 164),
|
|
188
|
+
(225, 179, 164),
|
|
189
|
+
(225, 179, 164),
|
|
190
|
+
(225, 179, 164),
|
|
191
|
+
(225, 178, 165),
|
|
192
|
+
(224, 178, 165),
|
|
193
|
+
(224, 178, 165),
|
|
194
|
+
(224, 177, 165),
|
|
195
|
+
(224, 177, 166),
|
|
196
|
+
(223, 177, 166),
|
|
197
|
+
(223, 177, 166),
|
|
198
|
+
(223, 176, 166),
|
|
199
|
+
(223, 176, 167),
|
|
200
|
+
(223, 176, 167),
|
|
201
|
+
(222, 175, 167),
|
|
202
|
+
(222, 175, 167),
|
|
203
|
+
(222, 175, 168),
|
|
204
|
+
(222, 174, 168),
|
|
205
|
+
(222, 174, 168),
|
|
206
|
+
(221, 174, 168),
|
|
207
|
+
(221, 173, 169),
|
|
208
|
+
(221, 173, 169),
|
|
209
|
+
(221, 173, 169),
|
|
210
|
+
(220, 173, 169),
|
|
211
|
+
(220, 172, 170),
|
|
212
|
+
(220, 172, 170),
|
|
213
|
+
(220, 172, 170),
|
|
214
|
+
(220, 171, 170),
|
|
215
|
+
(219, 171, 171),
|
|
216
|
+
(219, 171, 171),
|
|
217
|
+
(219, 170, 171),
|
|
218
|
+
(219, 170, 171),
|
|
219
|
+
(219, 170, 171),
|
|
220
|
+
(218, 169, 172),
|
|
221
|
+
(218, 169, 172),
|
|
222
|
+
(218, 169, 172),
|
|
223
|
+
(218, 168, 172),
|
|
224
|
+
(218, 168, 173),
|
|
225
|
+
(217, 168, 173),
|
|
226
|
+
(217, 168, 173),
|
|
227
|
+
(217, 167, 173),
|
|
228
|
+
(217, 167, 174),
|
|
229
|
+
(216, 167, 174),
|
|
230
|
+
(216, 166, 174),
|
|
231
|
+
(216, 166, 174),
|
|
232
|
+
(216, 166, 175),
|
|
233
|
+
(216, 165, 175),
|
|
234
|
+
(215, 165, 175),
|
|
235
|
+
(215, 165, 175),
|
|
236
|
+
(215, 164, 176),
|
|
237
|
+
(215, 164, 176),
|
|
238
|
+
(215, 164, 176),
|
|
239
|
+
(214, 164, 176),
|
|
240
|
+
(214, 163, 177),
|
|
241
|
+
(214, 163, 177),
|
|
242
|
+
(214, 163, 177),
|
|
243
|
+
(213, 162, 177),
|
|
244
|
+
(213, 162, 178),
|
|
245
|
+
(213, 162, 178),
|
|
246
|
+
(213, 161, 178),
|
|
247
|
+
(213, 161, 178),
|
|
248
|
+
(212, 161, 179),
|
|
249
|
+
(212, 160, 179),
|
|
250
|
+
(212, 160, 179),
|
|
251
|
+
(212, 160, 179),
|
|
252
|
+
(212, 159, 180),
|
|
253
|
+
(211, 159, 180),
|
|
254
|
+
(211, 159, 180),
|
|
255
|
+
(211, 159, 180),
|
|
256
|
+
(211, 158, 181),
|
|
257
|
+
(211, 158, 181),
|
|
258
|
+
(210, 158, 181),
|
|
259
|
+
(210, 157, 181),
|
|
260
|
+
(210, 157, 182),
|
|
261
|
+
(210, 157, 182),
|
|
262
|
+
(209, 156, 182),
|
|
263
|
+
(209, 156, 182),
|
|
264
|
+
(209, 156, 183),
|
|
265
|
+
(209, 155, 183),
|
|
266
|
+
(209, 155, 183),
|
|
267
|
+
(208, 155, 183),
|
|
268
|
+
(208, 155, 184),
|
|
269
|
+
(208, 154, 184),
|
|
270
|
+
(208, 154, 184),
|
|
271
|
+
(208, 154, 184),
|
|
272
|
+
(207, 153, 185),
|
|
273
|
+
(207, 153, 185),
|
|
274
|
+
(207, 153, 185),
|
|
275
|
+
(207, 152, 185),
|
|
276
|
+
(207, 152, 186),
|
|
277
|
+
(206, 152, 186),
|
|
278
|
+
(206, 151, 186),
|
|
279
|
+
(206, 151, 186),
|
|
280
|
+
(206, 151, 187),
|
|
281
|
+
(205, 151, 187),
|
|
282
|
+
(205, 150, 187),
|
|
283
|
+
(205, 150, 187),
|
|
284
|
+
(205, 150, 188),
|
|
285
|
+
(205, 149, 188),
|
|
286
|
+
(204, 149, 188),
|
|
287
|
+
(204, 149, 188),
|
|
288
|
+
(204, 148, 189),
|
|
289
|
+
(204, 148, 189),
|
|
290
|
+
(204, 148, 189),
|
|
291
|
+
(203, 147, 189),
|
|
292
|
+
(203, 147, 190),
|
|
293
|
+
(203, 147, 190),
|
|
294
|
+
(203, 146, 190),
|
|
295
|
+
(202, 146, 190),
|
|
296
|
+
(202, 146, 191),
|
|
297
|
+
(202, 146, 191),
|
|
298
|
+
(202, 145, 191),
|
|
299
|
+
(202, 145, 191),
|
|
300
|
+
(201, 145, 192),
|
|
301
|
+
(201, 144, 192),
|
|
302
|
+
(201, 144, 192),
|
|
303
|
+
]
|
neo4j_viz/gds.py
CHANGED
|
@@ -6,7 +6,7 @@ from typing import Optional
|
|
|
6
6
|
import pandas as pd
|
|
7
7
|
from graphdatascience import Graph, GraphDataScience
|
|
8
8
|
|
|
9
|
-
from .pandas import
|
|
9
|
+
from .pandas import _from_dfs
|
|
10
10
|
from .visualization_graph import VisualizationGraph
|
|
11
11
|
|
|
12
12
|
|
|
@@ -35,6 +35,11 @@ def from_gds(
|
|
|
35
35
|
"""
|
|
36
36
|
Create a VisualizationGraph from a GraphDataScience object and a Graph object.
|
|
37
37
|
|
|
38
|
+
All `additional_node_properties` will be included in the visualization graph.
|
|
39
|
+
If the properties are named as the fields of the `Node` class, they will be included as top level fields of the
|
|
40
|
+
created `Node` objects. Otherwise, they will be included in the `properties` dictionary.
|
|
41
|
+
Additionally, a new "labels" node property will be added, containing the node labels of the node.
|
|
42
|
+
|
|
38
43
|
Parameters
|
|
39
44
|
----------
|
|
40
45
|
gds : GraphDataScience
|
|
@@ -75,9 +80,13 @@ def from_gds(
|
|
|
75
80
|
|
|
76
81
|
node_props_df = pd.concat(node_dfs.values(), ignore_index=True, axis=0).drop_duplicates()
|
|
77
82
|
if size_property is not None:
|
|
83
|
+
if "size" in actual_node_properties and size_property != "size":
|
|
84
|
+
node_props_df.rename(columns={"size": "__size"}, inplace=True)
|
|
78
85
|
node_props_df.rename(columns={size_property: "size"}, inplace=True)
|
|
79
86
|
|
|
80
87
|
for lbl, df in node_dfs.items():
|
|
88
|
+
if "labels" in actual_node_properties:
|
|
89
|
+
df.rename(columns={"labels": "__labels"}, inplace=True)
|
|
81
90
|
df["labels"] = lbl
|
|
82
91
|
|
|
83
92
|
node_lbls_df = pd.concat([df[["id", "labels"]] for df in node_dfs.values()], ignore_index=True, axis=0)
|
|
@@ -88,4 +97,4 @@ def from_gds(
|
|
|
88
97
|
rel_df = _rel_df(gds, G)
|
|
89
98
|
rel_df.rename(columns={"sourceNodeId": "source", "targetNodeId": "target"}, inplace=True)
|
|
90
99
|
|
|
91
|
-
return
|
|
100
|
+
return _from_dfs(node_df, rel_df, node_radius_min_max=node_radius_min_max, rename_properties={"__size": "size"})
|
neo4j_viz/gql_create.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import uuid
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from neo4j_viz import Node, Relationship, VisualizationGraph
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _parse_value(value_str: str) -> Any:
|
|
9
|
+
value_str = value_str.strip()
|
|
10
|
+
if not value_str:
|
|
11
|
+
return None
|
|
12
|
+
|
|
13
|
+
# Parse map
|
|
14
|
+
if value_str.startswith("{") and value_str.endswith("}"):
|
|
15
|
+
inner = value_str[1:-1].strip()
|
|
16
|
+
result = {}
|
|
17
|
+
depth = 0
|
|
18
|
+
in_string = None
|
|
19
|
+
start_idx = 0
|
|
20
|
+
for i, ch in enumerate(inner):
|
|
21
|
+
if in_string is None:
|
|
22
|
+
if ch in ["'", '"']:
|
|
23
|
+
in_string = ch
|
|
24
|
+
elif ch in ["{", "["]:
|
|
25
|
+
depth += 1
|
|
26
|
+
elif ch in ["}", "]"]:
|
|
27
|
+
depth -= 1
|
|
28
|
+
elif ch == "," and depth == 0:
|
|
29
|
+
segment = inner[start_idx:i].strip()
|
|
30
|
+
if ":" not in segment:
|
|
31
|
+
return None
|
|
32
|
+
k, v = segment.split(":", 1)
|
|
33
|
+
k = k.strip().strip("'\"")
|
|
34
|
+
result[k] = _parse_value(v)
|
|
35
|
+
start_idx = i + 1
|
|
36
|
+
else:
|
|
37
|
+
if ch == in_string:
|
|
38
|
+
in_string = None
|
|
39
|
+
|
|
40
|
+
if inner[start_idx:]:
|
|
41
|
+
segment = inner[start_idx:].strip()
|
|
42
|
+
if ":" not in segment:
|
|
43
|
+
return None
|
|
44
|
+
k, v = segment.split(":", 1)
|
|
45
|
+
k = k.strip().strip("'\"")
|
|
46
|
+
result[k] = _parse_value(v)
|
|
47
|
+
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
# Parse list
|
|
51
|
+
if value_str.startswith("[") and value_str.endswith("]"):
|
|
52
|
+
inner = value_str[1:-1].strip()
|
|
53
|
+
items = []
|
|
54
|
+
depth = 0
|
|
55
|
+
in_string = None
|
|
56
|
+
start_idx = 0
|
|
57
|
+
for i, ch in enumerate(inner):
|
|
58
|
+
if in_string is None:
|
|
59
|
+
if ch in ["'", '"']:
|
|
60
|
+
in_string = ch
|
|
61
|
+
elif ch in ["{", "["]:
|
|
62
|
+
depth += 1
|
|
63
|
+
elif ch in ["}", "]"]:
|
|
64
|
+
depth -= 1
|
|
65
|
+
elif ch == "," and depth == 0:
|
|
66
|
+
items.append(_parse_value(inner[start_idx:i]))
|
|
67
|
+
start_idx = i + 1
|
|
68
|
+
else:
|
|
69
|
+
if ch == in_string:
|
|
70
|
+
in_string = None
|
|
71
|
+
|
|
72
|
+
if inner[start_idx:]:
|
|
73
|
+
items.append(_parse_value(inner[start_idx:]))
|
|
74
|
+
|
|
75
|
+
return items
|
|
76
|
+
|
|
77
|
+
# Parse boolean, float, int, or string
|
|
78
|
+
if re.match(r"^-?\d+$", value_str):
|
|
79
|
+
return int(value_str)
|
|
80
|
+
if re.match(r"^-?\d+\.\d+$", value_str):
|
|
81
|
+
return float(value_str)
|
|
82
|
+
if value_str.lower() == "true":
|
|
83
|
+
return True
|
|
84
|
+
if value_str.lower() == "false":
|
|
85
|
+
return False
|
|
86
|
+
if value_str.lower() == "null":
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
return value_str.strip("'\"")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _parse_prop_str(
|
|
93
|
+
query: str, prop_str: str, prop_start: int, top_level_keys: set[str]
|
|
94
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
95
|
+
top_level: dict[str, Any] = {}
|
|
96
|
+
props: dict[str, Any] = {}
|
|
97
|
+
depth = 0
|
|
98
|
+
in_string = None
|
|
99
|
+
start_idx = 0
|
|
100
|
+
for i, ch in enumerate(prop_str):
|
|
101
|
+
if in_string is None:
|
|
102
|
+
if ch in ["'", '"']:
|
|
103
|
+
in_string = ch
|
|
104
|
+
elif ch in ["{", "["]:
|
|
105
|
+
depth += 1
|
|
106
|
+
elif ch in ["}", "]"]:
|
|
107
|
+
depth -= 1
|
|
108
|
+
elif ch == "," and depth == 0:
|
|
109
|
+
pair = prop_str[start_idx:i].strip()
|
|
110
|
+
if ":" not in pair:
|
|
111
|
+
snippet = _get_snippet(query, prop_start + start_idx)
|
|
112
|
+
raise ValueError(f"Property syntax error near: `{snippet}`.")
|
|
113
|
+
k, v = pair.split(":", 1)
|
|
114
|
+
k = k.strip().strip("'\"")
|
|
115
|
+
|
|
116
|
+
if k in top_level_keys:
|
|
117
|
+
top_level[k] = _parse_value(v)
|
|
118
|
+
else:
|
|
119
|
+
props[k] = _parse_value(v)
|
|
120
|
+
|
|
121
|
+
start_idx = i + 1
|
|
122
|
+
else:
|
|
123
|
+
if ch == in_string:
|
|
124
|
+
in_string = None
|
|
125
|
+
|
|
126
|
+
if prop_str[start_idx:]:
|
|
127
|
+
pair = prop_str[start_idx:].strip()
|
|
128
|
+
if ":" not in pair:
|
|
129
|
+
snippet = _get_snippet(query, prop_start + start_idx)
|
|
130
|
+
raise ValueError(f"Property syntax error near: `{snippet}`.")
|
|
131
|
+
k, v = pair.split(":", 1)
|
|
132
|
+
k = k.strip().strip("'\"")
|
|
133
|
+
|
|
134
|
+
if k in top_level_keys:
|
|
135
|
+
top_level[k] = _parse_value(v)
|
|
136
|
+
else:
|
|
137
|
+
props[k] = _parse_value(v)
|
|
138
|
+
|
|
139
|
+
return top_level, props
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _parse_labels_and_props(
|
|
143
|
+
query: str, s: str, top_level_keys: set[str]
|
|
144
|
+
) -> tuple[Optional[str], dict[str, Any], dict[str, Any]]:
|
|
145
|
+
prop_match = re.search(r"\{(.*)\}", s)
|
|
146
|
+
prop_str = ""
|
|
147
|
+
if prop_match:
|
|
148
|
+
prop_str = prop_match.group(1)
|
|
149
|
+
prop_start = query.index(prop_str, query.index(s))
|
|
150
|
+
s = s[: prop_match.start()].strip()
|
|
151
|
+
alias_labels = re.split(r"[:&]", s)
|
|
152
|
+
raw_alias = alias_labels[0].strip()
|
|
153
|
+
final_alias = raw_alias if raw_alias else None
|
|
154
|
+
|
|
155
|
+
if prop_str:
|
|
156
|
+
top_level, props = _parse_prop_str(query, prop_str, prop_start, top_level_keys)
|
|
157
|
+
else:
|
|
158
|
+
top_level = {}
|
|
159
|
+
props = {}
|
|
160
|
+
|
|
161
|
+
label_list = [lbl.strip() for lbl in alias_labels[1:]]
|
|
162
|
+
if "labels" in props:
|
|
163
|
+
props["__labels"] = props["labels"]
|
|
164
|
+
props["labels"] = sorted(label_list)
|
|
165
|
+
|
|
166
|
+
return final_alias, top_level, props
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _get_snippet(q: str, idx: int, context: int = 15) -> str:
|
|
170
|
+
start = max(0, idx - context)
|
|
171
|
+
end = min(len(q), idx + context)
|
|
172
|
+
|
|
173
|
+
return q[start:end].replace("\n", " ")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def from_gql_create(
|
|
177
|
+
query: str,
|
|
178
|
+
size_property: Optional[str] = None,
|
|
179
|
+
node_caption: Optional[str] = "labels",
|
|
180
|
+
relationship_caption: Optional[str] = "type",
|
|
181
|
+
node_radius_min_max: Optional[tuple[float, float]] = (3, 60),
|
|
182
|
+
) -> VisualizationGraph:
|
|
183
|
+
"""
|
|
184
|
+
Parse a GQL CREATE query and return a VisualizationGraph object representing the graph it creates.
|
|
185
|
+
|
|
186
|
+
All node and relationship properties will be included in the visualization graph.
|
|
187
|
+
If the properties are named as the fields of the `Node` or `Relationship` classes, they will be included as
|
|
188
|
+
top level fields of the respective objects. Otherwise, they will be included in the `properties` dictionary.
|
|
189
|
+
Additionally, a "labels" property will be added for nodes and a "type" property for relationships.
|
|
190
|
+
|
|
191
|
+
Please note that this function is not a full GQL parser, it only handles CREATE queries that do not contain
|
|
192
|
+
other clauses like MATCH, WHERE, RETURN, etc, or any Cypher function calls.
|
|
193
|
+
It also does not handle all possible GQL syntax, but it should work for most common cases.
|
|
194
|
+
For more complex cases, we recommend using a Neo4j database and the `from_neo4j` method.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
query : str
|
|
199
|
+
The GQL CREATE query to parse
|
|
200
|
+
size_property : str, optional
|
|
201
|
+
Property to use for node size, by default None.
|
|
202
|
+
node_caption : str, optional
|
|
203
|
+
Property to use as the node caption, by default the node labels will be used.
|
|
204
|
+
relationship_caption : str, optional
|
|
205
|
+
Property to use as the relationship caption, by default the relationship type will be used.
|
|
206
|
+
node_radius_min_max : tuple[float, float], optional
|
|
207
|
+
Minimum and maximum node radius, by default (3, 60).
|
|
208
|
+
To avoid tiny or huge nodes in the visualization, the node sizes are scaled to fit in the given range.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
query = query.strip()
|
|
212
|
+
if not re.match(r"(?i)^create\b", query):
|
|
213
|
+
raise ValueError("Query must begin with 'CREATE' (case insensitive).")
|
|
214
|
+
|
|
215
|
+
query = re.sub(r"(?i)^create\s*", "", query, count=1).rstrip(";").strip()
|
|
216
|
+
parts = []
|
|
217
|
+
paren_level = 0
|
|
218
|
+
bracket_level = 0
|
|
219
|
+
current: list[str] = []
|
|
220
|
+
for i, char in enumerate(query):
|
|
221
|
+
if char == "(":
|
|
222
|
+
paren_level += 1
|
|
223
|
+
elif char == ")":
|
|
224
|
+
paren_level -= 1
|
|
225
|
+
if paren_level < 0:
|
|
226
|
+
snippet = _get_snippet(query, i)
|
|
227
|
+
raise ValueError(f"Unbalanced parentheses near: `{snippet}`.")
|
|
228
|
+
if char == "[":
|
|
229
|
+
bracket_level += 1
|
|
230
|
+
elif char == "]":
|
|
231
|
+
bracket_level -= 1
|
|
232
|
+
if bracket_level < 0:
|
|
233
|
+
snippet = _get_snippet(query, i)
|
|
234
|
+
raise ValueError(f"Unbalanced square brackets near: `{snippet}`.")
|
|
235
|
+
if char == "," and paren_level == 0 and bracket_level == 0:
|
|
236
|
+
parts.append("".join(current).strip())
|
|
237
|
+
current = []
|
|
238
|
+
else:
|
|
239
|
+
current.append(char)
|
|
240
|
+
|
|
241
|
+
parts.append("".join(current).strip())
|
|
242
|
+
if paren_level != 0:
|
|
243
|
+
snippet = _get_snippet(query, len(query) - 1)
|
|
244
|
+
raise ValueError(f"Unbalanced parentheses near: `{snippet}`.")
|
|
245
|
+
if bracket_level != 0:
|
|
246
|
+
snippet = _get_snippet(query, len(query) - 1)
|
|
247
|
+
raise ValueError(f"Unbalanced square brackets near: `{snippet}`.")
|
|
248
|
+
|
|
249
|
+
node_pattern = re.compile(r"^\(([^)]*)\)$")
|
|
250
|
+
rel_pattern = re.compile(r"^\(([^)]*)\)-\s*\[\s*:(\w+)\s*(\{[^}]*\})?\s*\]->\(([^)]*)\)$")
|
|
251
|
+
|
|
252
|
+
node_top_level_keys = set(Node.model_fields.keys())
|
|
253
|
+
node_top_level_keys.remove("id")
|
|
254
|
+
|
|
255
|
+
rel_top_level_keys = set(Relationship.model_fields.keys())
|
|
256
|
+
rel_top_level_keys.remove("id")
|
|
257
|
+
rel_top_level_keys.remove("source")
|
|
258
|
+
rel_top_level_keys.remove("target")
|
|
259
|
+
|
|
260
|
+
nodes = []
|
|
261
|
+
relationships = []
|
|
262
|
+
alias_to_id = {}
|
|
263
|
+
anonymous_count = 0
|
|
264
|
+
|
|
265
|
+
for part in parts:
|
|
266
|
+
node_m = node_pattern.match(part)
|
|
267
|
+
if node_m:
|
|
268
|
+
alias_labels_props = node_m.group(1).strip()
|
|
269
|
+
alias, top_level, props = _parse_labels_and_props(query, alias_labels_props, node_top_level_keys)
|
|
270
|
+
if not alias:
|
|
271
|
+
alias = f"_anon_{anonymous_count}"
|
|
272
|
+
anonymous_count += 1
|
|
273
|
+
if alias not in alias_to_id:
|
|
274
|
+
alias_to_id[alias] = str(uuid.uuid4())
|
|
275
|
+
nodes.append(Node(id=alias_to_id[alias], **top_level, properties=props))
|
|
276
|
+
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
rel_m = rel_pattern.match(part)
|
|
280
|
+
if rel_m:
|
|
281
|
+
left_node = rel_m.group(1).strip()
|
|
282
|
+
right_node = rel_m.group(4).strip()
|
|
283
|
+
|
|
284
|
+
# Parse left node pattern
|
|
285
|
+
left_alias, left_top_level, left_props = _parse_labels_and_props(query, left_node, node_top_level_keys)
|
|
286
|
+
if not left_alias:
|
|
287
|
+
left_alias = f"_anon_{anonymous_count}"
|
|
288
|
+
anonymous_count += 1
|
|
289
|
+
if left_alias not in alias_to_id:
|
|
290
|
+
alias_to_id[left_alias] = str(uuid.uuid4())
|
|
291
|
+
nodes.append(Node(id=alias_to_id[left_alias], **left_top_level, properties=left_props))
|
|
292
|
+
elif left_alias not in alias_to_id:
|
|
293
|
+
snippet = _get_snippet(query, query.index(left_node))
|
|
294
|
+
raise ValueError(f"Relationship references unknown node alias: '{left_alias}' near: `{snippet}`.")
|
|
295
|
+
|
|
296
|
+
# Parse right node pattern
|
|
297
|
+
right_alias, right_top_level, right_props = _parse_labels_and_props(query, right_node, node_top_level_keys)
|
|
298
|
+
if not right_alias:
|
|
299
|
+
right_alias = f"_anon_{anonymous_count}"
|
|
300
|
+
anonymous_count += 1
|
|
301
|
+
if right_alias not in alias_to_id:
|
|
302
|
+
alias_to_id[right_alias] = str(uuid.uuid4())
|
|
303
|
+
nodes.append(Node(id=alias_to_id[right_alias], **right_top_level, properties=right_props))
|
|
304
|
+
elif right_alias not in alias_to_id:
|
|
305
|
+
snippet = _get_snippet(query, query.index(right_node))
|
|
306
|
+
raise ValueError(f"Relationship references unknown node alias: '{right_alias}' near: `{snippet}`.")
|
|
307
|
+
|
|
308
|
+
rel_id = str(uuid.uuid4())
|
|
309
|
+
rel_type = rel_m.group(2).replace(":", "").strip()
|
|
310
|
+
rel_props_str = rel_m.group(3) or ""
|
|
311
|
+
if rel_props_str:
|
|
312
|
+
inner_str = rel_props_str.strip("{}").strip()
|
|
313
|
+
prop_start = query.index(inner_str, query.index(inner_str))
|
|
314
|
+
top_level, props = _parse_prop_str(query, inner_str, prop_start, rel_top_level_keys)
|
|
315
|
+
else:
|
|
316
|
+
top_level = {}
|
|
317
|
+
props = {}
|
|
318
|
+
if "type" in props:
|
|
319
|
+
props["__type"] = props["type"]
|
|
320
|
+
props["type"] = rel_type
|
|
321
|
+
relationships.append(
|
|
322
|
+
Relationship(
|
|
323
|
+
id=rel_id,
|
|
324
|
+
source=alias_to_id[left_alias],
|
|
325
|
+
target=alias_to_id[right_alias],
|
|
326
|
+
**top_level,
|
|
327
|
+
properties=props,
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
continue
|
|
331
|
+
|
|
332
|
+
snippet = part[:30]
|
|
333
|
+
raise ValueError(f"Invalid element in CREATE near: `{snippet}`.")
|
|
334
|
+
|
|
335
|
+
if size_property is not None:
|
|
336
|
+
for node in nodes:
|
|
337
|
+
node.size = node.properties.get(size_property)
|
|
338
|
+
if node_caption is not None:
|
|
339
|
+
for node in nodes:
|
|
340
|
+
if node_caption == "labels":
|
|
341
|
+
if len(node.properties["labels"]) > 0:
|
|
342
|
+
node.caption = ":".join([label for label in node.properties["labels"]])
|
|
343
|
+
else:
|
|
344
|
+
node.caption = str(node.properties.get(node_caption))
|
|
345
|
+
if relationship_caption is not None:
|
|
346
|
+
for rel in relationships:
|
|
347
|
+
if relationship_caption == "type":
|
|
348
|
+
rel.caption = rel.properties["type"]
|
|
349
|
+
else:
|
|
350
|
+
rel.caption = str(rel.properties.get(relationship_caption))
|
|
351
|
+
|
|
352
|
+
VG = VisualizationGraph(nodes=nodes, relationships=relationships)
|
|
353
|
+
if (node_radius_min_max is not None) and (size_property is not None):
|
|
354
|
+
VG.resize_nodes(node_radius_min_max=node_radius_min_max)
|
|
355
|
+
|
|
356
|
+
return VG
|