anywidget-graph 0.1.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.
anywidget_graph/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Main Graph widget using anywidget and Sigma.js."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
import anywidget
|
|
8
|
+
import traitlets
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
|
|
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
|
+
|
|
128
|
+
class Graph(anywidget.AnyWidget):
|
|
129
|
+
"""Interactive graph visualization widget using Sigma.js."""
|
|
130
|
+
|
|
131
|
+
_esm = _ESM
|
|
132
|
+
_css = _CSS
|
|
133
|
+
|
|
134
|
+
nodes = traitlets.List(trait=traitlets.Dict()).tag(sync=True)
|
|
135
|
+
edges = traitlets.List(trait=traitlets.Dict()).tag(sync=True)
|
|
136
|
+
|
|
137
|
+
width = traitlets.Int(default_value=800).tag(sync=True)
|
|
138
|
+
height = traitlets.Int(default_value=600).tag(sync=True)
|
|
139
|
+
background = traitlets.Unicode(default_value="#fafafa").tag(sync=True)
|
|
140
|
+
show_labels = traitlets.Bool(default_value=True).tag(sync=True)
|
|
141
|
+
show_edge_labels = traitlets.Bool(default_value=False).tag(sync=True)
|
|
142
|
+
|
|
143
|
+
selected_node = traitlets.Dict(allow_none=True, default_value=None).tag(sync=True)
|
|
144
|
+
selected_edge = traitlets.Dict(allow_none=True, default_value=None).tag(sync=True)
|
|
145
|
+
|
|
146
|
+
def __init__(
|
|
147
|
+
self,
|
|
148
|
+
nodes: list[dict[str, Any]] | None = None,
|
|
149
|
+
edges: list[dict[str, Any]] | None = None,
|
|
150
|
+
**kwargs: Any,
|
|
151
|
+
) -> None:
|
|
152
|
+
super().__init__(nodes=nodes or [], edges=edges or [], **kwargs)
|
|
153
|
+
self._node_click_callbacks: list[Callable] = []
|
|
154
|
+
self._edge_click_callbacks: list[Callable] = []
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def from_dict(cls, data: dict[str, Any], **kwargs: Any) -> Graph:
|
|
158
|
+
"""Create a Graph from a dictionary with nodes and edges keys."""
|
|
159
|
+
return cls(nodes=data.get("nodes", []), edges=data.get("edges", []), **kwargs)
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def from_cypher(cls, result: Any, **kwargs: Any) -> Graph:
|
|
163
|
+
"""Create a Graph from Cypher query results."""
|
|
164
|
+
nodes: dict[str, dict] = {}
|
|
165
|
+
edges: list[dict] = []
|
|
166
|
+
|
|
167
|
+
records = list(result) if hasattr(result, "__iter__") else [result]
|
|
168
|
+
|
|
169
|
+
for record in records:
|
|
170
|
+
if hasattr(record, "items"):
|
|
171
|
+
items = record.items() if callable(record.items) else record.items
|
|
172
|
+
elif hasattr(record, "data"):
|
|
173
|
+
items = record.data().items()
|
|
174
|
+
else:
|
|
175
|
+
items = record.items() if isinstance(record, dict) else []
|
|
176
|
+
|
|
177
|
+
for key, value in items:
|
|
178
|
+
if _is_node(value):
|
|
179
|
+
node_id = _get_node_id(value)
|
|
180
|
+
if node_id not in nodes:
|
|
181
|
+
nodes[node_id] = _node_to_dict(value)
|
|
182
|
+
elif _is_relationship(value):
|
|
183
|
+
edges.append(_relationship_to_dict(value))
|
|
184
|
+
|
|
185
|
+
return cls(nodes=list(nodes.values()), edges=edges, **kwargs)
|
|
186
|
+
|
|
187
|
+
def on_node_click(self, callback: Callable[[str, dict], None]) -> Callable:
|
|
188
|
+
"""Register a callback for node click events."""
|
|
189
|
+
self._node_click_callbacks.append(callback)
|
|
190
|
+
|
|
191
|
+
def observer(change: dict) -> None:
|
|
192
|
+
if change["new"] is not None:
|
|
193
|
+
node_data = change["new"].copy()
|
|
194
|
+
node_id = node_data.pop("id", None)
|
|
195
|
+
for cb in self._node_click_callbacks:
|
|
196
|
+
cb(node_id, node_data)
|
|
197
|
+
|
|
198
|
+
self.observe(observer, names=["selected_node"])
|
|
199
|
+
return callback
|
|
200
|
+
|
|
201
|
+
def on_edge_click(self, callback: Callable[[dict], None]) -> Callable:
|
|
202
|
+
"""Register a callback for edge click events."""
|
|
203
|
+
self._edge_click_callbacks.append(callback)
|
|
204
|
+
|
|
205
|
+
def observer(change: dict) -> None:
|
|
206
|
+
if change["new"] is not None:
|
|
207
|
+
for cb in self._edge_click_callbacks:
|
|
208
|
+
cb(change["new"])
|
|
209
|
+
|
|
210
|
+
self.observe(observer, names=["selected_edge"])
|
|
211
|
+
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
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: anywidget-graph
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Interactive graph visualization for Python notebooks using anywidget
|
|
5
|
+
Project-URL: Homepage, https://grafeo.dev/
|
|
6
|
+
Project-URL: Repository, https://github.com/GrafeoDB/anywidget-graph
|
|
7
|
+
Author-email: "S.T. Grond" <widget@grafeo.dev>
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Keywords: anywidget,cypher,graph,jupyter,marimo,neo4j,visualization
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Framework :: Jupyter
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: anywidget>=0.9.21
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: marimo>=0.19.7; extra == 'dev'
|
|
24
|
+
Requires-Dist: prek>=0.3.1; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=9.0.2; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.14.14; extra == 'dev'
|
|
27
|
+
Requires-Dist: ty>=0.0.14; extra == 'dev'
|
|
28
|
+
Provides-Extra: networkx
|
|
29
|
+
Requires-Dist: networkx>=3.0; extra == 'networkx'
|
|
30
|
+
Provides-Extra: pandas
|
|
31
|
+
Requires-Dist: pandas>=2.0; extra == 'pandas'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# anywidget-graph
|
|
35
|
+
|
|
36
|
+
Interactive graph visualization for Python notebooks.
|
|
37
|
+
|
|
38
|
+
Works with Marimo, Jupyter, VS Code, Colab, anywhere [anywidget](https://anywidget.dev/) runs.
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- **Universal** — One widget, every notebook environment
|
|
43
|
+
- **Backend-agnostic** — Grafeo, Neo4j, NetworkX, pandas, or raw dicts
|
|
44
|
+
- **Interactive** — Pan, zoom, click, expand neighbors, select paths
|
|
45
|
+
- **Customizable** — Colors, sizes, shapes, layouts
|
|
46
|
+
- **Performant** — Virtualized rendering for large graphs
|
|
47
|
+
- **Exportable** — PNG, SVG, JSON
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv add anywidget-graph
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from anywidget_graph import Graph
|
|
59
|
+
|
|
60
|
+
graph = Graph.from_dict({
|
|
61
|
+
"nodes": [
|
|
62
|
+
{"id": "alice", "label": "Alice", "group": "person"},
|
|
63
|
+
{"id": "bob", "label": "Bob", "group": "person"},
|
|
64
|
+
{"id": "paper", "label": "Graph Theory", "group": "document"},
|
|
65
|
+
],
|
|
66
|
+
"edges": [
|
|
67
|
+
{"source": "alice", "target": "bob", "label": "knows"},
|
|
68
|
+
{"source": "alice", "target": "paper", "label": "authored"},
|
|
69
|
+
]
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
graph
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Data Sources
|
|
76
|
+
|
|
77
|
+
### Dictionary
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from anywidget_graph import Graph
|
|
81
|
+
|
|
82
|
+
graph = Graph.from_dict({
|
|
83
|
+
"nodes": [{"id": "a"}, {"id": "b"}],
|
|
84
|
+
"edges": [{"source": "a", "target": "b"}]
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Grafeo
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from grafeo import GrafeoDB
|
|
92
|
+
from anywidget_graph import Graph
|
|
93
|
+
|
|
94
|
+
db = GrafeoDB()
|
|
95
|
+
db.execute("INSERT (:Person {name: 'Alice'})-[:KNOWS]->(:Person {name: 'Bob'})")
|
|
96
|
+
|
|
97
|
+
result = db.execute("MATCH (a)-[r]->(b) RETURN a, r, b")
|
|
98
|
+
graph = Graph.from_grafeo(result)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Neo4j
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from neo4j import GraphDatabase
|
|
105
|
+
from anywidget_graph import Graph
|
|
106
|
+
|
|
107
|
+
driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password"))
|
|
108
|
+
|
|
109
|
+
with driver.session() as session:
|
|
110
|
+
result = session.run("MATCH (a)-[r]->(b) RETURN a, r, b LIMIT 100")
|
|
111
|
+
graph = Graph.from_neo4j(result)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### NetworkX
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
import networkx as nx
|
|
118
|
+
from anywidget_graph import Graph
|
|
119
|
+
|
|
120
|
+
G = nx.karate_club_graph()
|
|
121
|
+
graph = Graph.from_networkx(G)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### pandas
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
import pandas as pd
|
|
128
|
+
from anywidget_graph import Graph
|
|
129
|
+
|
|
130
|
+
edges = pd.DataFrame({
|
|
131
|
+
"source": ["alice", "alice", "bob"],
|
|
132
|
+
"target": ["bob", "carol", "carol"],
|
|
133
|
+
"weight": [1.0, 0.5, 0.8]
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
graph = Graph.from_pandas(edges)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Interactivity
|
|
140
|
+
|
|
141
|
+
### Events
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
graph = Graph.from_dict(data)
|
|
145
|
+
|
|
146
|
+
@graph.on_node_click
|
|
147
|
+
def handle_node(node_id, node_data):
|
|
148
|
+
print(f"Clicked: {node_id}")
|
|
149
|
+
|
|
150
|
+
@graph.on_edge_click
|
|
151
|
+
def handle_edge(edge_id, edge_data):
|
|
152
|
+
print(f"Edge: {edge_data['label']}")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Selection
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
graph.selected_nodes # Get current selection
|
|
159
|
+
graph.select(["alice"]) # Select nodes
|
|
160
|
+
graph.clear_selection() # Clear
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Expansion
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
graph.expand("alice") # Show neighbors
|
|
167
|
+
graph.collapse("alice") # Hide neighbors
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Styling
|
|
171
|
+
|
|
172
|
+
### By Group
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
graph = Graph.from_dict(
|
|
176
|
+
data,
|
|
177
|
+
node_styles={
|
|
178
|
+
"person": {"color": "#4CAF50", "size": 30},
|
|
179
|
+
"document": {"color": "#2196F3", "shape": "square"},
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### By Property
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
graph = Graph.from_dict(
|
|
188
|
+
data,
|
|
189
|
+
node_color="group", # Color by field
|
|
190
|
+
node_size=lambda n: n["score"] * 10, # Size by function
|
|
191
|
+
edge_width="weight", # Width by field
|
|
192
|
+
)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Layouts
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
Graph.from_dict(data, layout="force") # Default
|
|
199
|
+
Graph.from_dict(data, layout="hierarchical")
|
|
200
|
+
Graph.from_dict(data, layout="circular")
|
|
201
|
+
Graph.from_dict(data, layout="grid")
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Options
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
graph = Graph.from_dict(
|
|
208
|
+
data,
|
|
209
|
+
width=800,
|
|
210
|
+
height=600,
|
|
211
|
+
directed=True,
|
|
212
|
+
labels=True,
|
|
213
|
+
edge_labels=False,
|
|
214
|
+
physics=True,
|
|
215
|
+
zoom=(0.1, 4),
|
|
216
|
+
)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Large Graphs
|
|
220
|
+
|
|
221
|
+
For 1000+ nodes:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
graph = Graph.from_dict(
|
|
225
|
+
data,
|
|
226
|
+
virtualize=True,
|
|
227
|
+
cluster=True,
|
|
228
|
+
)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Export
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
graph.to_png("graph.png")
|
|
235
|
+
graph.to_svg("graph.svg")
|
|
236
|
+
graph.to_json("graph.json")
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Environment Support
|
|
240
|
+
|
|
241
|
+
| Environment | Supported |
|
|
242
|
+
|-------------|-----------|
|
|
243
|
+
| Marimo | ✅ |
|
|
244
|
+
| JupyterLab | ✅ |
|
|
245
|
+
| Jupyter Notebook | ✅ |
|
|
246
|
+
| VS Code | ✅ |
|
|
247
|
+
| Google Colab | ✅ |
|
|
248
|
+
| Databricks | ✅ |
|
|
249
|
+
|
|
250
|
+
## Related
|
|
251
|
+
|
|
252
|
+
- [anywidget](https://anywidget.dev/) — Custom Jupyter widgets made easy
|
|
253
|
+
- [Grafeo](https://github.com/GrafeoDB/grafeo) — Embeddable graph database
|
|
254
|
+
- [grafeo-wasm](https://github.com/GrafeoDB/grafeo-wasm) — Grafeo in the browser
|
|
255
|
+
|
|
256
|
+
## License
|
|
257
|
+
|
|
258
|
+
Apache-2.0
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
anywidget_graph/__init__.py,sha256=uPbg0OSg6iO8dySwupCzx6zc0LWjsjQcnr-VgLQTSEM,145
|
|
2
|
+
anywidget_graph/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
anywidget_graph/widget.py,sha256=HRcbtmJLnOWzrusKy7Ikedoe_HryNJmDHgK4HK3rCYc,9444
|
|
4
|
+
anywidget_graph-0.1.0.dist-info/METADATA,sha256=RP9R6XK4DQkarK-GXsM-9s6hiR7mBElLgv7TSEFL0Q0,5747
|
|
5
|
+
anywidget_graph-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
+
anywidget_graph-0.1.0.dist-info/RECORD,,
|