anywidget-graph 0.2.5__tar.gz → 0.2.7__tar.gz
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-0.2.7/PKG-INFO +332 -0
- anywidget_graph-0.2.7/README.md +291 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/pyproject.toml +6 -6
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/__init__.py +1 -1
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/ui/__init__.py +3 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/ui/icons.js +4 -0
- anywidget_graph-0.2.7/src/anywidget_graph/ui/index.js +810 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/ui/styles.css +155 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/ui/toolbar.js +33 -1
- anywidget_graph-0.2.7/src/anywidget_graph/widget.py +1070 -0
- anywidget_graph-0.2.7/tests/test_graph.py +589 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/uv.lock +29 -29
- anywidget_graph-0.2.5/PKG-INFO +0 -266
- anywidget_graph-0.2.5/README.md +0 -225
- anywidget_graph-0.2.5/src/anywidget_graph/ui/index.js +0 -262
- anywidget_graph-0.2.5/src/anywidget_graph/widget.py +0 -492
- anywidget_graph-0.2.5/tests/test_graph.py +0 -55
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/.github/workflows/ci.yml +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/.github/workflows/pypi.yml +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/.gitignore +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/.python-version +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/LICENSE +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/examples/demo.py +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/backends/__init__.py +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/backends/arango.py +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/backends/cosmosdb.py +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/backends/grafeo.py +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/backends/ladybug.py +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/backends/neo4j.py +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/converters/__init__.py +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/converters/base.py +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/converters/common.py +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/converters/cypher.py +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/converters/gql.py +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/converters/graphql.py +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/converters/gremlin.py +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/converters/sparql.py +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/demo.py +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/py.typed +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/ui/grafeo-embed.js +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/ui/grafeo.js +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/ui/neo4j.js +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/ui/properties.js +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/ui/schema.js +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/src/anywidget_graph/ui/settings.js +0 -0
- {anywidget_graph-0.2.5 → anywidget_graph-0.2.7}/tests/__init__.py +0 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: anywidget-graph
|
|
3
|
+
Version: 0.2.7
|
|
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" <dev@grafeo.dev>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: anywidget,cypher,graph,jupyter,marimo,neo4j,visualization
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Framework :: Jupyter
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Requires-Dist: anywidget>=0.9.21
|
|
23
|
+
Requires-Dist: marimo>=0.19.9
|
|
24
|
+
Provides-Extra: cosmosdb
|
|
25
|
+
Requires-Dist: gremlinpython>=3.7; extra == 'cosmosdb'
|
|
26
|
+
Provides-Extra: demo
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: marimo>=0.20; extra == 'dev'
|
|
29
|
+
Requires-Dist: prek>=0.3; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=9.0.2; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.15; extra == 'dev'
|
|
32
|
+
Requires-Dist: ty>=0.0.20; extra == 'dev'
|
|
33
|
+
Provides-Extra: embed
|
|
34
|
+
Provides-Extra: grafeo
|
|
35
|
+
Requires-Dist: grafeo>=0.5; extra == 'grafeo'
|
|
36
|
+
Provides-Extra: networkx
|
|
37
|
+
Requires-Dist: networkx>=3.0; extra == 'networkx'
|
|
38
|
+
Provides-Extra: pandas
|
|
39
|
+
Requires-Dist: pandas>=2.0; extra == 'pandas'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# anywidget-graph
|
|
43
|
+
|
|
44
|
+
Interactive graph visualization for Python notebooks.
|
|
45
|
+
|
|
46
|
+
Works with Marimo, Jupyter, VS Code, Colab, anywhere [anywidget](https://anywidget.dev/) runs.
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
- **Universal**: One widget, every notebook environment
|
|
51
|
+
- **Backend-agnostic**: Grafeo, Neo4j, NetworkX, pandas, or raw dicts
|
|
52
|
+
- **Interactive**: Pan, zoom, click, drag, pin, expand neighbors, box select
|
|
53
|
+
- **Customizable**: Colors, sizes, layouts, dark mode
|
|
54
|
+
- **Exportable**: HTML, JSON
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv add anywidget-graph
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Optional extras:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uv add "anywidget-graph[networkx]" # NetworkX support
|
|
66
|
+
uv add "anywidget-graph[pandas]" # pandas support
|
|
67
|
+
uv add "anywidget-graph[grafeo]" # Grafeo backend
|
|
68
|
+
uv add "anywidget-graph[cosmosdb]" # CosmosDB / Gremlin support
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Quick Start
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from anywidget_graph import Graph
|
|
75
|
+
|
|
76
|
+
graph = Graph.from_dict({
|
|
77
|
+
"nodes": [
|
|
78
|
+
{"id": "alice", "label": "Alice", "group": "person"},
|
|
79
|
+
{"id": "bob", "label": "Bob", "group": "person"},
|
|
80
|
+
{"id": "paper", "label": "Graph Theory", "group": "document"},
|
|
81
|
+
],
|
|
82
|
+
"edges": [
|
|
83
|
+
{"source": "alice", "target": "bob", "label": "knows"},
|
|
84
|
+
{"source": "alice", "target": "paper", "label": "authored"},
|
|
85
|
+
]
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
graph
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Data Sources
|
|
92
|
+
|
|
93
|
+
### Dictionary
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
graph = Graph.from_dict({
|
|
97
|
+
"nodes": [{"id": "a"}, {"id": "b"}],
|
|
98
|
+
"edges": [{"source": "a", "target": "b"}]
|
|
99
|
+
})
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Direct initialization
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
graph = Graph(
|
|
106
|
+
nodes=[{"id": "a", "label": "Alice"}, {"id": "b", "label": "Bob"}],
|
|
107
|
+
edges=[{"source": "a", "target": "b", "label": "KNOWS"}],
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Cypher results (Neo4j)
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from neo4j import GraphDatabase
|
|
115
|
+
|
|
116
|
+
driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password"))
|
|
117
|
+
|
|
118
|
+
with driver.session() as session:
|
|
119
|
+
result = session.run("MATCH (a)-[r]->(b) RETURN a, r, b LIMIT 100")
|
|
120
|
+
graph = Graph.from_cypher(result)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### GQL results
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
graph = Graph.from_gql(result)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### SPARQL results
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from rdflib import Graph as RDFGraph
|
|
133
|
+
|
|
134
|
+
g = RDFGraph()
|
|
135
|
+
g.parse("data.ttl")
|
|
136
|
+
result = g.query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
|
|
137
|
+
graph = Graph.from_sparql(result)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Gremlin results (CosmosDB, TinkerPop)
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
graph = Graph.from_gremlin(result)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### GraphQL results
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
graph = Graph.from_graphql(
|
|
150
|
+
response.json(),
|
|
151
|
+
nodes_path="data.characters.results",
|
|
152
|
+
id_field="id",
|
|
153
|
+
label_field="name",
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### NetworkX
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
import networkx as nx
|
|
161
|
+
|
|
162
|
+
G = nx.karate_club_graph()
|
|
163
|
+
graph = Graph.from_networkx(G)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### pandas DataFrames
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
import pandas as pd
|
|
170
|
+
|
|
171
|
+
nodes_df = pd.DataFrame({"id": ["alice", "bob"], "group": ["person", "person"]})
|
|
172
|
+
edges_df = pd.DataFrame({"source": ["alice"], "target": ["bob"], "weight": [1.0]})
|
|
173
|
+
|
|
174
|
+
graph = Graph.from_dataframe(nodes_df, edges_df)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Interactivity
|
|
178
|
+
|
|
179
|
+
### Events
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
graph = Graph.from_dict(data)
|
|
183
|
+
|
|
184
|
+
@graph.on_node_click
|
|
185
|
+
def handle_node(node_id, node_data):
|
|
186
|
+
print(f"Clicked: {node_id}")
|
|
187
|
+
|
|
188
|
+
@graph.on_edge_click
|
|
189
|
+
def handle_edge(edge_data):
|
|
190
|
+
print(f"Edge: {edge_data['label']}")
|
|
191
|
+
|
|
192
|
+
@graph.on_selection
|
|
193
|
+
def handle_selection(node_ids):
|
|
194
|
+
print(f"Selected: {node_ids}")
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Selection
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
graph.selected_nodes # Current selection (list of IDs)
|
|
201
|
+
graph.selection_mode = "box" # Switch to box-select mode
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Node expansion
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
graph.expand_node("alice") # Fetch and merge neighbors (requires backend)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Node pinning
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
graph.pin_nodes(["alice", "bob"]) # Pin at current positions
|
|
214
|
+
graph.unpin_nodes(["alice"]) # Release back to layout
|
|
215
|
+
graph.toggle_pin("bob") # Toggle pin state
|
|
216
|
+
graph.unpin_all() # Unpin everything
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Clear
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
graph.clear() # Remove all nodes, edges, pins, and selection
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Styling
|
|
226
|
+
|
|
227
|
+
### Property-based coloring
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
graph = Graph.from_dict(
|
|
231
|
+
data,
|
|
232
|
+
color_field="group", # Color nodes by field
|
|
233
|
+
color_scale="viridis", # Scale: viridis, plasma, inferno, magma, cividis, turbo
|
|
234
|
+
size_field="score", # Size nodes by field
|
|
235
|
+
size_range=[5, 30], # Min/max node size
|
|
236
|
+
)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Edge styling
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
graph.edge_color_field = "type"
|
|
243
|
+
graph.edge_color_scale = "plasma"
|
|
244
|
+
graph.edge_size_field = "weight"
|
|
245
|
+
graph.edge_size_range = [1, 8]
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Layouts
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
Graph.from_dict(data, layout="force") # ForceAtlas2 (default)
|
|
252
|
+
Graph.from_dict(data, layout="circular")
|
|
253
|
+
Graph.from_dict(data, layout="random")
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Options
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
graph = Graph(
|
|
260
|
+
nodes=nodes,
|
|
261
|
+
edges=edges,
|
|
262
|
+
width=800, # Widget width (px)
|
|
263
|
+
height=600, # Widget height (px)
|
|
264
|
+
background="#fafafa", # Background color
|
|
265
|
+
show_labels=True, # Node labels
|
|
266
|
+
show_edge_labels=False, # Edge labels
|
|
267
|
+
show_toolbar=True, # Toolbar visibility
|
|
268
|
+
show_settings=True, # Settings panel
|
|
269
|
+
show_query_input=True, # Query input box
|
|
270
|
+
dark_mode=True, # Dark theme
|
|
271
|
+
show_tooltip=True, # Hover tooltips
|
|
272
|
+
tooltip_fields=["label", "id"],
|
|
273
|
+
max_nodes=300, # Limit for node expansion
|
|
274
|
+
)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Database Backends
|
|
278
|
+
|
|
279
|
+
### Grafeo (default)
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
import grafeo
|
|
283
|
+
db = grafeo.GrafeoDB()
|
|
284
|
+
graph = Graph(database_backend="grafeo", grafeo_db=db)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Neo4j (browser-side)
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
graph = Graph(
|
|
291
|
+
database_backend="neo4j",
|
|
292
|
+
connection_uri="neo4j+s://demo.neo4jlabs.com",
|
|
293
|
+
connection_username="neo4j",
|
|
294
|
+
connection_password="password",
|
|
295
|
+
)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Generic backend
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
graph = Graph(backend=my_backend) # Any object implementing DatabaseBackend protocol
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Export
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
graph.to_json() # JSON string with nodes and edges
|
|
308
|
+
graph.to_html() # Self-contained HTML string
|
|
309
|
+
graph.to_html(title="My Graph") # Custom title
|
|
310
|
+
graph.save_html("graph.html") # Write HTML to file
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Environment Support
|
|
314
|
+
|
|
315
|
+
| Environment | Supported |
|
|
316
|
+
|-------------|-----------|
|
|
317
|
+
| Marimo | Yes |
|
|
318
|
+
| JupyterLab | Yes |
|
|
319
|
+
| Jupyter Notebook | Yes |
|
|
320
|
+
| VS Code | Yes |
|
|
321
|
+
| Google Colab | Yes |
|
|
322
|
+
| Databricks | Yes |
|
|
323
|
+
|
|
324
|
+
## Related
|
|
325
|
+
|
|
326
|
+
- [anywidget](https://anywidget.dev/), custom Jupyter widgets made easy
|
|
327
|
+
- [Grafeo](https://github.com/GrafeoDB/grafeo), embeddable graph database
|
|
328
|
+
- [grafeo-web](https://github.com/GrafeoDB/grafeo-web), Grafeo in the browser
|
|
329
|
+
|
|
330
|
+
## License
|
|
331
|
+
|
|
332
|
+
Apache-2.0
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# anywidget-graph
|
|
2
|
+
|
|
3
|
+
Interactive graph visualization for Python notebooks.
|
|
4
|
+
|
|
5
|
+
Works with Marimo, Jupyter, VS Code, Colab, anywhere [anywidget](https://anywidget.dev/) runs.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Universal**: One widget, every notebook environment
|
|
10
|
+
- **Backend-agnostic**: Grafeo, Neo4j, NetworkX, pandas, or raw dicts
|
|
11
|
+
- **Interactive**: Pan, zoom, click, drag, pin, expand neighbors, box select
|
|
12
|
+
- **Customizable**: Colors, sizes, layouts, dark mode
|
|
13
|
+
- **Exportable**: HTML, JSON
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv add anywidget-graph
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Optional extras:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv add "anywidget-graph[networkx]" # NetworkX support
|
|
25
|
+
uv add "anywidget-graph[pandas]" # pandas support
|
|
26
|
+
uv add "anywidget-graph[grafeo]" # Grafeo backend
|
|
27
|
+
uv add "anywidget-graph[cosmosdb]" # CosmosDB / Gremlin support
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from anywidget_graph import Graph
|
|
34
|
+
|
|
35
|
+
graph = Graph.from_dict({
|
|
36
|
+
"nodes": [
|
|
37
|
+
{"id": "alice", "label": "Alice", "group": "person"},
|
|
38
|
+
{"id": "bob", "label": "Bob", "group": "person"},
|
|
39
|
+
{"id": "paper", "label": "Graph Theory", "group": "document"},
|
|
40
|
+
],
|
|
41
|
+
"edges": [
|
|
42
|
+
{"source": "alice", "target": "bob", "label": "knows"},
|
|
43
|
+
{"source": "alice", "target": "paper", "label": "authored"},
|
|
44
|
+
]
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
graph
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Data Sources
|
|
51
|
+
|
|
52
|
+
### Dictionary
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
graph = Graph.from_dict({
|
|
56
|
+
"nodes": [{"id": "a"}, {"id": "b"}],
|
|
57
|
+
"edges": [{"source": "a", "target": "b"}]
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Direct initialization
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
graph = Graph(
|
|
65
|
+
nodes=[{"id": "a", "label": "Alice"}, {"id": "b", "label": "Bob"}],
|
|
66
|
+
edges=[{"source": "a", "target": "b", "label": "KNOWS"}],
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Cypher results (Neo4j)
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from neo4j import GraphDatabase
|
|
74
|
+
|
|
75
|
+
driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password"))
|
|
76
|
+
|
|
77
|
+
with driver.session() as session:
|
|
78
|
+
result = session.run("MATCH (a)-[r]->(b) RETURN a, r, b LIMIT 100")
|
|
79
|
+
graph = Graph.from_cypher(result)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### GQL results
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
graph = Graph.from_gql(result)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### SPARQL results
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from rdflib import Graph as RDFGraph
|
|
92
|
+
|
|
93
|
+
g = RDFGraph()
|
|
94
|
+
g.parse("data.ttl")
|
|
95
|
+
result = g.query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
|
|
96
|
+
graph = Graph.from_sparql(result)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Gremlin results (CosmosDB, TinkerPop)
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
graph = Graph.from_gremlin(result)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### GraphQL results
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
graph = Graph.from_graphql(
|
|
109
|
+
response.json(),
|
|
110
|
+
nodes_path="data.characters.results",
|
|
111
|
+
id_field="id",
|
|
112
|
+
label_field="name",
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### NetworkX
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import networkx as nx
|
|
120
|
+
|
|
121
|
+
G = nx.karate_club_graph()
|
|
122
|
+
graph = Graph.from_networkx(G)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### pandas DataFrames
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
import pandas as pd
|
|
129
|
+
|
|
130
|
+
nodes_df = pd.DataFrame({"id": ["alice", "bob"], "group": ["person", "person"]})
|
|
131
|
+
edges_df = pd.DataFrame({"source": ["alice"], "target": ["bob"], "weight": [1.0]})
|
|
132
|
+
|
|
133
|
+
graph = Graph.from_dataframe(nodes_df, edges_df)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Interactivity
|
|
137
|
+
|
|
138
|
+
### Events
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
graph = Graph.from_dict(data)
|
|
142
|
+
|
|
143
|
+
@graph.on_node_click
|
|
144
|
+
def handle_node(node_id, node_data):
|
|
145
|
+
print(f"Clicked: {node_id}")
|
|
146
|
+
|
|
147
|
+
@graph.on_edge_click
|
|
148
|
+
def handle_edge(edge_data):
|
|
149
|
+
print(f"Edge: {edge_data['label']}")
|
|
150
|
+
|
|
151
|
+
@graph.on_selection
|
|
152
|
+
def handle_selection(node_ids):
|
|
153
|
+
print(f"Selected: {node_ids}")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Selection
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
graph.selected_nodes # Current selection (list of IDs)
|
|
160
|
+
graph.selection_mode = "box" # Switch to box-select mode
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Node expansion
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
graph.expand_node("alice") # Fetch and merge neighbors (requires backend)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Node pinning
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
graph.pin_nodes(["alice", "bob"]) # Pin at current positions
|
|
173
|
+
graph.unpin_nodes(["alice"]) # Release back to layout
|
|
174
|
+
graph.toggle_pin("bob") # Toggle pin state
|
|
175
|
+
graph.unpin_all() # Unpin everything
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Clear
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
graph.clear() # Remove all nodes, edges, pins, and selection
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Styling
|
|
185
|
+
|
|
186
|
+
### Property-based coloring
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
graph = Graph.from_dict(
|
|
190
|
+
data,
|
|
191
|
+
color_field="group", # Color nodes by field
|
|
192
|
+
color_scale="viridis", # Scale: viridis, plasma, inferno, magma, cividis, turbo
|
|
193
|
+
size_field="score", # Size nodes by field
|
|
194
|
+
size_range=[5, 30], # Min/max node size
|
|
195
|
+
)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Edge styling
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
graph.edge_color_field = "type"
|
|
202
|
+
graph.edge_color_scale = "plasma"
|
|
203
|
+
graph.edge_size_field = "weight"
|
|
204
|
+
graph.edge_size_range = [1, 8]
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Layouts
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
Graph.from_dict(data, layout="force") # ForceAtlas2 (default)
|
|
211
|
+
Graph.from_dict(data, layout="circular")
|
|
212
|
+
Graph.from_dict(data, layout="random")
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Options
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
graph = Graph(
|
|
219
|
+
nodes=nodes,
|
|
220
|
+
edges=edges,
|
|
221
|
+
width=800, # Widget width (px)
|
|
222
|
+
height=600, # Widget height (px)
|
|
223
|
+
background="#fafafa", # Background color
|
|
224
|
+
show_labels=True, # Node labels
|
|
225
|
+
show_edge_labels=False, # Edge labels
|
|
226
|
+
show_toolbar=True, # Toolbar visibility
|
|
227
|
+
show_settings=True, # Settings panel
|
|
228
|
+
show_query_input=True, # Query input box
|
|
229
|
+
dark_mode=True, # Dark theme
|
|
230
|
+
show_tooltip=True, # Hover tooltips
|
|
231
|
+
tooltip_fields=["label", "id"],
|
|
232
|
+
max_nodes=300, # Limit for node expansion
|
|
233
|
+
)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Database Backends
|
|
237
|
+
|
|
238
|
+
### Grafeo (default)
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
import grafeo
|
|
242
|
+
db = grafeo.GrafeoDB()
|
|
243
|
+
graph = Graph(database_backend="grafeo", grafeo_db=db)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Neo4j (browser-side)
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
graph = Graph(
|
|
250
|
+
database_backend="neo4j",
|
|
251
|
+
connection_uri="neo4j+s://demo.neo4jlabs.com",
|
|
252
|
+
connection_username="neo4j",
|
|
253
|
+
connection_password="password",
|
|
254
|
+
)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Generic backend
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
graph = Graph(backend=my_backend) # Any object implementing DatabaseBackend protocol
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Export
|
|
264
|
+
|
|
265
|
+
```python
|
|
266
|
+
graph.to_json() # JSON string with nodes and edges
|
|
267
|
+
graph.to_html() # Self-contained HTML string
|
|
268
|
+
graph.to_html(title="My Graph") # Custom title
|
|
269
|
+
graph.save_html("graph.html") # Write HTML to file
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Environment Support
|
|
273
|
+
|
|
274
|
+
| Environment | Supported |
|
|
275
|
+
|-------------|-----------|
|
|
276
|
+
| Marimo | Yes |
|
|
277
|
+
| JupyterLab | Yes |
|
|
278
|
+
| Jupyter Notebook | Yes |
|
|
279
|
+
| VS Code | Yes |
|
|
280
|
+
| Google Colab | Yes |
|
|
281
|
+
| Databricks | Yes |
|
|
282
|
+
|
|
283
|
+
## Related
|
|
284
|
+
|
|
285
|
+
- [anywidget](https://anywidget.dev/), custom Jupyter widgets made easy
|
|
286
|
+
- [Grafeo](https://github.com/GrafeoDB/grafeo), embeddable graph database
|
|
287
|
+
- [grafeo-web](https://github.com/GrafeoDB/grafeo-web), Grafeo in the browser
|
|
288
|
+
|
|
289
|
+
## License
|
|
290
|
+
|
|
291
|
+
Apache-2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "anywidget-graph"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.7"
|
|
4
4
|
description = "Interactive graph visualization for Python notebooks using anywidget"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -27,15 +27,15 @@ dependencies = [
|
|
|
27
27
|
|
|
28
28
|
[project.optional-dependencies]
|
|
29
29
|
dev = [
|
|
30
|
-
"prek>=0.3
|
|
30
|
+
"prek>=0.3",
|
|
31
31
|
"pytest>=9.0.2",
|
|
32
|
-
"ruff>=0.15
|
|
33
|
-
"ty>=0.0.
|
|
34
|
-
"marimo>=0.
|
|
32
|
+
"ruff>=0.15",
|
|
33
|
+
"ty>=0.0.20",
|
|
34
|
+
"marimo>=0.20",
|
|
35
35
|
]
|
|
36
36
|
networkx = ["networkx>=3.0"]
|
|
37
37
|
pandas = ["pandas>=2.0"]
|
|
38
|
-
grafeo = ["grafeo>=0.5
|
|
38
|
+
grafeo = ["grafeo>=0.5"]
|
|
39
39
|
cosmosdb = ["gremlinpython>=3.7"]
|
|
40
40
|
embed = [] # Marker extra — WASM loads from CDN on demand
|
|
41
41
|
demo = [] # Marker extra — enables demo mode with pre-populated dataset
|
|
@@ -147,6 +147,9 @@ def get_esm() -> str:
|
|
|
147
147
|
// === Auto-generated ESM bundle for anywidget-graph ===
|
|
148
148
|
import Graph from "https://esm.sh/graphology@0.25.4";
|
|
149
149
|
import Sigma from "https://esm.sh/sigma@3.0.0";
|
|
150
|
+
import circular from "https://esm.sh/graphology-layout@0.6.1/circular.js";
|
|
151
|
+
import random from "https://esm.sh/graphology-layout@0.6.1/random.js";
|
|
152
|
+
import forceAtlas2 from "https://esm.sh/graphology-layout-forceatlas2@0.10.1";
|
|
150
153
|
import neo4j from "https://cdn.jsdelivr.net/npm/neo4j-driver@5.28.0/lib/browser/neo4j-web.esm.min.js";
|
|
151
154
|
|
|
152
155
|
// === Icons ===
|
|
@@ -12,4 +12,8 @@ export const ICONS = {
|
|
|
12
12
|
property: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M4 12h16M4 17h10"/></svg>`,
|
|
13
13
|
sidebar: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/></svg>`,
|
|
14
14
|
refresh: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg>`,
|
|
15
|
+
zoomIn: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>`,
|
|
16
|
+
zoomOut: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>`,
|
|
17
|
+
zoomFit: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>`,
|
|
18
|
+
search: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`,
|
|
15
19
|
};
|