neo4j-viz 0.2.6__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 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
- neo4j_colors = [
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 from_dfs
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 from_dfs(node_df, rel_df, node_radius_min_max=node_radius_min_max)
100
+ return _from_dfs(node_df, rel_df, node_radius_min_max=node_radius_min_max, rename_properties={"__size": "size"})
@@ -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