anywidget-graph 0.2.9__tar.gz → 0.3.0__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.9 → anywidget_graph-0.3.0}/CHANGELOG.md +29 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/PKG-INFO +3 -1
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/README.md +2 -0
- anywidget_graph-0.3.0/assets/demo.png +0 -0
- anywidget_graph-0.3.0/assets/widget.png +0 -0
- anywidget_graph-0.3.0/examples/test_hierarchy.py +99 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/pyproject.toml +1 -1
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/ui/__init__.py +1 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/ui/index.js +153 -29
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/ui/schema.js +6 -3
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/ui/settings.js +5 -5
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/ui/styles.css +24 -19
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/widget.py +2 -2
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/tests/test_graph.py +1 -1
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/uv.lock +1 -1
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/.github/workflows/ci.yml +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/.github/workflows/pypi.yml +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/.gitignore +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/.python-version +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/LICENSE +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/examples/demo.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/examples/test_large_graph.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/__init__.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/backends/__init__.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/backends/arango.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/backends/cosmosdb.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/backends/grafeo.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/backends/ladybug.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/backends/neo4j.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/converters/__init__.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/converters/base.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/converters/common.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/converters/cypher.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/converters/gql.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/converters/graphql.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/converters/gremlin.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/converters/sparql.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/demo.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/py.typed +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/ui/grafeo-embed.js +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/ui/grafeo.js +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/ui/icons.js +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/ui/neo4j.js +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/ui/properties.js +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/ui/results.js +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/src/anywidget_graph/ui/toolbar.js +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.3.0}/tests/__init__.py +0 -0
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### New Features
|
|
6
|
+
|
|
7
|
+
- **Automatic dark mode**: Detects host theme (marimo `class="dark"`, `data-theme`, `prefers-color-scheme`) and syncs automatically via MutationObserver
|
|
8
|
+
|
|
9
|
+
### Improvements
|
|
10
|
+
|
|
11
|
+
- Increased node spread in spring layout: stronger charge repulsion, larger link distance and collision radius for clearer hub-spoke separation
|
|
12
|
+
- Spring layout renamed to "Default" in the layout dropdown
|
|
13
|
+
- Updated color palette: blue accent and slate-based neutrals, replacing the previous indigo tint
|
|
14
|
+
- Theme toggle auto-hidden when running inside a themed host (e.g. marimo)
|
|
15
|
+
- Dark mode label colors update dynamically with theme changes
|
|
16
|
+
|
|
17
|
+
## 0.2.10
|
|
18
|
+
|
|
19
|
+
### New Features
|
|
20
|
+
|
|
21
|
+
- **d3-force spring layout**: New default layout using d3-force with charge, link-distance and collision forces; produces clean hub-spoke rings for hierarchical data
|
|
22
|
+
- **Degree-based node sizing**: Nodes auto-sized by connection count when no explicit `size_field` is set; hub nodes appear larger, pushing neighbors into natural rings
|
|
23
|
+
- **Filter-aware layouts**: Switching layout or clicking refresh re-runs the layout using only visible nodes, respecting type filters and search state
|
|
24
|
+
|
|
25
|
+
### Improvements
|
|
26
|
+
|
|
27
|
+
- Spring layout is the new default
|
|
28
|
+
- Force layout uses `linLogMode` for better community separation
|
|
29
|
+
- Cluster layout filters out hidden node types before packing
|
|
30
|
+
- Filter panel refresh button triggers a layout refresh
|
|
31
|
+
|
|
3
32
|
## 0.2.9
|
|
4
33
|
|
|
5
34
|
### New Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anywidget-graph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Interactive graph visualization for Python notebooks using anywidget
|
|
5
5
|
Project-URL: Homepage, https://grafeo.dev/
|
|
6
6
|
Project-URL: Repository, https://github.com/GrafeoDB/anywidget-graph
|
|
@@ -45,6 +45,8 @@ Interactive graph visualization for Python notebooks.
|
|
|
45
45
|
|
|
46
46
|
Works with Marimo, Jupyter, VS Code, Colab, anywhere [anywidget](https://anywidget.dev/) runs.
|
|
47
47
|
|
|
48
|
+

|
|
49
|
+
|
|
48
50
|
## Features
|
|
49
51
|
|
|
50
52
|
- **Universal**: One widget, every notebook environment
|
|
@@ -4,6 +4,8 @@ Interactive graph visualization for Python notebooks.
|
|
|
4
4
|
|
|
5
5
|
Works with Marimo, Jupyter, VS Code, Colab, anywhere [anywidget](https://anywidget.dev/) runs.
|
|
6
6
|
|
|
7
|
+

|
|
8
|
+
|
|
7
9
|
## Features
|
|
8
10
|
|
|
9
11
|
- **Universal**: One widget, every notebook environment
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Marimo demo: hierarchical repo-like graph to test force layout with hub-spoke data."""
|
|
2
|
+
|
|
3
|
+
import marimo
|
|
4
|
+
|
|
5
|
+
__generated_with = "0.19.5"
|
|
6
|
+
app = marimo.App(width="medium")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@app.cell
|
|
10
|
+
def _():
|
|
11
|
+
import marimo as mo
|
|
12
|
+
|
|
13
|
+
return (mo,)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.cell
|
|
17
|
+
def _():
|
|
18
|
+
from anywidget_graph import Graph
|
|
19
|
+
|
|
20
|
+
return (Graph,)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.cell
|
|
24
|
+
def _(Graph):
|
|
25
|
+
# Simulate a repository structure:
|
|
26
|
+
# 1 Repository -> 4 Directories -> Files -> TypeDefinitions
|
|
27
|
+
nodes = [
|
|
28
|
+
{"id": "repo", "labels": ["Repository"], "name": "my-app"},
|
|
29
|
+
]
|
|
30
|
+
edges = []
|
|
31
|
+
|
|
32
|
+
dirs = ["src", "lib", "tests", "config"]
|
|
33
|
+
file_id = 0
|
|
34
|
+
type_id = 0
|
|
35
|
+
|
|
36
|
+
for d in dirs:
|
|
37
|
+
did = f"dir_{d}"
|
|
38
|
+
nodes.append({"id": did, "labels": ["Directory"], "name": d})
|
|
39
|
+
edges.append({"source": "repo", "target": did, "label": "CONTAINS", "type": "CONTAINS"})
|
|
40
|
+
|
|
41
|
+
# Each directory has 3-8 files
|
|
42
|
+
import random
|
|
43
|
+
|
|
44
|
+
random.seed(hash(d))
|
|
45
|
+
n_files = random.randint(3, 8)
|
|
46
|
+
for i in range(n_files):
|
|
47
|
+
fid = f"file_{file_id}"
|
|
48
|
+
fname = f"{d}/file_{i}.py"
|
|
49
|
+
nodes.append({"id": fid, "labels": ["File"], "name": fname})
|
|
50
|
+
edges.append({"source": did, "target": fid, "label": "CONTAINS", "type": "CONTAINS"})
|
|
51
|
+
file_id += 1
|
|
52
|
+
|
|
53
|
+
# Each file has 1-5 type definitions
|
|
54
|
+
n_types = random.randint(1, 5)
|
|
55
|
+
for _j in range(n_types):
|
|
56
|
+
tid = f"type_{type_id}"
|
|
57
|
+
tname = f"Class_{type_id}"
|
|
58
|
+
nodes.append({"id": tid, "labels": ["TypeDefinition"], "name": tname})
|
|
59
|
+
edges.append({"source": fid, "target": tid, "label": "DEFINES", "type": "DEFINES"})
|
|
60
|
+
type_id += 1
|
|
61
|
+
|
|
62
|
+
# Some types depend on other types (cross-file references)
|
|
63
|
+
if type_id > 3 and random.random() < 0.3:
|
|
64
|
+
dep = f"type_{random.randint(0, type_id - 2)}"
|
|
65
|
+
edges.append({"source": tid, "target": dep, "label": "DEPENDS_ON", "type": "DEPENDS_ON"})
|
|
66
|
+
|
|
67
|
+
# Some cross-directory imports
|
|
68
|
+
if d != "src":
|
|
69
|
+
edges.append(
|
|
70
|
+
{
|
|
71
|
+
"source": did,
|
|
72
|
+
"target": "dir_src",
|
|
73
|
+
"label": "IMPORTS_FROM",
|
|
74
|
+
"type": "IMPORTS_FROM",
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
graph = Graph.from_dict(
|
|
79
|
+
{"nodes": nodes, "edges": edges},
|
|
80
|
+
width=1000,
|
|
81
|
+
height=700,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
graph # noqa: B018
|
|
85
|
+
return (graph,)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@app.cell
|
|
89
|
+
def _(graph, mo):
|
|
90
|
+
mo.md(f"""
|
|
91
|
+
**Selected node:** {graph.selected_node}
|
|
92
|
+
|
|
93
|
+
Hierarchical repo graph: Repository -> Directories -> Files -> TypeDefinitions
|
|
94
|
+
""")
|
|
95
|
+
return ()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
app.run()
|
|
@@ -151,6 +151,7 @@ import Sigma from "https://esm.sh/sigma@3.0.0";
|
|
|
151
151
|
import circular from "https://esm.sh/graphology-layout@0.6.1/circular.js";
|
|
152
152
|
import random from "https://esm.sh/graphology-layout@0.6.1/random.js";
|
|
153
153
|
import forceAtlas2 from "https://esm.sh/graphology-layout-forceatlas2@0.10.1";
|
|
154
|
+
import * as d3Force from "https://esm.sh/d3-force@3.0.0";
|
|
154
155
|
import neo4j from "https://cdn.jsdelivr.net/npm/neo4j-driver@5.28.0/lib/browser/neo4j-web.esm.min.js";
|
|
155
156
|
|
|
156
157
|
// === Icons ===
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import Graph from "https://esm.sh/graphology@0.25.4";
|
|
6
6
|
import Sigma from "https://esm.sh/sigma@3.0.0";
|
|
7
|
+
import * as d3Force from "https://esm.sh/d3-force@3.0.0";
|
|
7
8
|
|
|
8
9
|
import { createToolbar } from "./toolbar.js";
|
|
9
10
|
import { createSchemaPanel } from "./schema.js";
|
|
@@ -97,12 +98,18 @@ function computeNodeColor(node, colorField, colorScale, colorDomain) {
|
|
|
97
98
|
return getCategoricalColor(value);
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
function computeNodeSize(node, sizeField, sizeDomain, sizeRange) {
|
|
101
|
+
function computeNodeSize(node, sizeField, sizeDomain, sizeRange, degree) {
|
|
101
102
|
if (node.size !== undefined) return node.size;
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
if (sizeField && node[sizeField] !== undefined && sizeDomain) {
|
|
104
|
+
const [min, max] = sizeDomain;
|
|
105
|
+
const t = max > min ? (node[sizeField] - min) / (max - min) : 0.5;
|
|
106
|
+
return sizeRange[0] + t * (sizeRange[1] - sizeRange[0]);
|
|
107
|
+
}
|
|
108
|
+
// Auto-size by degree: hub nodes get bigger, leaf nodes smaller
|
|
109
|
+
if (degree !== undefined && degree > 0) {
|
|
110
|
+
return Math.max(4, Math.min(30, 4 + Math.sqrt(degree) * 3));
|
|
111
|
+
}
|
|
112
|
+
return 8;
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
function autoLabel(node) {
|
|
@@ -152,10 +159,20 @@ function getStylingOpts(model, nodes, edges) {
|
|
|
152
159
|
if (vals.length > 0) edgeSizeDomain = [Math.min(...vals), Math.max(...vals)];
|
|
153
160
|
}
|
|
154
161
|
|
|
162
|
+
// Compute degree map for auto-sizing when no sizeField is set
|
|
163
|
+
const degreeMap = new Map();
|
|
164
|
+
if (!sizeField) {
|
|
165
|
+
edges.forEach((e) => {
|
|
166
|
+
degreeMap.set(e.source, (degreeMap.get(e.source) || 0) + 1);
|
|
167
|
+
degreeMap.set(e.target, (degreeMap.get(e.target) || 0) + 1);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
155
171
|
return {
|
|
156
172
|
colorField, colorScale, colorDomain, sizeField, sizeDomain, sizeRange,
|
|
157
173
|
edgeColorField, edgeColorScale, edgeColorDomain,
|
|
158
174
|
edgeSizeField, edgeSizeDomain, edgeSizeRange,
|
|
175
|
+
degreeMap,
|
|
159
176
|
};
|
|
160
177
|
}
|
|
161
178
|
|
|
@@ -166,7 +183,7 @@ function buildNodeAttrs(node, opts) {
|
|
|
166
183
|
nodeType: (node.labels && node.labels.length > 0) ? node.labels[0] : (node.label || "Unlabeled"),
|
|
167
184
|
x: node.x ?? Math.random() * 100,
|
|
168
185
|
y: node.y ?? Math.random() * 100,
|
|
169
|
-
size: computeNodeSize(node, opts.sizeField, opts.sizeDomain, opts.sizeRange),
|
|
186
|
+
size: computeNodeSize(node, opts.sizeField, opts.sizeDomain, opts.sizeRange, opts.degreeMap.get(node.id)),
|
|
170
187
|
color: computeNodeColor(node, opts.colorField, opts.colorScale, opts.colorDomain),
|
|
171
188
|
};
|
|
172
189
|
}
|
|
@@ -266,15 +283,45 @@ function render({ model, el }) {
|
|
|
266
283
|
wrapper.style.height = model.get("height") + "px";
|
|
267
284
|
wrapper.style.maxHeight = model.get("height") + "px";
|
|
268
285
|
|
|
269
|
-
//
|
|
286
|
+
// Auto-detect host theme (marimo uses Tailwind class="dark" on <html>)
|
|
287
|
+
function detectHostDark() {
|
|
288
|
+
const html = document.documentElement;
|
|
289
|
+
if (html.classList.contains("dark")) return true;
|
|
290
|
+
if (html.dataset.theme === "dark") return true;
|
|
291
|
+
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) return true;
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
270
295
|
function updateTheme() {
|
|
271
|
-
|
|
296
|
+
const dark = model.get("dark_mode");
|
|
297
|
+
wrapper.classList.toggle("awg-dark", dark);
|
|
298
|
+
rendererRef?.setSetting("labelColor", { color: dark ? "#e0e0e0" : "#333" });
|
|
299
|
+
rendererRef?.refresh();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Detect if inside a themed host (marimo, etc.)
|
|
303
|
+
const hasHostTheme = !!el.getRootNode()?.host?.tagName?.startsWith("MARIMO-");
|
|
304
|
+
if (hasHostTheme) {
|
|
305
|
+
wrapper.classList.add("awg-auto-theme");
|
|
306
|
+
model.set("dark_mode", detectHostDark());
|
|
307
|
+
model.save_changes();
|
|
308
|
+
updateTheme();
|
|
309
|
+
const themeObserver = new MutationObserver(() => {
|
|
310
|
+
const dark = detectHostDark();
|
|
311
|
+
if (model.get("dark_mode") !== dark) {
|
|
312
|
+
model.set("dark_mode", dark);
|
|
313
|
+
model.save_changes();
|
|
314
|
+
updateTheme();
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "data-theme"] });
|
|
318
|
+
} else {
|
|
319
|
+
updateTheme();
|
|
272
320
|
}
|
|
273
|
-
|
|
321
|
+
|
|
322
|
+
// Manual toggle from settings panel still works
|
|
274
323
|
model.on("change:dark_mode", () => {
|
|
275
324
|
updateTheme();
|
|
276
|
-
rendererRef?.setSetting("labelColor", { color: model.get("dark_mode") ? "#e0e0e0" : "#333" });
|
|
277
|
-
rendererRef?.refresh();
|
|
278
325
|
});
|
|
279
326
|
|
|
280
327
|
// Create query executor callback
|
|
@@ -308,7 +355,13 @@ function render({ model, el }) {
|
|
|
308
355
|
}
|
|
309
356
|
|
|
310
357
|
// Create panels first (all collapsed by default)
|
|
311
|
-
|
|
358
|
+
function refreshLayout() {
|
|
359
|
+
applyLayout(model.get("layout") || "spring");
|
|
360
|
+
rendererRef?.refresh();
|
|
361
|
+
rendererRef?.getCamera().animatedReset({ duration: 300 });
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const schema = createSchemaPanel(model, onExecuteQuery, null, onFilterChange, onColorChange, refreshLayout);
|
|
312
365
|
const properties = createPropertiesPanel(model);
|
|
313
366
|
const settings = model.get("show_settings")
|
|
314
367
|
? createSettingsPanel(model)
|
|
@@ -407,7 +460,7 @@ function render({ model, el }) {
|
|
|
407
460
|
|
|
408
461
|
rebuildGraph();
|
|
409
462
|
|
|
410
|
-
// Layout application (preserves pinned
|
|
463
|
+
// Layout application (preserves pinned positions, respects filters)
|
|
411
464
|
function applyLayout(layoutName) {
|
|
412
465
|
if (graph.order === 0) return;
|
|
413
466
|
const pinned = model.get("pinned_nodes") || {};
|
|
@@ -420,6 +473,14 @@ function render({ model, el }) {
|
|
|
420
473
|
}
|
|
421
474
|
});
|
|
422
475
|
|
|
476
|
+
// Build a set of visible nodes (respecting type filters and search)
|
|
477
|
+
const visibleNodes = new Set();
|
|
478
|
+
graph.forEachNode((id, attrs) => {
|
|
479
|
+
if (hiddenNodeTypes.size > 0 && hiddenNodeTypes.has(attrs.nodeType)) return;
|
|
480
|
+
if (searchTerm && !searchVisibleNodes.has(id)) return;
|
|
481
|
+
visibleNodes.add(id);
|
|
482
|
+
});
|
|
483
|
+
|
|
423
484
|
switch (layoutName) {
|
|
424
485
|
case "circular":
|
|
425
486
|
circular.assign(graph);
|
|
@@ -428,10 +489,11 @@ function render({ model, el }) {
|
|
|
428
489
|
random.assign(graph);
|
|
429
490
|
break;
|
|
430
491
|
case "cluster": {
|
|
431
|
-
// Step 1: Group nodes by type
|
|
492
|
+
// Step 1: Group visible nodes by type
|
|
432
493
|
const nodes = model.get("nodes") || [];
|
|
433
494
|
const labelGroups = new Map();
|
|
434
495
|
nodes.forEach((node) => {
|
|
496
|
+
if (!visibleNodes.has(node.id)) return;
|
|
435
497
|
const label = (node.labels && node.labels[0]) || node.label || "__other";
|
|
436
498
|
if (!labelGroups.has(label)) labelGroups.set(label, []);
|
|
437
499
|
labelGroups.get(label).push(node.id);
|
|
@@ -598,22 +660,88 @@ function render({ model, el }) {
|
|
|
598
660
|
break;
|
|
599
661
|
}
|
|
600
662
|
case "force": {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
663
|
+
// Build temp subgraph with visible nodes only
|
|
664
|
+
const tempGraph = new Graph({ multi: true });
|
|
665
|
+
graph.forEachNode((id, attrs) => {
|
|
666
|
+
if (visibleNodes.has(id)) tempGraph.addNode(id, { ...attrs });
|
|
667
|
+
});
|
|
668
|
+
graph.forEachEdge((edgeId, attrs, source, target) => {
|
|
669
|
+
if (visibleNodes.has(source) && visibleNodes.has(target)) {
|
|
670
|
+
tempGraph.addEdge(source, target, { ...attrs });
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
const n = tempGraph.order;
|
|
675
|
+
if (n === 0) break;
|
|
676
|
+
const iterations = Math.min(400, Math.max(100, n * 3));
|
|
677
|
+
const gravity = n < 20 ? 0.3 : n < 100 ? 0.1 : 0.05;
|
|
678
|
+
const scalingRatio = n < 20 ? 20 : n < 100 ? 15 : 10;
|
|
679
|
+
forceAtlas2.assign(tempGraph, {
|
|
607
680
|
iterations,
|
|
608
681
|
settings: {
|
|
609
682
|
gravity,
|
|
610
683
|
scalingRatio,
|
|
611
684
|
barnesHutOptimize: n > 50,
|
|
612
685
|
strongGravityMode: false,
|
|
686
|
+
linLogMode: true,
|
|
613
687
|
adjustSizes: true,
|
|
614
688
|
slowDown: 1,
|
|
615
689
|
},
|
|
616
690
|
});
|
|
691
|
+
|
|
692
|
+
// Copy positions back
|
|
693
|
+
tempGraph.forEachNode((id, attrs) => {
|
|
694
|
+
if (graph.hasNode(id)) {
|
|
695
|
+
graph.setNodeAttribute(id, "x", attrs.x);
|
|
696
|
+
graph.setNodeAttribute(id, "y", attrs.y);
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
case "spring": {
|
|
702
|
+
// d3-force spring layout: produces clean hub-spoke rings for hierarchical data
|
|
703
|
+
const n = visibleNodes.size || graph.order;
|
|
704
|
+
|
|
705
|
+
// Build node and link arrays for d3 (visible nodes only)
|
|
706
|
+
const d3Nodes = [];
|
|
707
|
+
const nodeIndexMap = new Map();
|
|
708
|
+
let idx = 0;
|
|
709
|
+
graph.forEachNode((id, attrs) => {
|
|
710
|
+
if (!visibleNodes.has(id)) return;
|
|
711
|
+
nodeIndexMap.set(id, idx);
|
|
712
|
+
d3Nodes.push({ id, x: attrs.x, y: attrs.y, size: attrs.size || 8 });
|
|
713
|
+
idx++;
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
const d3Links = [];
|
|
717
|
+
graph.forEachEdge((edgeId, attrs, source, target) => {
|
|
718
|
+
if (nodeIndexMap.has(source) && nodeIndexMap.has(target)) {
|
|
719
|
+
d3Links.push({ source: nodeIndexMap.get(source), target: nodeIndexMap.get(target) });
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// Configure forces: generous spacing so hub-spoke rings are clear
|
|
724
|
+
const linkDistance = n < 30 ? 80 : n < 100 ? 60 : 45;
|
|
725
|
+
const chargeStrength = n < 30 ? -400 : n < 100 ? -300 : -200;
|
|
726
|
+
|
|
727
|
+
const simulation = d3Force.forceSimulation(d3Nodes)
|
|
728
|
+
.force("link", d3Force.forceLink(d3Links).distance(linkDistance).strength(0.4))
|
|
729
|
+
.force("charge", d3Force.forceManyBody().strength(chargeStrength))
|
|
730
|
+
.force("center", d3Force.forceCenter(0, 0))
|
|
731
|
+
.force("collide", d3Force.forceCollide().radius((d) => d.size * 2 + 4).strength(0.8))
|
|
732
|
+
.stop();
|
|
733
|
+
|
|
734
|
+
// Run simulation synchronously
|
|
735
|
+
const ticks = Math.min(300, Math.max(100, n * 2));
|
|
736
|
+
for (let i = 0; i < ticks; i++) simulation.tick();
|
|
737
|
+
|
|
738
|
+
// Write positions back to graphology
|
|
739
|
+
d3Nodes.forEach((d) => {
|
|
740
|
+
if (graph.hasNode(d.id)) {
|
|
741
|
+
graph.setNodeAttribute(d.id, "x", d.x);
|
|
742
|
+
graph.setNodeAttribute(d.id, "y", d.y);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
617
745
|
break;
|
|
618
746
|
}
|
|
619
747
|
}
|
|
@@ -628,7 +756,7 @@ function render({ model, el }) {
|
|
|
628
756
|
}
|
|
629
757
|
|
|
630
758
|
// Apply initial layout
|
|
631
|
-
applyLayout(model.get("layout") || "
|
|
759
|
+
applyLayout(model.get("layout") || "spring");
|
|
632
760
|
|
|
633
761
|
// Initialize Sigma renderer with LOD settings
|
|
634
762
|
const nodeCount = graph.order;
|
|
@@ -790,11 +918,11 @@ function render({ model, el }) {
|
|
|
790
918
|
const layoutSelect = document.createElement("select");
|
|
791
919
|
layoutSelect.className = "awg-layout-select";
|
|
792
920
|
layoutSelect.title = "Layout algorithm";
|
|
793
|
-
[["force", "Force"], ["cluster", "Cluster"], ["circular", "Circular"], ["random", "Random"]].forEach(([val, text]) => {
|
|
921
|
+
[["spring", "Default"], ["force", "Force"], ["cluster", "Cluster"], ["circular", "Circular"], ["random", "Random"]].forEach(([val, text]) => {
|
|
794
922
|
const opt = document.createElement("option");
|
|
795
923
|
opt.value = val;
|
|
796
924
|
opt.textContent = text;
|
|
797
|
-
if (val === (model.get("layout") || "
|
|
925
|
+
if (val === (model.get("layout") || "spring")) opt.selected = true;
|
|
798
926
|
layoutSelect.appendChild(opt);
|
|
799
927
|
});
|
|
800
928
|
layoutSelect.addEventListener("change", () => {
|
|
@@ -1073,7 +1201,7 @@ function render({ model, el }) {
|
|
|
1073
1201
|
// Update graph when data or styling changes
|
|
1074
1202
|
function onDataOrStyleChange() {
|
|
1075
1203
|
rebuildGraph();
|
|
1076
|
-
applyLayout(model.get("layout") || "
|
|
1204
|
+
applyLayout(model.get("layout") || "spring");
|
|
1077
1205
|
|
|
1078
1206
|
// Update LOD thresholds based on new graph size
|
|
1079
1207
|
const n = graph.order;
|
|
@@ -1098,11 +1226,7 @@ function render({ model, el }) {
|
|
|
1098
1226
|
model.on("change:edge_size_range", onDataOrStyleChange);
|
|
1099
1227
|
|
|
1100
1228
|
// Layout change handler
|
|
1101
|
-
model.on("change:layout",
|
|
1102
|
-
applyLayout(model.get("layout"));
|
|
1103
|
-
renderer.refresh();
|
|
1104
|
-
renderer.getCamera().animatedReset({ duration: 300 });
|
|
1105
|
-
});
|
|
1229
|
+
model.on("change:layout", refreshLayout);
|
|
1106
1230
|
|
|
1107
1231
|
// === Demo Mode: auto-init WASM and populate data ===
|
|
1108
1232
|
if (model.get("_demo_mode")) {
|
|
@@ -62,7 +62,7 @@ function extractTypesFromData(model) {
|
|
|
62
62
|
* @param {function} onFilterChange - callback(hiddenNodeTypes: Set, hiddenEdgeTypes: Set)
|
|
63
63
|
* @param {function} onColorChange - callback(typeColorMap: Map<string, string>)
|
|
64
64
|
*/
|
|
65
|
-
export function createSchemaPanel(model, onExecuteQuery, callbacks, onFilterChange, onColorChange) {
|
|
65
|
+
export function createSchemaPanel(model, onExecuteQuery, callbacks, onFilterChange, onColorChange, onRefreshLayout) {
|
|
66
66
|
const panel = document.createElement("div");
|
|
67
67
|
panel.className = "awg-schema-panel";
|
|
68
68
|
|
|
@@ -85,8 +85,11 @@ export function createSchemaPanel(model, onExecuteQuery, callbacks, onFilterChan
|
|
|
85
85
|
const refreshBtn = document.createElement("button");
|
|
86
86
|
refreshBtn.className = "awg-btn awg-btn-icon awg-btn-sm";
|
|
87
87
|
refreshBtn.innerHTML = ICONS.refresh;
|
|
88
|
-
refreshBtn.title = "Refresh
|
|
89
|
-
refreshBtn.addEventListener("click", () =>
|
|
88
|
+
refreshBtn.title = "Refresh layout";
|
|
89
|
+
refreshBtn.addEventListener("click", () => {
|
|
90
|
+
refreshSchema(model);
|
|
91
|
+
onRefreshLayout?.();
|
|
92
|
+
});
|
|
90
93
|
headerBtns.appendChild(refreshBtn);
|
|
91
94
|
|
|
92
95
|
const closeBtn = document.createElement("button");
|
|
@@ -58,9 +58,8 @@ export function createSettingsPanel(model, callbacks) {
|
|
|
58
58
|
const form = document.createElement("div");
|
|
59
59
|
form.className = "awg-panel-form";
|
|
60
60
|
|
|
61
|
-
// Dark mode toggle
|
|
62
|
-
|
|
63
|
-
createFormGroup("Theme", () => {
|
|
61
|
+
// Dark mode toggle (hidden when host provides theme)
|
|
62
|
+
const themeGroup = createFormGroup("Theme", () => {
|
|
64
63
|
const toggle = document.createElement("label");
|
|
65
64
|
toggle.className = "awg-toggle";
|
|
66
65
|
const checkbox = document.createElement("input");
|
|
@@ -82,8 +81,9 @@ export function createSettingsPanel(model, callbacks) {
|
|
|
82
81
|
toggle.appendChild(slider);
|
|
83
82
|
toggle.appendChild(label);
|
|
84
83
|
return toggle;
|
|
85
|
-
})
|
|
86
|
-
);
|
|
84
|
+
});
|
|
85
|
+
themeGroup.classList.add("awg-theme-toggle");
|
|
86
|
+
form.appendChild(themeGroup);
|
|
87
87
|
|
|
88
88
|
// Backend selector
|
|
89
89
|
const backendSelect = document.createElement("select");
|
|
@@ -2,35 +2,35 @@
|
|
|
2
2
|
* Styles for anywidget-graph UI components.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
/* === CSS VARIABLES === */
|
|
5
|
+
/* === CSS VARIABLES (matches marimo theme) === */
|
|
6
6
|
.awg-wrapper {
|
|
7
7
|
--awg-bg: #ffffff;
|
|
8
8
|
--awg-bg-secondary: #f8f9fa;
|
|
9
|
-
--awg-bg-tertiary: #
|
|
10
|
-
--awg-border: #
|
|
11
|
-
--awg-text: #
|
|
9
|
+
--awg-bg-tertiary: #f1f5f9;
|
|
10
|
+
--awg-border: #e2e8f0;
|
|
11
|
+
--awg-text: #0f172a;
|
|
12
12
|
--awg-text-secondary: #6b7280;
|
|
13
13
|
--awg-text-muted: #9ca3af;
|
|
14
|
-
--awg-accent: #
|
|
15
|
-
--awg-accent-hover: #
|
|
16
|
-
--awg-graph-bg: #
|
|
14
|
+
--awg-accent: #0880ea;
|
|
15
|
+
--awg-accent-hover: #0670d0;
|
|
16
|
+
--awg-graph-bg: #ffffff;
|
|
17
17
|
--awg-input-bg: #ffffff;
|
|
18
|
-
--awg-input-border: #
|
|
18
|
+
--awg-input-border: #e2e8f0;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
.awg-wrapper.awg-dark {
|
|
22
|
-
--awg-bg: #
|
|
23
|
-
--awg-bg-secondary: #
|
|
24
|
-
--awg-bg-tertiary: #
|
|
25
|
-
--awg-border: #
|
|
26
|
-
--awg-text: #
|
|
22
|
+
--awg-bg: #181c1a;
|
|
23
|
+
--awg-bg-secondary: #252927;
|
|
24
|
+
--awg-bg-tertiary: #2f3533;
|
|
25
|
+
--awg-border: #3b403e;
|
|
26
|
+
--awg-text: #eceeed;
|
|
27
27
|
--awg-text-secondary: #a0aec0;
|
|
28
|
-
--awg-text-muted: #
|
|
29
|
-
--awg-accent: #
|
|
30
|
-
--awg-accent-hover: #
|
|
31
|
-
--awg-graph-bg: #
|
|
32
|
-
--awg-input-bg: #
|
|
33
|
-
--awg-input-border: #
|
|
28
|
+
--awg-text-muted: #9ca3af;
|
|
29
|
+
--awg-accent: #28879f;
|
|
30
|
+
--awg-accent-hover: #1d5b6a;
|
|
31
|
+
--awg-graph-bg: #181c1a;
|
|
32
|
+
--awg-input-bg: #181c1a;
|
|
33
|
+
--awg-input-border: #3b403e;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/* === BASE === */
|
|
@@ -515,6 +515,11 @@
|
|
|
515
515
|
color: var(--awg-text);
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
+
/* Hide dark mode toggle when host provides theme */
|
|
519
|
+
.awg-wrapper.awg-auto-theme .awg-theme-toggle {
|
|
520
|
+
display: none;
|
|
521
|
+
}
|
|
522
|
+
|
|
518
523
|
/* === SCHEMA PANEL (LEFT) === */
|
|
519
524
|
.awg-schema-panel {
|
|
520
525
|
width: 0;
|
|
@@ -76,7 +76,7 @@ class Graph(anywidget.AnyWidget):
|
|
|
76
76
|
edge_size_range = traitlets.List(default_value=[1, 8]).tag(sync=True)
|
|
77
77
|
|
|
78
78
|
# === Layout ===
|
|
79
|
-
layout = traitlets.Unicode(default_value="
|
|
79
|
+
layout = traitlets.Unicode(default_value="spring").tag(sync=True)
|
|
80
80
|
|
|
81
81
|
# === Hover / Tooltip ===
|
|
82
82
|
show_tooltip = traitlets.Bool(default_value=True).tag(sync=True)
|
|
@@ -929,7 +929,7 @@ edges.forEach(edge => {{
|
|
|
929
929
|
}});
|
|
930
930
|
|
|
931
931
|
// --- Apply layout ---
|
|
932
|
-
const layoutName = OPTIONS.layout || "
|
|
932
|
+
const layoutName = OPTIONS.layout || "spring";
|
|
933
933
|
if (graph.order > 0) {{
|
|
934
934
|
switch (layoutName) {{
|
|
935
935
|
case "circular":
|
|
@@ -51,7 +51,7 @@ def test_default_options():
|
|
|
51
51
|
assert graph.show_labels is True
|
|
52
52
|
assert graph.show_edge_labels is False
|
|
53
53
|
assert graph.dark_mode is True
|
|
54
|
-
assert graph.layout == "
|
|
54
|
+
assert graph.layout == "spring"
|
|
55
55
|
assert graph.selection_mode == "click"
|
|
56
56
|
assert graph.database_backend == "grafeo"
|
|
57
57
|
assert graph.max_nodes == 300
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|