anywidget-graph 0.1.0__py3-none-any.whl → 0.2.1__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.
- anywidget_graph/__init__.py +1 -1
- anywidget_graph/backends/__init__.py +71 -0
- anywidget_graph/backends/arango.py +201 -0
- anywidget_graph/backends/grafeo.py +70 -0
- anywidget_graph/backends/ladybug.py +94 -0
- anywidget_graph/backends/neo4j.py +143 -0
- anywidget_graph/converters/__init__.py +35 -0
- anywidget_graph/converters/base.py +77 -0
- anywidget_graph/converters/common.py +92 -0
- anywidget_graph/converters/cypher.py +107 -0
- anywidget_graph/converters/gql.py +15 -0
- anywidget_graph/converters/graphql.py +208 -0
- anywidget_graph/converters/gremlin.py +159 -0
- anywidget_graph/converters/sparql.py +167 -0
- anywidget_graph/ui/__init__.py +19 -0
- anywidget_graph/ui/icons.js +15 -0
- anywidget_graph/ui/index.js +199 -0
- anywidget_graph/ui/neo4j.js +193 -0
- anywidget_graph/ui/properties.js +137 -0
- anywidget_graph/ui/schema.js +178 -0
- anywidget_graph/ui/settings.js +299 -0
- anywidget_graph/ui/styles.css +584 -0
- anywidget_graph/ui/toolbar.js +106 -0
- anywidget_graph/widget.py +417 -226
- {anywidget_graph-0.1.0.dist-info → anywidget_graph-0.2.1.dist-info}/METADATA +4 -3
- anywidget_graph-0.2.1.dist-info/RECORD +28 -0
- anywidget_graph-0.1.0.dist-info/RECORD +0 -6
- {anywidget_graph-0.1.0.dist-info → anywidget_graph-0.2.1.dist-info}/WHEEL +0 -0
anywidget_graph/widget.py
CHANGED
|
@@ -6,186 +6,448 @@ from typing import TYPE_CHECKING, Any
|
|
|
6
6
|
|
|
7
7
|
import anywidget
|
|
8
8
|
import traitlets
|
|
9
|
+
from traitlets import observe
|
|
10
|
+
|
|
11
|
+
from anywidget_graph.backends import DatabaseBackend
|
|
12
|
+
from anywidget_graph.backends.grafeo import GrafeoBackend
|
|
13
|
+
from anywidget_graph.ui import get_css, get_esm
|
|
9
14
|
|
|
10
15
|
if TYPE_CHECKING:
|
|
11
16
|
from collections.abc import Callable
|
|
12
17
|
|
|
13
|
-
_ESM = """
|
|
14
|
-
import Graph from "https://esm.sh/graphology@0.25.4";
|
|
15
|
-
import Sigma from "https://esm.sh/sigma@3.0.0";
|
|
16
|
-
|
|
17
|
-
function render({ model, el }) {
|
|
18
|
-
const container = document.createElement("div");
|
|
19
|
-
container.style.width = model.get("width") + "px";
|
|
20
|
-
container.style.height = model.get("height") + "px";
|
|
21
|
-
container.style.border = "1px solid #ddd";
|
|
22
|
-
container.style.borderRadius = "4px";
|
|
23
|
-
container.style.background = model.get("background") || "#fafafa";
|
|
24
|
-
el.appendChild(container);
|
|
25
|
-
|
|
26
|
-
const graph = new Graph();
|
|
27
|
-
|
|
28
|
-
const nodes = model.get("nodes") || [];
|
|
29
|
-
nodes.forEach((node) => {
|
|
30
|
-
graph.addNode(node.id, {
|
|
31
|
-
label: node.label || node.id,
|
|
32
|
-
x: node.x ?? Math.random() * 100,
|
|
33
|
-
y: node.y ?? Math.random() * 100,
|
|
34
|
-
size: node.size || 10,
|
|
35
|
-
color: node.color || "#6366f1",
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const edges = model.get("edges") || [];
|
|
40
|
-
edges.forEach((edge, i) => {
|
|
41
|
-
graph.addEdge(edge.source, edge.target, {
|
|
42
|
-
label: edge.label || "",
|
|
43
|
-
size: edge.size || 2,
|
|
44
|
-
color: edge.color || "#94a3b8",
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const renderer = new Sigma(graph, container, {
|
|
49
|
-
renderLabels: model.get("show_labels"),
|
|
50
|
-
renderEdgeLabels: model.get("show_edge_labels"),
|
|
51
|
-
defaultNodeColor: "#6366f1",
|
|
52
|
-
defaultEdgeColor: "#94a3b8",
|
|
53
|
-
labelColor: { color: "#333" },
|
|
54
|
-
labelSize: 12,
|
|
55
|
-
labelWeight: "500",
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
renderer.on("clickNode", ({ node }) => {
|
|
59
|
-
const nodeData = graph.getNodeAttributes(node);
|
|
60
|
-
model.set("selected_node", { id: node, ...nodeData });
|
|
61
|
-
model.save_changes();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
renderer.on("clickEdge", ({ edge }) => {
|
|
65
|
-
const edgeData = graph.getEdgeAttributes(edge);
|
|
66
|
-
const [source, target] = graph.extremities(edge);
|
|
67
|
-
model.set("selected_edge", { source, target, ...edgeData });
|
|
68
|
-
model.save_changes();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
renderer.on("clickStage", () => {
|
|
72
|
-
model.set("selected_node", null);
|
|
73
|
-
model.set("selected_edge", null);
|
|
74
|
-
model.save_changes();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
model.on("change:nodes", () => {
|
|
78
|
-
graph.clear();
|
|
79
|
-
const newNodes = model.get("nodes") || [];
|
|
80
|
-
newNodes.forEach((node) => {
|
|
81
|
-
graph.addNode(node.id, {
|
|
82
|
-
label: node.label || node.id,
|
|
83
|
-
x: node.x ?? Math.random() * 100,
|
|
84
|
-
y: node.y ?? Math.random() * 100,
|
|
85
|
-
size: node.size || 10,
|
|
86
|
-
color: node.color || "#6366f1",
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
const newEdges = model.get("edges") || [];
|
|
90
|
-
newEdges.forEach((edge) => {
|
|
91
|
-
graph.addEdge(edge.source, edge.target, {
|
|
92
|
-
label: edge.label || "",
|
|
93
|
-
size: edge.size || 2,
|
|
94
|
-
color: edge.color || "#94a3b8",
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
renderer.refresh();
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
model.on("change:edges", () => {
|
|
101
|
-
graph.clearEdges();
|
|
102
|
-
const newEdges = model.get("edges") || [];
|
|
103
|
-
newEdges.forEach((edge) => {
|
|
104
|
-
graph.addEdge(edge.source, edge.target, {
|
|
105
|
-
label: edge.label || "",
|
|
106
|
-
size: edge.size || 2,
|
|
107
|
-
color: edge.color || "#94a3b8",
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
renderer.refresh();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
return () => {
|
|
114
|
-
renderer.kill();
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export default { render };
|
|
119
|
-
"""
|
|
120
|
-
|
|
121
|
-
_CSS = """
|
|
122
|
-
.anywidget-graph {
|
|
123
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
124
|
-
}
|
|
125
|
-
"""
|
|
126
|
-
|
|
127
18
|
|
|
128
19
|
class Graph(anywidget.AnyWidget):
|
|
129
|
-
"""Interactive graph visualization widget using Sigma.js.
|
|
20
|
+
"""Interactive graph visualization widget using Sigma.js.
|
|
21
|
+
|
|
22
|
+
Supports Neo4j (browser-side) and Grafeo (Python-side) backends.
|
|
23
|
+
|
|
24
|
+
Examples
|
|
25
|
+
--------
|
|
26
|
+
Basic usage with static data:
|
|
27
|
+
|
|
28
|
+
>>> graph = Graph(
|
|
29
|
+
... nodes=[{"id": "a", "label": "Alice"}, {"id": "b", "label": "Bob"}],
|
|
30
|
+
... edges=[{"source": "a", "target": "b", "label": "KNOWS"}],
|
|
31
|
+
... )
|
|
32
|
+
|
|
33
|
+
With Neo4j connection:
|
|
34
|
+
|
|
35
|
+
>>> graph = Graph(
|
|
36
|
+
... database_backend="neo4j",
|
|
37
|
+
... connection_uri="neo4j+s://demo.neo4jlabs.com",
|
|
38
|
+
... connection_username="neo4j",
|
|
39
|
+
... connection_password="password",
|
|
40
|
+
... )
|
|
41
|
+
|
|
42
|
+
With Grafeo backend:
|
|
43
|
+
|
|
44
|
+
>>> import grafeo
|
|
45
|
+
>>> db = grafeo.GrafeoDB()
|
|
46
|
+
>>> graph = Graph(database_backend="grafeo", grafeo_db=db)
|
|
47
|
+
"""
|
|
130
48
|
|
|
131
|
-
_esm =
|
|
132
|
-
_css =
|
|
49
|
+
_esm = get_esm()
|
|
50
|
+
_css = get_css()
|
|
133
51
|
|
|
52
|
+
# === Graph Data ===
|
|
134
53
|
nodes = traitlets.List(trait=traitlets.Dict()).tag(sync=True)
|
|
135
54
|
edges = traitlets.List(trait=traitlets.Dict()).tag(sync=True)
|
|
136
55
|
|
|
56
|
+
# === Display Settings ===
|
|
137
57
|
width = traitlets.Int(default_value=800).tag(sync=True)
|
|
138
58
|
height = traitlets.Int(default_value=600).tag(sync=True)
|
|
139
59
|
background = traitlets.Unicode(default_value="#fafafa").tag(sync=True)
|
|
140
60
|
show_labels = traitlets.Bool(default_value=True).tag(sync=True)
|
|
141
61
|
show_edge_labels = traitlets.Bool(default_value=False).tag(sync=True)
|
|
142
62
|
|
|
63
|
+
# === Selection State ===
|
|
143
64
|
selected_node = traitlets.Dict(allow_none=True, default_value=None).tag(sync=True)
|
|
144
65
|
selected_edge = traitlets.Dict(allow_none=True, default_value=None).tag(sync=True)
|
|
145
66
|
|
|
67
|
+
# === Toolbar Visibility ===
|
|
68
|
+
show_toolbar = traitlets.Bool(default_value=True).tag(sync=True)
|
|
69
|
+
show_settings = traitlets.Bool(default_value=True).tag(sync=True)
|
|
70
|
+
show_query_input = traitlets.Bool(default_value=True).tag(sync=True)
|
|
71
|
+
|
|
72
|
+
# === Theme ===
|
|
73
|
+
dark_mode = traitlets.Bool(default_value=True).tag(sync=True)
|
|
74
|
+
|
|
75
|
+
# === Database Backend ===
|
|
76
|
+
database_backend = traitlets.Unicode(default_value="neo4j").tag(sync=True)
|
|
77
|
+
|
|
78
|
+
# === Neo4j Connection (browser-side) ===
|
|
79
|
+
connection_uri = traitlets.Unicode(default_value="").tag(sync=True)
|
|
80
|
+
connection_username = traitlets.Unicode(default_value="").tag(sync=True)
|
|
81
|
+
connection_password = traitlets.Unicode(default_value="").tag(sync=True)
|
|
82
|
+
connection_database = traitlets.Unicode(default_value="neo4j").tag(sync=True)
|
|
83
|
+
|
|
84
|
+
# === Query State ===
|
|
85
|
+
query = traitlets.Unicode(default_value="").tag(sync=True)
|
|
86
|
+
query_language = traitlets.Unicode(default_value="cypher").tag(sync=True)
|
|
87
|
+
query_running = traitlets.Bool(default_value=False).tag(sync=True)
|
|
88
|
+
query_error = traitlets.Unicode(default_value="").tag(sync=True)
|
|
89
|
+
|
|
90
|
+
# === Connection State ===
|
|
91
|
+
connection_status = traitlets.Unicode(default_value="disconnected").tag(sync=True)
|
|
92
|
+
|
|
93
|
+
# === Schema Data ===
|
|
94
|
+
schema_node_types = traitlets.List(trait=traitlets.Dict()).tag(sync=True)
|
|
95
|
+
schema_edge_types = traitlets.List(trait=traitlets.Dict()).tag(sync=True)
|
|
96
|
+
|
|
97
|
+
# === Query Execution Trigger (for Python backends) ===
|
|
98
|
+
_execute_query = traitlets.Int(default_value=0).tag(sync=True)
|
|
99
|
+
|
|
146
100
|
def __init__(
|
|
147
101
|
self,
|
|
148
102
|
nodes: list[dict[str, Any]] | None = None,
|
|
149
103
|
edges: list[dict[str, Any]] | None = None,
|
|
104
|
+
*,
|
|
105
|
+
width: int = 800,
|
|
106
|
+
height: int = 600,
|
|
107
|
+
background: str = "#fafafa",
|
|
108
|
+
show_labels: bool = True,
|
|
109
|
+
show_edge_labels: bool = False,
|
|
110
|
+
show_toolbar: bool = True,
|
|
111
|
+
show_settings: bool = True,
|
|
112
|
+
show_query_input: bool = True,
|
|
113
|
+
dark_mode: bool = True,
|
|
114
|
+
database_backend: str = "neo4j",
|
|
115
|
+
connection_uri: str = "",
|
|
116
|
+
connection_username: str = "",
|
|
117
|
+
connection_password: str = "",
|
|
118
|
+
connection_database: str = "neo4j",
|
|
119
|
+
grafeo_db: Any = None,
|
|
120
|
+
backend: DatabaseBackend | None = None,
|
|
150
121
|
**kwargs: Any,
|
|
151
122
|
) -> None:
|
|
152
|
-
|
|
123
|
+
"""Initialize the Graph widget.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
nodes : list[dict], optional
|
|
128
|
+
List of node dictionaries with 'id' and optional 'label', 'color', etc.
|
|
129
|
+
edges : list[dict], optional
|
|
130
|
+
List of edge dictionaries with 'source', 'target', and optional 'label'.
|
|
131
|
+
width : int
|
|
132
|
+
Widget width in pixels.
|
|
133
|
+
height : int
|
|
134
|
+
Widget height in pixels.
|
|
135
|
+
background : str
|
|
136
|
+
Background color for the graph area.
|
|
137
|
+
show_labels : bool
|
|
138
|
+
Whether to show node labels.
|
|
139
|
+
show_edge_labels : bool
|
|
140
|
+
Whether to show edge labels.
|
|
141
|
+
show_toolbar : bool
|
|
142
|
+
Whether to show the toolbar.
|
|
143
|
+
show_settings : bool
|
|
144
|
+
Whether to show the settings button.
|
|
145
|
+
show_query_input : bool
|
|
146
|
+
Whether to show the query input.
|
|
147
|
+
dark_mode : bool
|
|
148
|
+
Whether to use dark theme.
|
|
149
|
+
database_backend : str
|
|
150
|
+
Database backend: "neo4j" or "grafeo".
|
|
151
|
+
connection_uri : str
|
|
152
|
+
Neo4j connection URI.
|
|
153
|
+
connection_username : str
|
|
154
|
+
Neo4j username.
|
|
155
|
+
connection_password : str
|
|
156
|
+
Neo4j password.
|
|
157
|
+
connection_database : str
|
|
158
|
+
Neo4j database name.
|
|
159
|
+
grafeo_db : Any
|
|
160
|
+
Grafeo database instance for Python-side execution (legacy).
|
|
161
|
+
backend : DatabaseBackend | None
|
|
162
|
+
Generic database backend implementing the DatabaseBackend protocol.
|
|
163
|
+
"""
|
|
164
|
+
super().__init__(
|
|
165
|
+
nodes=nodes or [],
|
|
166
|
+
edges=edges or [],
|
|
167
|
+
width=width,
|
|
168
|
+
height=height,
|
|
169
|
+
background=background,
|
|
170
|
+
show_labels=show_labels,
|
|
171
|
+
show_edge_labels=show_edge_labels,
|
|
172
|
+
show_toolbar=show_toolbar,
|
|
173
|
+
show_settings=show_settings,
|
|
174
|
+
show_query_input=show_query_input,
|
|
175
|
+
dark_mode=dark_mode,
|
|
176
|
+
database_backend=database_backend,
|
|
177
|
+
connection_uri=connection_uri,
|
|
178
|
+
connection_username=connection_username,
|
|
179
|
+
connection_password=connection_password,
|
|
180
|
+
connection_database=connection_database,
|
|
181
|
+
**kwargs,
|
|
182
|
+
)
|
|
153
183
|
self._node_click_callbacks: list[Callable] = []
|
|
154
184
|
self._edge_click_callbacks: list[Callable] = []
|
|
155
185
|
|
|
186
|
+
# Support both legacy grafeo_db and new generic backend
|
|
187
|
+
if backend is not None:
|
|
188
|
+
self._backend = backend
|
|
189
|
+
elif grafeo_db is not None:
|
|
190
|
+
self._backend = GrafeoBackend(grafeo_db)
|
|
191
|
+
else:
|
|
192
|
+
self._backend = None
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def backend(self) -> DatabaseBackend | None:
|
|
196
|
+
"""Get the current database backend."""
|
|
197
|
+
return self._backend
|
|
198
|
+
|
|
199
|
+
@backend.setter
|
|
200
|
+
def backend(self, value: DatabaseBackend | None) -> None:
|
|
201
|
+
"""Set the database backend."""
|
|
202
|
+
self._backend = value
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def grafeo_db(self) -> Any:
|
|
206
|
+
"""Get the Grafeo database instance (legacy property)."""
|
|
207
|
+
if isinstance(self._backend, GrafeoBackend):
|
|
208
|
+
return self._backend.db
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
@grafeo_db.setter
|
|
212
|
+
def grafeo_db(self, value: Any) -> None:
|
|
213
|
+
"""Set the Grafeo database instance (legacy property)."""
|
|
214
|
+
self._backend = GrafeoBackend(value) if value else None
|
|
215
|
+
|
|
216
|
+
@observe("_execute_query")
|
|
217
|
+
def _on_execute_query(self, change: dict) -> None:
|
|
218
|
+
"""Handle query execution trigger from JavaScript."""
|
|
219
|
+
if change["new"] == 0:
|
|
220
|
+
return # Skip initial value
|
|
221
|
+
if self._backend is not None:
|
|
222
|
+
self._execute_backend_query()
|
|
223
|
+
|
|
224
|
+
def _execute_backend_query(self) -> None:
|
|
225
|
+
"""Execute query against the configured backend."""
|
|
226
|
+
if not self._backend:
|
|
227
|
+
self.query_error = "No database backend configured"
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
self.query_running = True
|
|
232
|
+
self.query_error = ""
|
|
233
|
+
nodes, edges = self._backend.execute(self.query)
|
|
234
|
+
self.nodes = nodes
|
|
235
|
+
self.edges = edges
|
|
236
|
+
except Exception as e:
|
|
237
|
+
self.query_error = str(e)
|
|
238
|
+
finally:
|
|
239
|
+
self.query_running = False
|
|
240
|
+
|
|
156
241
|
@classmethod
|
|
157
242
|
def from_dict(cls, data: dict[str, Any], **kwargs: Any) -> Graph:
|
|
158
|
-
"""Create a Graph from a dictionary with nodes and edges keys.
|
|
243
|
+
"""Create a Graph from a dictionary with 'nodes' and 'edges' keys.
|
|
244
|
+
|
|
245
|
+
Parameters
|
|
246
|
+
----------
|
|
247
|
+
data : dict
|
|
248
|
+
Dictionary with 'nodes' and 'edges' lists.
|
|
249
|
+
**kwargs
|
|
250
|
+
Additional arguments passed to Graph constructor.
|
|
251
|
+
|
|
252
|
+
Returns
|
|
253
|
+
-------
|
|
254
|
+
Graph
|
|
255
|
+
New Graph instance.
|
|
256
|
+
"""
|
|
159
257
|
return cls(nodes=data.get("nodes", []), edges=data.get("edges", []), **kwargs)
|
|
160
258
|
|
|
161
259
|
@classmethod
|
|
162
260
|
def from_cypher(cls, result: Any, **kwargs: Any) -> Graph:
|
|
163
|
-
"""Create a Graph from Cypher query results.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
261
|
+
"""Create a Graph from Cypher query results.
|
|
262
|
+
|
|
263
|
+
Parameters
|
|
264
|
+
----------
|
|
265
|
+
result : Any
|
|
266
|
+
Query result from Neo4j driver or similar.
|
|
267
|
+
**kwargs
|
|
268
|
+
Additional arguments passed to Graph constructor.
|
|
269
|
+
|
|
270
|
+
Returns
|
|
271
|
+
-------
|
|
272
|
+
Graph
|
|
273
|
+
New Graph instance with extracted nodes and edges.
|
|
274
|
+
|
|
275
|
+
Example
|
|
276
|
+
-------
|
|
277
|
+
>>> from neo4j import GraphDatabase
|
|
278
|
+
>>> driver = GraphDatabase.driver(uri, auth=auth)
|
|
279
|
+
>>> with driver.session() as session:
|
|
280
|
+
... result = session.run("MATCH (n)-[r]->(m) RETURN n, r, m")
|
|
281
|
+
... graph = Graph.from_cypher(result)
|
|
282
|
+
"""
|
|
283
|
+
from anywidget_graph.converters import CypherConverter
|
|
284
|
+
|
|
285
|
+
data = CypherConverter().convert(result)
|
|
286
|
+
return cls(nodes=data["nodes"], edges=data["edges"], **kwargs)
|
|
287
|
+
|
|
288
|
+
@classmethod
|
|
289
|
+
def from_gql(cls, result: Any, **kwargs: Any) -> Graph:
|
|
290
|
+
"""Create a Graph from GQL query results.
|
|
291
|
+
|
|
292
|
+
GQL (ISO Graph Query Language) uses a similar format to Cypher.
|
|
293
|
+
|
|
294
|
+
Parameters
|
|
295
|
+
----------
|
|
296
|
+
result : Any
|
|
297
|
+
Query result from GQL-compatible database.
|
|
298
|
+
**kwargs
|
|
299
|
+
Additional arguments passed to Graph constructor.
|
|
300
|
+
|
|
301
|
+
Returns
|
|
302
|
+
-------
|
|
303
|
+
Graph
|
|
304
|
+
New Graph instance with extracted nodes and edges.
|
|
305
|
+
"""
|
|
306
|
+
from anywidget_graph.converters import GQLConverter
|
|
307
|
+
|
|
308
|
+
data = GQLConverter().convert(result)
|
|
309
|
+
return cls(nodes=data["nodes"], edges=data["edges"], **kwargs)
|
|
310
|
+
|
|
311
|
+
@classmethod
|
|
312
|
+
def from_sparql(
|
|
313
|
+
cls,
|
|
314
|
+
result: Any,
|
|
315
|
+
subject_var: str = "s",
|
|
316
|
+
predicate_var: str = "p",
|
|
317
|
+
object_var: str = "o",
|
|
318
|
+
**kwargs: Any,
|
|
319
|
+
) -> Graph:
|
|
320
|
+
"""Create a Graph from SPARQL query results.
|
|
321
|
+
|
|
322
|
+
Parameters
|
|
323
|
+
----------
|
|
324
|
+
result : Any
|
|
325
|
+
SPARQL query result (e.g., from RDFLib or SPARQLWrapper).
|
|
326
|
+
subject_var : str
|
|
327
|
+
Variable name for subjects (default: "s").
|
|
328
|
+
predicate_var : str
|
|
329
|
+
Variable name for predicates (default: "p").
|
|
330
|
+
object_var : str
|
|
331
|
+
Variable name for objects (default: "o").
|
|
332
|
+
**kwargs
|
|
333
|
+
Additional arguments passed to Graph constructor.
|
|
334
|
+
|
|
335
|
+
Returns
|
|
336
|
+
-------
|
|
337
|
+
Graph
|
|
338
|
+
New Graph instance with extracted nodes and edges.
|
|
339
|
+
|
|
340
|
+
Example
|
|
341
|
+
-------
|
|
342
|
+
>>> from rdflib import Graph as RDFGraph
|
|
343
|
+
>>> g = RDFGraph()
|
|
344
|
+
>>> g.parse("data.ttl")
|
|
345
|
+
>>> result = g.query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
|
|
346
|
+
>>> graph = Graph.from_sparql(result)
|
|
347
|
+
"""
|
|
348
|
+
from anywidget_graph.converters import SPARQLConverter
|
|
349
|
+
|
|
350
|
+
converter = SPARQLConverter(
|
|
351
|
+
subject_var=subject_var,
|
|
352
|
+
predicate_var=predicate_var,
|
|
353
|
+
object_var=object_var,
|
|
354
|
+
)
|
|
355
|
+
data = converter.convert(result)
|
|
356
|
+
return cls(nodes=data["nodes"], edges=data["edges"], **kwargs)
|
|
357
|
+
|
|
358
|
+
@classmethod
|
|
359
|
+
def from_gremlin(cls, result: Any, **kwargs: Any) -> Graph:
|
|
360
|
+
"""Create a Graph from Gremlin/TinkerPop query results.
|
|
361
|
+
|
|
362
|
+
Parameters
|
|
363
|
+
----------
|
|
364
|
+
result : Any
|
|
365
|
+
Gremlin traversal result.
|
|
366
|
+
**kwargs
|
|
367
|
+
Additional arguments passed to Graph constructor.
|
|
368
|
+
|
|
369
|
+
Returns
|
|
370
|
+
-------
|
|
371
|
+
Graph
|
|
372
|
+
New Graph instance with extracted nodes and edges.
|
|
373
|
+
|
|
374
|
+
Example
|
|
375
|
+
-------
|
|
376
|
+
>>> from gremlin_python.driver import client
|
|
377
|
+
>>> gremlin_client = client.Client('ws://localhost:8182/gremlin', 'g')
|
|
378
|
+
>>> result = gremlin_client.submit("g.V().limit(10)").all().result()
|
|
379
|
+
>>> graph = Graph.from_gremlin(result)
|
|
380
|
+
"""
|
|
381
|
+
from anywidget_graph.converters import GremlinConverter
|
|
382
|
+
|
|
383
|
+
data = GremlinConverter().convert(result)
|
|
384
|
+
return cls(nodes=data["nodes"], edges=data["edges"], **kwargs)
|
|
385
|
+
|
|
386
|
+
@classmethod
|
|
387
|
+
def from_graphql(
|
|
388
|
+
cls,
|
|
389
|
+
result: Any,
|
|
390
|
+
nodes_path: str | None = None,
|
|
391
|
+
edges_path: str | None = None,
|
|
392
|
+
id_field: str = "id",
|
|
393
|
+
label_field: str = "name",
|
|
394
|
+
**kwargs: Any,
|
|
395
|
+
) -> Graph:
|
|
396
|
+
"""Create a Graph from GraphQL JSON response.
|
|
397
|
+
|
|
398
|
+
Parameters
|
|
399
|
+
----------
|
|
400
|
+
result : Any
|
|
401
|
+
GraphQL JSON response.
|
|
402
|
+
nodes_path : str | None
|
|
403
|
+
Dot-separated path to nodes list (e.g., "data.users").
|
|
404
|
+
edges_path : str | None
|
|
405
|
+
Dot-separated path to edges list.
|
|
406
|
+
id_field : str
|
|
407
|
+
Field name for node IDs (default: "id").
|
|
408
|
+
label_field : str
|
|
409
|
+
Field name for node labels (default: "name").
|
|
410
|
+
**kwargs
|
|
411
|
+
Additional arguments passed to Graph constructor.
|
|
412
|
+
|
|
413
|
+
Returns
|
|
414
|
+
-------
|
|
415
|
+
Graph
|
|
416
|
+
New Graph instance with extracted nodes and edges.
|
|
417
|
+
|
|
418
|
+
Example
|
|
419
|
+
-------
|
|
420
|
+
>>> import requests
|
|
421
|
+
>>> response = requests.post(url, json={"query": query})
|
|
422
|
+
>>> graph = Graph.from_graphql(
|
|
423
|
+
... response.json(),
|
|
424
|
+
... nodes_path="data.characters.results",
|
|
425
|
+
... )
|
|
426
|
+
"""
|
|
427
|
+
from anywidget_graph.converters import GraphQLConverter
|
|
428
|
+
|
|
429
|
+
converter = GraphQLConverter(
|
|
430
|
+
nodes_path=nodes_path,
|
|
431
|
+
edges_path=edges_path,
|
|
432
|
+
id_field=id_field,
|
|
433
|
+
label_field=label_field,
|
|
434
|
+
)
|
|
435
|
+
data = converter.convert(result)
|
|
436
|
+
return cls(nodes=data["nodes"], edges=data["edges"], **kwargs)
|
|
186
437
|
|
|
187
438
|
def on_node_click(self, callback: Callable[[str, dict], None]) -> Callable:
|
|
188
|
-
"""Register a callback for node click events.
|
|
439
|
+
"""Register a callback for node click events.
|
|
440
|
+
|
|
441
|
+
Parameters
|
|
442
|
+
----------
|
|
443
|
+
callback : Callable[[str, dict], None]
|
|
444
|
+
Function called with (node_id, node_attributes) when a node is clicked.
|
|
445
|
+
|
|
446
|
+
Returns
|
|
447
|
+
-------
|
|
448
|
+
Callable
|
|
449
|
+
The callback function (for decorator usage).
|
|
450
|
+
"""
|
|
189
451
|
self._node_click_callbacks.append(callback)
|
|
190
452
|
|
|
191
453
|
def observer(change: dict) -> None:
|
|
@@ -199,7 +461,18 @@ class Graph(anywidget.AnyWidget):
|
|
|
199
461
|
return callback
|
|
200
462
|
|
|
201
463
|
def on_edge_click(self, callback: Callable[[dict], None]) -> Callable:
|
|
202
|
-
"""Register a callback for edge click events.
|
|
464
|
+
"""Register a callback for edge click events.
|
|
465
|
+
|
|
466
|
+
Parameters
|
|
467
|
+
----------
|
|
468
|
+
callback : Callable[[dict], None]
|
|
469
|
+
Function called with edge data when an edge is clicked.
|
|
470
|
+
|
|
471
|
+
Returns
|
|
472
|
+
-------
|
|
473
|
+
Callable
|
|
474
|
+
The callback function (for decorator usage).
|
|
475
|
+
"""
|
|
203
476
|
self._edge_click_callbacks.append(callback)
|
|
204
477
|
|
|
205
478
|
def observer(change: dict) -> None:
|
|
@@ -209,85 +482,3 @@ class Graph(anywidget.AnyWidget):
|
|
|
209
482
|
|
|
210
483
|
self.observe(observer, names=["selected_edge"])
|
|
211
484
|
return callback
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def _is_node(obj: Any) -> bool:
|
|
215
|
-
if hasattr(obj, "labels") and hasattr(obj, "element_id"):
|
|
216
|
-
return True
|
|
217
|
-
if hasattr(obj, "labels") and hasattr(obj, "properties"):
|
|
218
|
-
return True
|
|
219
|
-
if isinstance(obj, dict) and "id" in obj:
|
|
220
|
-
return True
|
|
221
|
-
return False
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def _is_relationship(obj: Any) -> bool:
|
|
225
|
-
if hasattr(obj, "type") and hasattr(obj, "start_node"):
|
|
226
|
-
return True
|
|
227
|
-
if hasattr(obj, "type") and hasattr(obj, "source") and hasattr(obj, "target"):
|
|
228
|
-
return True
|
|
229
|
-
if isinstance(obj, dict) and "source" in obj and "target" in obj:
|
|
230
|
-
return True
|
|
231
|
-
return False
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
def _get_node_id(node: Any) -> str:
|
|
235
|
-
if hasattr(node, "element_id"):
|
|
236
|
-
return str(node.element_id)
|
|
237
|
-
if hasattr(node, "id"):
|
|
238
|
-
return str(node.id)
|
|
239
|
-
if isinstance(node, dict):
|
|
240
|
-
return str(node.get("id", id(node)))
|
|
241
|
-
return str(id(node))
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
def _node_to_dict(node: Any) -> dict:
|
|
245
|
-
result: dict[str, Any] = {"id": _get_node_id(node)}
|
|
246
|
-
|
|
247
|
-
if hasattr(node, "labels"):
|
|
248
|
-
labels = list(node.labels) if hasattr(node.labels, "__iter__") else [node.labels]
|
|
249
|
-
if labels:
|
|
250
|
-
result["label"] = labels[0]
|
|
251
|
-
result["labels"] = labels
|
|
252
|
-
|
|
253
|
-
if hasattr(node, "properties"):
|
|
254
|
-
props = node.properties if isinstance(node.properties, dict) else dict(node.properties)
|
|
255
|
-
result.update(props)
|
|
256
|
-
elif hasattr(node, "items"):
|
|
257
|
-
result.update(dict(node))
|
|
258
|
-
elif isinstance(node, dict):
|
|
259
|
-
result.update(node)
|
|
260
|
-
|
|
261
|
-
if "label" not in result and "name" in result:
|
|
262
|
-
result["label"] = result["name"]
|
|
263
|
-
|
|
264
|
-
return result
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def _relationship_to_dict(rel: Any) -> dict:
|
|
268
|
-
result: dict[str, Any] = {}
|
|
269
|
-
|
|
270
|
-
if hasattr(rel, "start_node") and hasattr(rel, "end_node"):
|
|
271
|
-
result["source"] = _get_node_id(rel.start_node)
|
|
272
|
-
result["target"] = _get_node_id(rel.end_node)
|
|
273
|
-
elif hasattr(rel, "source") and hasattr(rel, "target"):
|
|
274
|
-
result["source"] = _get_node_id(rel.source)
|
|
275
|
-
result["target"] = _get_node_id(rel.target)
|
|
276
|
-
elif isinstance(rel, dict):
|
|
277
|
-
result["source"] = str(rel.get("source", ""))
|
|
278
|
-
result["target"] = str(rel.get("target", ""))
|
|
279
|
-
|
|
280
|
-
if hasattr(rel, "type"):
|
|
281
|
-
result["label"] = str(rel.type)
|
|
282
|
-
elif isinstance(rel, dict) and "type" in rel:
|
|
283
|
-
result["label"] = str(rel["type"])
|
|
284
|
-
elif isinstance(rel, dict) and "label" in rel:
|
|
285
|
-
result["label"] = str(rel["label"])
|
|
286
|
-
|
|
287
|
-
if hasattr(rel, "properties"):
|
|
288
|
-
props = rel.properties if isinstance(rel.properties, dict) else dict(rel.properties)
|
|
289
|
-
result.update(props)
|
|
290
|
-
elif hasattr(rel, "items") and not isinstance(rel, dict):
|
|
291
|
-
result.update(dict(rel))
|
|
292
|
-
|
|
293
|
-
return result
|