anywidget-graph 0.2.9__tar.gz → 0.2.10__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.2.10}/CHANGELOG.md +15 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/PKG-INFO +1 -1
- anywidget_graph-0.2.10/examples/test_hierarchy.py +99 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/pyproject.toml +1 -1
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/ui/__init__.py +1 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/ui/index.js +118 -24
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/ui/schema.js +6 -3
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/widget.py +2 -2
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/tests/test_graph.py +1 -1
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/.github/workflows/ci.yml +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/.github/workflows/pypi.yml +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/.gitignore +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/.python-version +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/LICENSE +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/README.md +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/examples/demo.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/examples/test_large_graph.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/__init__.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/backends/__init__.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/backends/arango.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/backends/cosmosdb.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/backends/grafeo.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/backends/ladybug.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/backends/neo4j.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/converters/__init__.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/converters/base.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/converters/common.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/converters/cypher.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/converters/gql.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/converters/graphql.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/converters/gremlin.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/converters/sparql.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/demo.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/py.typed +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/ui/grafeo-embed.js +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/ui/grafeo.js +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/ui/icons.js +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/ui/neo4j.js +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/ui/properties.js +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/ui/results.js +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/ui/settings.js +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/ui/styles.css +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/src/anywidget_graph/ui/toolbar.js +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/tests/__init__.py +0 -0
- {anywidget_graph-0.2.9 → anywidget_graph-0.2.10}/uv.lock +0 -0
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.10
|
|
4
|
+
|
|
5
|
+
### New Features
|
|
6
|
+
|
|
7
|
+
- **Spring layout (d3-force)**: New default layout using d3-force with charge, link-distance and collision forces; produces clean hub-spoke rings for hierarchical data like Neo4j browser
|
|
8
|
+
- **Degree-based node sizing**: When no explicit `size_field` is set, nodes are sized by their connection count; hub nodes appear larger and push neighbors into natural rings
|
|
9
|
+
- **Filter-aware layouts**: Switching layout or clicking refresh now re-runs the layout using only visible nodes, respecting type filters and search state
|
|
10
|
+
|
|
11
|
+
### Improvements
|
|
12
|
+
|
|
13
|
+
- Spring layout is the new default (renamed to "Default" in the dropdown)
|
|
14
|
+
- Force layout uses `linLogMode` for better community separation
|
|
15
|
+
- Cluster layout filters out hidden node types before packing
|
|
16
|
+
- Filter panel refresh button triggers a layout refresh
|
|
17
|
+
|
|
3
18
|
## 0.2.9
|
|
4
19
|
|
|
5
20
|
### New Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anywidget-graph
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.10
|
|
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
|
|
@@ -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
|
}
|
|
@@ -308,7 +325,13 @@ function render({ model, el }) {
|
|
|
308
325
|
}
|
|
309
326
|
|
|
310
327
|
// Create panels first (all collapsed by default)
|
|
311
|
-
|
|
328
|
+
function refreshLayout() {
|
|
329
|
+
applyLayout(model.get("layout") || "spring");
|
|
330
|
+
rendererRef?.refresh();
|
|
331
|
+
rendererRef?.getCamera().animatedReset({ duration: 300 });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const schema = createSchemaPanel(model, onExecuteQuery, null, onFilterChange, onColorChange, refreshLayout);
|
|
312
335
|
const properties = createPropertiesPanel(model);
|
|
313
336
|
const settings = model.get("show_settings")
|
|
314
337
|
? createSettingsPanel(model)
|
|
@@ -407,7 +430,7 @@ function render({ model, el }) {
|
|
|
407
430
|
|
|
408
431
|
rebuildGraph();
|
|
409
432
|
|
|
410
|
-
// Layout application (preserves pinned
|
|
433
|
+
// Layout application (preserves pinned positions, respects filters)
|
|
411
434
|
function applyLayout(layoutName) {
|
|
412
435
|
if (graph.order === 0) return;
|
|
413
436
|
const pinned = model.get("pinned_nodes") || {};
|
|
@@ -420,6 +443,14 @@ function render({ model, el }) {
|
|
|
420
443
|
}
|
|
421
444
|
});
|
|
422
445
|
|
|
446
|
+
// Build a set of visible nodes (respecting type filters and search)
|
|
447
|
+
const visibleNodes = new Set();
|
|
448
|
+
graph.forEachNode((id, attrs) => {
|
|
449
|
+
if (hiddenNodeTypes.size > 0 && hiddenNodeTypes.has(attrs.nodeType)) return;
|
|
450
|
+
if (searchTerm && !searchVisibleNodes.has(id)) return;
|
|
451
|
+
visibleNodes.add(id);
|
|
452
|
+
});
|
|
453
|
+
|
|
423
454
|
switch (layoutName) {
|
|
424
455
|
case "circular":
|
|
425
456
|
circular.assign(graph);
|
|
@@ -428,10 +459,11 @@ function render({ model, el }) {
|
|
|
428
459
|
random.assign(graph);
|
|
429
460
|
break;
|
|
430
461
|
case "cluster": {
|
|
431
|
-
// Step 1: Group nodes by type
|
|
462
|
+
// Step 1: Group visible nodes by type
|
|
432
463
|
const nodes = model.get("nodes") || [];
|
|
433
464
|
const labelGroups = new Map();
|
|
434
465
|
nodes.forEach((node) => {
|
|
466
|
+
if (!visibleNodes.has(node.id)) return;
|
|
435
467
|
const label = (node.labels && node.labels[0]) || node.label || "__other";
|
|
436
468
|
if (!labelGroups.has(label)) labelGroups.set(label, []);
|
|
437
469
|
labelGroups.get(label).push(node.id);
|
|
@@ -598,22 +630,88 @@ function render({ model, el }) {
|
|
|
598
630
|
break;
|
|
599
631
|
}
|
|
600
632
|
case "force": {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
633
|
+
// Build temp subgraph with visible nodes only
|
|
634
|
+
const tempGraph = new Graph({ multi: true });
|
|
635
|
+
graph.forEachNode((id, attrs) => {
|
|
636
|
+
if (visibleNodes.has(id)) tempGraph.addNode(id, { ...attrs });
|
|
637
|
+
});
|
|
638
|
+
graph.forEachEdge((edgeId, attrs, source, target) => {
|
|
639
|
+
if (visibleNodes.has(source) && visibleNodes.has(target)) {
|
|
640
|
+
tempGraph.addEdge(source, target, { ...attrs });
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const n = tempGraph.order;
|
|
645
|
+
if (n === 0) break;
|
|
646
|
+
const iterations = Math.min(400, Math.max(100, n * 3));
|
|
647
|
+
const gravity = n < 20 ? 0.3 : n < 100 ? 0.1 : 0.05;
|
|
648
|
+
const scalingRatio = n < 20 ? 20 : n < 100 ? 15 : 10;
|
|
649
|
+
forceAtlas2.assign(tempGraph, {
|
|
607
650
|
iterations,
|
|
608
651
|
settings: {
|
|
609
652
|
gravity,
|
|
610
653
|
scalingRatio,
|
|
611
654
|
barnesHutOptimize: n > 50,
|
|
612
655
|
strongGravityMode: false,
|
|
656
|
+
linLogMode: true,
|
|
613
657
|
adjustSizes: true,
|
|
614
658
|
slowDown: 1,
|
|
615
659
|
},
|
|
616
660
|
});
|
|
661
|
+
|
|
662
|
+
// Copy positions back
|
|
663
|
+
tempGraph.forEachNode((id, attrs) => {
|
|
664
|
+
if (graph.hasNode(id)) {
|
|
665
|
+
graph.setNodeAttribute(id, "x", attrs.x);
|
|
666
|
+
graph.setNodeAttribute(id, "y", attrs.y);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
case "spring": {
|
|
672
|
+
// d3-force spring layout: produces clean hub-spoke rings for hierarchical data
|
|
673
|
+
const n = visibleNodes.size || graph.order;
|
|
674
|
+
|
|
675
|
+
// Build node and link arrays for d3 (visible nodes only)
|
|
676
|
+
const d3Nodes = [];
|
|
677
|
+
const nodeIndexMap = new Map();
|
|
678
|
+
let idx = 0;
|
|
679
|
+
graph.forEachNode((id, attrs) => {
|
|
680
|
+
if (!visibleNodes.has(id)) return;
|
|
681
|
+
nodeIndexMap.set(id, idx);
|
|
682
|
+
d3Nodes.push({ id, x: attrs.x, y: attrs.y, size: attrs.size || 8 });
|
|
683
|
+
idx++;
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
const d3Links = [];
|
|
687
|
+
graph.forEachEdge((edgeId, attrs, source, target) => {
|
|
688
|
+
if (nodeIndexMap.has(source) && nodeIndexMap.has(target)) {
|
|
689
|
+
d3Links.push({ source: nodeIndexMap.get(source), target: nodeIndexMap.get(target) });
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Configure forces
|
|
694
|
+
const linkDistance = n < 30 ? 50 : n < 100 ? 40 : 30;
|
|
695
|
+
const chargeStrength = n < 30 ? -200 : n < 100 ? -150 : -80;
|
|
696
|
+
|
|
697
|
+
const simulation = d3Force.forceSimulation(d3Nodes)
|
|
698
|
+
.force("link", d3Force.forceLink(d3Links).distance(linkDistance).strength(0.5))
|
|
699
|
+
.force("charge", d3Force.forceManyBody().strength(chargeStrength))
|
|
700
|
+
.force("center", d3Force.forceCenter(0, 0))
|
|
701
|
+
.force("collide", d3Force.forceCollide().radius((d) => d.size * 1.5 + 2).strength(0.7))
|
|
702
|
+
.stop();
|
|
703
|
+
|
|
704
|
+
// Run simulation synchronously
|
|
705
|
+
const ticks = Math.min(300, Math.max(100, n * 2));
|
|
706
|
+
for (let i = 0; i < ticks; i++) simulation.tick();
|
|
707
|
+
|
|
708
|
+
// Write positions back to graphology
|
|
709
|
+
d3Nodes.forEach((d) => {
|
|
710
|
+
if (graph.hasNode(d.id)) {
|
|
711
|
+
graph.setNodeAttribute(d.id, "x", d.x);
|
|
712
|
+
graph.setNodeAttribute(d.id, "y", d.y);
|
|
713
|
+
}
|
|
714
|
+
});
|
|
617
715
|
break;
|
|
618
716
|
}
|
|
619
717
|
}
|
|
@@ -628,7 +726,7 @@ function render({ model, el }) {
|
|
|
628
726
|
}
|
|
629
727
|
|
|
630
728
|
// Apply initial layout
|
|
631
|
-
applyLayout(model.get("layout") || "
|
|
729
|
+
applyLayout(model.get("layout") || "spring");
|
|
632
730
|
|
|
633
731
|
// Initialize Sigma renderer with LOD settings
|
|
634
732
|
const nodeCount = graph.order;
|
|
@@ -790,11 +888,11 @@ function render({ model, el }) {
|
|
|
790
888
|
const layoutSelect = document.createElement("select");
|
|
791
889
|
layoutSelect.className = "awg-layout-select";
|
|
792
890
|
layoutSelect.title = "Layout algorithm";
|
|
793
|
-
[["force", "Force"], ["cluster", "Cluster"], ["circular", "Circular"], ["random", "Random"]].forEach(([val, text]) => {
|
|
891
|
+
[["spring", "Default"], ["force", "Force"], ["cluster", "Cluster"], ["circular", "Circular"], ["random", "Random"]].forEach(([val, text]) => {
|
|
794
892
|
const opt = document.createElement("option");
|
|
795
893
|
opt.value = val;
|
|
796
894
|
opt.textContent = text;
|
|
797
|
-
if (val === (model.get("layout") || "
|
|
895
|
+
if (val === (model.get("layout") || "spring")) opt.selected = true;
|
|
798
896
|
layoutSelect.appendChild(opt);
|
|
799
897
|
});
|
|
800
898
|
layoutSelect.addEventListener("change", () => {
|
|
@@ -1073,7 +1171,7 @@ function render({ model, el }) {
|
|
|
1073
1171
|
// Update graph when data or styling changes
|
|
1074
1172
|
function onDataOrStyleChange() {
|
|
1075
1173
|
rebuildGraph();
|
|
1076
|
-
applyLayout(model.get("layout") || "
|
|
1174
|
+
applyLayout(model.get("layout") || "spring");
|
|
1077
1175
|
|
|
1078
1176
|
// Update LOD thresholds based on new graph size
|
|
1079
1177
|
const n = graph.order;
|
|
@@ -1098,11 +1196,7 @@ function render({ model, el }) {
|
|
|
1098
1196
|
model.on("change:edge_size_range", onDataOrStyleChange);
|
|
1099
1197
|
|
|
1100
1198
|
// Layout change handler
|
|
1101
|
-
model.on("change:layout",
|
|
1102
|
-
applyLayout(model.get("layout"));
|
|
1103
|
-
renderer.refresh();
|
|
1104
|
-
renderer.getCamera().animatedReset({ duration: 300 });
|
|
1105
|
-
});
|
|
1199
|
+
model.on("change:layout", refreshLayout);
|
|
1106
1200
|
|
|
1107
1201
|
// === Demo Mode: auto-init WASM and populate data ===
|
|
1108
1202
|
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");
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|