pycode-kg 0.21.4__tar.gz → 0.22.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.
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/PKG-INFO +6 -6
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/README.md +4 -4
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/pyproject.toml +28 -3
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/__init__.py +1 -1
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/analysis/bridge.py +1 -1
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/analysis/centrality.py +1 -1
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/analysis/framework_detector.py +1 -1
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_hooks.py +1 -1
- pycode_kg-0.22.0/src/pycode_kg/layout3d.py +84 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/pycodekg.py +1 -1
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/snapshots.py +1 -1
- pycode_kg-0.21.4/src/pycode_kg/layout3d.py +0 -476
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/LICENSE +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/.DS_Store +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/__main__.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/analysis/__init__.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/analysis/hybrid_rank.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/app.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/architecture.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/build_pycodekg_sqlite.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/cli/__init__.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_analyze.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_architecture.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_bridges.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_build.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_build_full.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_centrality.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_explain.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_framework_nodes.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_init.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_mcp.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_model.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_query.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_snapshot.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_viz.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/cli/main.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/cli/options.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/config.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/explain.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/graph.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/graph_html.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/index.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/kg.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/mcp_server.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/module/__init__.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/module/base.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/module/extractor.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/module/types.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/pycodekg_query.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/pycodekg_snippet_packer.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/pycodekg_thorough_analysis.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/pycodekg_viz.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/pycodekg_viz3d.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/ranking/__init__.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/ranking/cli_rank.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/ranking/coderank.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/sql/004_add_centrality_table.sql +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/store.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/theme.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/utils.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/visitor.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/viz3d.py +0 -0
- {pycode_kg-0.21.4 → pycode_kg-0.22.0}/src/pycode_kg/viz3d_timeline.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pycode-kg
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.22.0
|
|
4
4
|
Summary: A tool to build a searchable knowledge graph from Python repositories
|
|
5
5
|
License-Expression: Elastic-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -24,7 +24,7 @@ Requires-Dist: PyQt5 (>=5.15.0) ; extra == "viz3d"
|
|
|
24
24
|
Requires-Dist: click (>=8.1.0,<9)
|
|
25
25
|
Requires-Dist: detect-secrets (>=1.5.0) ; extra == "all"
|
|
26
26
|
Requires-Dist: detect-secrets (>=1.5.0) ; extra == "dev"
|
|
27
|
-
Requires-Dist: kgmodule-utils[semantic] (>=0.
|
|
27
|
+
Requires-Dist: kgmodule-utils[semantic,viz3d] (>=0.11.0)
|
|
28
28
|
Requires-Dist: markdown (>=3.6) ; extra == "all"
|
|
29
29
|
Requires-Dist: markdown (>=3.6) ; extra == "viz3d"
|
|
30
30
|
Requires-Dist: mcp (>=1.0.0,<2)
|
|
@@ -72,7 +72,7 @@ Description-Content-Type: text/markdown
|
|
|
72
72
|
|
|
73
73
|
[](https://www.python.org/)
|
|
74
74
|
[](https://www.elastic.co/licensing/elastic-license)
|
|
75
|
-
[](https://github.com/Flux-Frontiers/pycode_kg/releases)
|
|
76
76
|
[](https://github.com/Flux-Frontiers/pycode_kg/actions/workflows/ci.yml)
|
|
77
77
|
[](https://python-poetry.org/)
|
|
78
78
|
[](https://zenodo.org/badge/latestdoi/1202379010)
|
|
@@ -243,7 +243,7 @@ src/pycode_kg/
|
|
|
243
243
|
└── viz3d_timeline.py # Metric history timeline
|
|
244
244
|
```
|
|
245
245
|
|
|
246
|
-
The MCP server, the CLI, and the Streamlit app are thin wrappers over the same store + index + ranking core — there is exactly one code path for each capability. The latest architectural deep-dive is in [docs/analysis_v0.
|
|
246
|
+
The MCP server, the CLI, and the Streamlit app are thin wrappers over the same store + index + ranking core — there is exactly one code path for each capability. The latest architectural deep-dive is in [docs/analysis_v0.22.0.md](docs/analysis_v0.22.0.md), produced (of course) by `pycodekg analyze` against this very repo.
|
|
247
247
|
|
|
248
248
|
---
|
|
249
249
|
|
|
@@ -269,13 +269,13 @@ If you use PyCodeKG in your research or project, please cite it:
|
|
|
269
269
|
|
|
270
270
|
[](https://zenodo.org/badge/latestdoi/1202379010)
|
|
271
271
|
|
|
272
|
-
> Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.
|
|
272
|
+
> Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.22.0) [Software]. Flux-Frontiers. https://doi.org/10.5281/zenodo.19737993
|
|
273
273
|
|
|
274
274
|
```bibtex
|
|
275
275
|
@software{suchanek_pycode_kg,
|
|
276
276
|
author = {Suchanek, Eric G.},
|
|
277
277
|
title = {{PyCodeKG}: A Knowledge Graph for Python Codebases},
|
|
278
|
-
version = {0.
|
|
278
|
+
version = {0.22.0},
|
|
279
279
|
year = {2026},
|
|
280
280
|
publisher = {Flux-Frontiers},
|
|
281
281
|
url = {https://github.com/Flux-Frontiers/pycode_kg},
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
[](https://www.python.org/)
|
|
7
7
|
[](https://www.elastic.co/licensing/elastic-license)
|
|
8
|
-
[](https://github.com/Flux-Frontiers/pycode_kg/releases)
|
|
9
9
|
[](https://github.com/Flux-Frontiers/pycode_kg/actions/workflows/ci.yml)
|
|
10
10
|
[](https://python-poetry.org/)
|
|
11
11
|
[](https://zenodo.org/badge/latestdoi/1202379010)
|
|
@@ -176,7 +176,7 @@ src/pycode_kg/
|
|
|
176
176
|
└── viz3d_timeline.py # Metric history timeline
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
-
The MCP server, the CLI, and the Streamlit app are thin wrappers over the same store + index + ranking core — there is exactly one code path for each capability. The latest architectural deep-dive is in [docs/analysis_v0.
|
|
179
|
+
The MCP server, the CLI, and the Streamlit app are thin wrappers over the same store + index + ranking core — there is exactly one code path for each capability. The latest architectural deep-dive is in [docs/analysis_v0.22.0.md](docs/analysis_v0.22.0.md), produced (of course) by `pycodekg analyze` against this very repo.
|
|
180
180
|
|
|
181
181
|
---
|
|
182
182
|
|
|
@@ -202,13 +202,13 @@ If you use PyCodeKG in your research or project, please cite it:
|
|
|
202
202
|
|
|
203
203
|
[](https://zenodo.org/badge/latestdoi/1202379010)
|
|
204
204
|
|
|
205
|
-
> Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.
|
|
205
|
+
> Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.22.0) [Software]. Flux-Frontiers. https://doi.org/10.5281/zenodo.19737993
|
|
206
206
|
|
|
207
207
|
```bibtex
|
|
208
208
|
@software{suchanek_pycode_kg,
|
|
209
209
|
author = {Suchanek, Eric G.},
|
|
210
210
|
title = {{PyCodeKG}: A Knowledge Graph for Python Codebases},
|
|
211
|
-
version = {0.
|
|
211
|
+
version = {0.22.0},
|
|
212
212
|
year = {2026},
|
|
213
213
|
publisher = {Flux-Frontiers},
|
|
214
214
|
url = {https://github.com/Flux-Frontiers/pycode_kg},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pyproject.toml — PyCodeKG package configuration (PEP 621)
|
|
2
2
|
#
|
|
3
3
|
# Author: Eric G. Suchanek, PhD
|
|
4
|
-
# Last Revision: 2026-08-
|
|
4
|
+
# Last Revision: 2026-08-11
|
|
5
5
|
#
|
|
6
6
|
# Build system : Poetry 2.x with PEP 621 [project] table
|
|
7
7
|
#
|
|
@@ -70,7 +70,7 @@ torch = [
|
|
|
70
70
|
# ---------------------------------------------------------------------------
|
|
71
71
|
[project]
|
|
72
72
|
name = "pycode-kg"
|
|
73
|
-
version = "0.
|
|
73
|
+
version = "0.22.0"
|
|
74
74
|
description = "A tool to build a searchable knowledge graph from Python repositories"
|
|
75
75
|
readme = "README.md"
|
|
76
76
|
license = "Elastic-2.0"
|
|
@@ -122,7 +122,11 @@ dependencies = [
|
|
|
122
122
|
# `[viz]` stays off: kg_utils.viz imports pyvis lazily, so it would force
|
|
123
123
|
# pyvis into every bare install when it belongs in this project's own `viz`
|
|
124
124
|
# extra.
|
|
125
|
-
|
|
125
|
+
# `[viz3d]` is the shared 3-D layout engine behind `pycode_kg.layout3d`.
|
|
126
|
+
# Its only dependency is numpy, which `[semantic]` already pulls, so it adds
|
|
127
|
+
# nothing to the install — it just declares what layout3d actually imports.
|
|
128
|
+
# Unlike `[viz]`, there is no pyvis to leak into a bare install.
|
|
129
|
+
"kgmodule-utils[semantic,viz3d]>=0.11.0",
|
|
126
130
|
]
|
|
127
131
|
|
|
128
132
|
[project.optional-dependencies]
|
|
@@ -175,6 +179,27 @@ all = [
|
|
|
175
179
|
"ty>=0.0.41",
|
|
176
180
|
]
|
|
177
181
|
|
|
182
|
+
# Maintainer tooling, not a dependency of this package. `.mcp.json` serves a
|
|
183
|
+
# `dockg` MCP server from `.venv/bin/dockg`, so the CLI has to be in this repo's
|
|
184
|
+
# environment — but nothing under src/ imports doc_kg, and declaring it as a real
|
|
185
|
+
# dependency would put it in the published wheel for every consumer.
|
|
186
|
+
#
|
|
187
|
+
# A Poetry group is the right shape: it is locked and installable but never
|
|
188
|
+
# written into wheel metadata, so `pip install pycode-kg` is unaffected.
|
|
189
|
+
#
|
|
190
|
+
# poetry install --with kg # gets the dockg CLI into .venv/bin
|
|
191
|
+
# poetry install # default — the group is optional, skipped
|
|
192
|
+
#
|
|
193
|
+
# `kg` is the fleet-wide name for this group so there is one command to remember;
|
|
194
|
+
# contents are per-repo and minimal, covering only what that repo actually
|
|
195
|
+
# invokes. Safe to lock because doc-kg keeps its own pycode-kg in a group too, so
|
|
196
|
+
# the published metadata on either side has no cycle for the resolver to chase.
|
|
197
|
+
[tool.poetry.group.kg]
|
|
198
|
+
optional = true
|
|
199
|
+
|
|
200
|
+
[tool.poetry.group.kg.dependencies]
|
|
201
|
+
doc-kg = ">=0.21.1"
|
|
202
|
+
|
|
178
203
|
[project.urls]
|
|
179
204
|
Homepage = "https://github.com/Flux-Frontiers/pycode_kg"
|
|
180
205
|
Repository = "https://github.com/Flux-Frontiers/pycode_kg"
|
|
@@ -32,7 +32,7 @@ KGModule SDK (build new domain KGs)::
|
|
|
32
32
|
from pycode_kg import KGModule, KGExtractor, PyCodeKGExtractor, NodeSpec, EdgeSpec
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
__version__ = "0.
|
|
35
|
+
__version__ = "0.22.0"
|
|
36
36
|
__author__ = "Eric G. Suchanek, PhD"
|
|
37
37
|
|
|
38
38
|
# Low-level primitives (locked v0 contract)
|
|
@@ -4,7 +4,7 @@ Measures module interaction complexity: how many unique modules each module call
|
|
|
4
4
|
For well-modularized codebases, identifies orchestrator and hub modules.
|
|
5
5
|
|
|
6
6
|
Author: Eric G. Suchanek, PhD
|
|
7
|
-
Last Revision: 2026-08-
|
|
7
|
+
Last Revision: 2026-08-03 11:37:06
|
|
8
8
|
License: Elastic 2.0
|
|
9
9
|
"""
|
|
10
10
|
|
|
@@ -6,7 +6,7 @@ CLI command for installing PyCodeKG git hooks:
|
|
|
6
6
|
install-hooks — install the pre-commit snapshot hook into .git/hooks/
|
|
7
7
|
|
|
8
8
|
Author: Eric G. Suchanek, PhD
|
|
9
|
-
Last Revision: 2026-08-
|
|
9
|
+
Last Revision: 2026-08-03 11:37:06
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PyCodeKG's 3-D layout bindings over the shared layout engine.
|
|
4
|
+
|
|
5
|
+
The engine itself lives in :mod:`kg_utils.viz3d`, shared with every other KG
|
|
6
|
+
module — GutenbergKG was already importing it from here, paying a full
|
|
7
|
+
``pycode-kg`` dependency for five symbols that have nothing to do with parsing
|
|
8
|
+
Python. What stays here is the part that is genuinely specific to a *code*
|
|
9
|
+
graph: which node kinds occupy which Z level, which comes from
|
|
10
|
+
:mod:`pycode_kg.theme`.
|
|
11
|
+
|
|
12
|
+
Everything is re-exported so existing callers do not need to know where the
|
|
13
|
+
implementation moved.
|
|
14
|
+
|
|
15
|
+
The Fibonacci utilities (``fibonacci_sphere``, ``fibonacci_annulus``) are
|
|
16
|
+
adapted from *repo_vis* ``pkg_visualizer/utility.py``
|
|
17
|
+
(Eric G. Suchanek, PhD — https://github.com/Suchanek/repo_vis).
|
|
18
|
+
|
|
19
|
+
Author: Eric G. Suchanek, PhD
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from kg_utils.viz3d import (
|
|
25
|
+
AlliumLayout,
|
|
26
|
+
Layout3D,
|
|
27
|
+
LayoutEdge,
|
|
28
|
+
LayoutNode,
|
|
29
|
+
fibonacci_annulus,
|
|
30
|
+
fibonacci_sphere,
|
|
31
|
+
golden_spiral_2d,
|
|
32
|
+
)
|
|
33
|
+
from kg_utils.viz3d import FunnelLayout as _SharedFunnelLayout
|
|
34
|
+
|
|
35
|
+
from pycode_kg import theme
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"AlliumLayout",
|
|
39
|
+
"FunnelLayout",
|
|
40
|
+
"Layout3D",
|
|
41
|
+
"LayoutEdge",
|
|
42
|
+
"LayoutNode",
|
|
43
|
+
"fibonacci_annulus",
|
|
44
|
+
"fibonacci_sphere",
|
|
45
|
+
"golden_spiral_2d",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class FunnelLayout(_SharedFunnelLayout):
|
|
50
|
+
"""Funnel layout stratified by *code* node kind.
|
|
51
|
+
|
|
52
|
+
The shared engine takes the kind→Z-level mapping as an argument, because
|
|
53
|
+
only the domain knows its own hierarchy — modules at the bottom, then
|
|
54
|
+
classes, then functions and methods, with symbol stubs on top is a fact
|
|
55
|
+
about Python code, not about knowledge graphs. This subclass supplies
|
|
56
|
+
that mapping from :mod:`pycode_kg.theme`, so the layout engine, the 3-D
|
|
57
|
+
viewer and the 2-D explorer keep sharing one definition.
|
|
58
|
+
|
|
59
|
+
Unknown kinds land on the symbol level, matching
|
|
60
|
+
:func:`pycode_kg.theme.resolve_kind`, which collapses anything the palette
|
|
61
|
+
does not recognise to ``symbol``.
|
|
62
|
+
|
|
63
|
+
:param layer_gap: Vertical distance between adjacent layers.
|
|
64
|
+
:param node_spacing: Spacing multiplier — larger spreads layers out more.
|
|
65
|
+
:param kwargs: Forwarded to :class:`kg_utils.viz3d.FunnelLayout`; the
|
|
66
|
+
theme-derived defaults are only applied to keys left unset.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
layer_gap: float = 12.0,
|
|
72
|
+
node_spacing: float = 2.0,
|
|
73
|
+
**kwargs,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Initialise layout parameters, defaulting the maps from the theme.
|
|
76
|
+
|
|
77
|
+
:param layer_gap: Vertical separation between layers.
|
|
78
|
+
:param node_spacing: Controls minimum gap between node surfaces.
|
|
79
|
+
:param kwargs: Overrides forwarded to the shared layout.
|
|
80
|
+
"""
|
|
81
|
+
kwargs.setdefault("zlevels", theme.KIND_ZLEVEL)
|
|
82
|
+
kwargs.setdefault("level_sizes", theme.LEVEL_NODE_SIZE)
|
|
83
|
+
kwargs.setdefault("default_level", theme.KIND_ZLEVEL[theme.UNKNOWN_KIND])
|
|
84
|
+
super().__init__(layer_gap=layer_gap, node_spacing=node_spacing, **kwargs)
|
|
@@ -23,7 +23,7 @@ No persistence, no embeddings, no LLMs—just pure AST extraction. Integration w
|
|
|
23
23
|
vector databases and semantic search happens downstream.
|
|
24
24
|
|
|
25
25
|
Author: Eric G. Suchanek, PhD
|
|
26
|
-
Last Revision: 2026-08-
|
|
26
|
+
Last Revision: 2026-08-03 11:37:06
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
29
|
from __future__ import annotations
|
|
@@ -1,476 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
layout3d.py — Pluggable 3-D layout engine for the PyCodeKG knowledge graph.
|
|
4
|
-
|
|
5
|
-
Provides an abstract :class:`Layout3D` base class and two concrete
|
|
6
|
-
implementations:
|
|
7
|
-
|
|
8
|
-
- :class:`AlliumLayout`: Each module is rendered as a Giant Allium plant
|
|
9
|
-
(a vertical stem with a Fibonacci-sphere "head" of classes and functions).
|
|
10
|
-
Modules are arranged in a Fibonacci annulus in the XY plane.
|
|
11
|
-
|
|
12
|
-
- :class:`FunnelLayout`: Node kind determines the Z level (modules at
|
|
13
|
-
the bottom, classes above, functions/methods at the top). XY positions
|
|
14
|
-
are spread via a golden-angle spiral within each layer.
|
|
15
|
-
|
|
16
|
-
The Fibonacci utilities (``fibonacci_sphere``, ``fibonacci_annulus``) are
|
|
17
|
-
adapted from *repo_vis* ``pkg_visualizer/utility.py``
|
|
18
|
-
(Eric G. Suchanek, PhD — https://github.com/Suchanek/repo_vis).
|
|
19
|
-
|
|
20
|
-
Author: Eric G. Suchanek, PhD
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
from __future__ import annotations
|
|
24
|
-
|
|
25
|
-
from abc import ABC, abstractmethod
|
|
26
|
-
from dataclasses import dataclass
|
|
27
|
-
|
|
28
|
-
import numpy as np
|
|
29
|
-
|
|
30
|
-
from pycode_kg import theme
|
|
31
|
-
|
|
32
|
-
# ---------------------------------------------------------------------------
|
|
33
|
-
# Fibonacci spatial utilities (adapted from repo_vis/pkg_visualizer/utility.py)
|
|
34
|
-
# ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def fibonacci_sphere(
|
|
38
|
-
samples: int,
|
|
39
|
-
radius: float = 1.0,
|
|
40
|
-
center: np.ndarray | None = None,
|
|
41
|
-
) -> list[np.ndarray]:
|
|
42
|
-
"""
|
|
43
|
-
Distribute *samples* points uniformly on a sphere using the Fibonacci spiral.
|
|
44
|
-
|
|
45
|
-
Adapted from ``utility.fibonacci_sphere`` in *repo_vis*.
|
|
46
|
-
|
|
47
|
-
:param samples: Number of points to generate.
|
|
48
|
-
:param radius: Sphere radius.
|
|
49
|
-
:param center: Centre of the sphere (default: origin).
|
|
50
|
-
:return: List of 3-D coordinate arrays.
|
|
51
|
-
"""
|
|
52
|
-
if center is None:
|
|
53
|
-
center = np.zeros(3)
|
|
54
|
-
if samples <= 0:
|
|
55
|
-
return []
|
|
56
|
-
if samples == 1:
|
|
57
|
-
return [center + radius * np.array([0.0, 0.0, 1.0])]
|
|
58
|
-
|
|
59
|
-
phi = np.pi * (3.0 - np.sqrt(5.0)) # golden angle in radians
|
|
60
|
-
points: list[np.ndarray] = []
|
|
61
|
-
for i in range(samples):
|
|
62
|
-
y = 1.0 - (i / float(samples - 1)) * 2.0
|
|
63
|
-
r_at_y = np.sqrt(max(0.0, 1.0 - y * y))
|
|
64
|
-
theta = phi * i
|
|
65
|
-
x = np.cos(theta) * r_at_y
|
|
66
|
-
z = np.sin(theta) * r_at_y
|
|
67
|
-
points.append(center + radius * np.array([x, y, z]))
|
|
68
|
-
return points
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def fibonacci_annulus(
|
|
72
|
-
samples: int,
|
|
73
|
-
inner_radius: float = 1.0,
|
|
74
|
-
outer_radius: float = 2.0,
|
|
75
|
-
center: np.ndarray | None = None,
|
|
76
|
-
z_thickness: float = 0.2,
|
|
77
|
-
) -> list[np.ndarray]:
|
|
78
|
-
"""
|
|
79
|
-
Distribute *samples* points in a flat annular ring in the XY plane.
|
|
80
|
-
|
|
81
|
-
A small Z jitter (``z_thickness``) adds visual depth when non-zero.
|
|
82
|
-
Adapted from ``utility.fibonacci_annulus`` in *repo_vis*.
|
|
83
|
-
|
|
84
|
-
:param samples: Number of points to generate.
|
|
85
|
-
:param inner_radius: Inner radius of the annulus.
|
|
86
|
-
:param outer_radius: Outer radius of the annulus.
|
|
87
|
-
:param center: Centre of the annulus (default: origin).
|
|
88
|
-
:param z_thickness: Half-range of Z jitter applied to each point.
|
|
89
|
-
:return: List of 3-D coordinate arrays.
|
|
90
|
-
"""
|
|
91
|
-
if center is None:
|
|
92
|
-
center = np.zeros(3)
|
|
93
|
-
if samples <= 0:
|
|
94
|
-
return []
|
|
95
|
-
if samples == 1:
|
|
96
|
-
mid = (inner_radius + outer_radius) / 2.0
|
|
97
|
-
return [center + np.array([mid, 0.0, 0.0])]
|
|
98
|
-
|
|
99
|
-
phi = np.pi * (3.0 - np.sqrt(5.0))
|
|
100
|
-
r_range = outer_radius - inner_radius
|
|
101
|
-
r_step = r_range / max(samples - 1, 1)
|
|
102
|
-
rng = np.random.default_rng(42) # deterministic jitter seed
|
|
103
|
-
|
|
104
|
-
points: list[np.ndarray] = []
|
|
105
|
-
for i in range(samples):
|
|
106
|
-
r = inner_radius + i * r_step
|
|
107
|
-
theta = phi * i
|
|
108
|
-
x = np.cos(theta) * r
|
|
109
|
-
y = np.sin(theta) * r
|
|
110
|
-
z = (rng.random() * 2.0 - 1.0) * z_thickness
|
|
111
|
-
points.append(center + np.array([x, y, z]))
|
|
112
|
-
return points
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _golden_spiral_2d(
|
|
116
|
-
samples: int,
|
|
117
|
-
radius: float = 1.0,
|
|
118
|
-
center: np.ndarray | None = None,
|
|
119
|
-
z: float = 0.0,
|
|
120
|
-
) -> list[np.ndarray]:
|
|
121
|
-
"""
|
|
122
|
-
Place *samples* points in the XY plane using a golden-angle disc spiral.
|
|
123
|
-
|
|
124
|
-
:param samples: Number of points.
|
|
125
|
-
:param radius: Outer radius of the disc.
|
|
126
|
-
:param center: XY centre (Z component ignored; overridden by *z*).
|
|
127
|
-
:param z: Fixed Z coordinate for all output points.
|
|
128
|
-
:return: List of 3-D coordinate arrays.
|
|
129
|
-
"""
|
|
130
|
-
if center is None:
|
|
131
|
-
center = np.zeros(3)
|
|
132
|
-
if samples <= 0:
|
|
133
|
-
return []
|
|
134
|
-
|
|
135
|
-
phi = np.pi * (3.0 - np.sqrt(5.0))
|
|
136
|
-
points: list[np.ndarray] = []
|
|
137
|
-
for i in range(samples):
|
|
138
|
-
r = radius * np.sqrt(i / max(samples - 1, 1))
|
|
139
|
-
theta = phi * i
|
|
140
|
-
x = r * np.cos(theta)
|
|
141
|
-
y = r * np.sin(theta)
|
|
142
|
-
points.append(center + np.array([x, y, z]))
|
|
143
|
-
return points
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
# ---------------------------------------------------------------------------
|
|
147
|
-
# Data transfer objects
|
|
148
|
-
# ---------------------------------------------------------------------------
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
@dataclass
|
|
152
|
-
class LayoutNode:
|
|
153
|
-
"""
|
|
154
|
-
Thin wrapper around a node dict from :class:`~pycode_kg.store.GraphStore`.
|
|
155
|
-
|
|
156
|
-
:param id: Stable node identifier (e.g. ``mod:src/foo.py``).
|
|
157
|
-
:param kind: Node kind — ``module``, ``class``, ``function``, ``method``,
|
|
158
|
-
or ``symbol``.
|
|
159
|
-
:param name: Short name of the node.
|
|
160
|
-
:param module_path: Source module path (may be ``None`` for symbol stubs).
|
|
161
|
-
:param docstring: Raw docstring text (may be ``None``).
|
|
162
|
-
:param lineno: First source line number (may be ``None``).
|
|
163
|
-
:param end_lineno: Last source line number (may be ``None``).
|
|
164
|
-
"""
|
|
165
|
-
|
|
166
|
-
id: str
|
|
167
|
-
kind: str
|
|
168
|
-
name: str
|
|
169
|
-
module_path: str | None = None
|
|
170
|
-
docstring: str | None = None
|
|
171
|
-
lineno: int | None = None
|
|
172
|
-
end_lineno: int | None = None
|
|
173
|
-
|
|
174
|
-
@classmethod
|
|
175
|
-
def from_dict(cls, d: dict) -> LayoutNode:
|
|
176
|
-
"""Construct from a GraphStore node dict.
|
|
177
|
-
|
|
178
|
-
:param d: Dict with keys ``id``, ``kind``, ``name``, etc.
|
|
179
|
-
:return: New :class:`LayoutNode`.
|
|
180
|
-
"""
|
|
181
|
-
return cls(
|
|
182
|
-
id=d["id"],
|
|
183
|
-
kind=d["kind"],
|
|
184
|
-
name=d["name"],
|
|
185
|
-
module_path=d.get("module_path"),
|
|
186
|
-
docstring=d.get("docstring"),
|
|
187
|
-
lineno=d.get("lineno"),
|
|
188
|
-
end_lineno=d.get("end_lineno"),
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
@property
|
|
192
|
-
def line_count(self) -> int:
|
|
193
|
-
"""Approximate source size in lines (0 if unknown).
|
|
194
|
-
|
|
195
|
-
:return: ``end_lineno - lineno`` or 0.
|
|
196
|
-
"""
|
|
197
|
-
if self.lineno and self.end_lineno:
|
|
198
|
-
return max(0, self.end_lineno - self.lineno)
|
|
199
|
-
return 0
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
@dataclass
|
|
203
|
-
class LayoutEdge:
|
|
204
|
-
"""
|
|
205
|
-
Thin wrapper around an edge dict from :class:`~pycode_kg.store.GraphStore`.
|
|
206
|
-
|
|
207
|
-
:param src: Source node ID.
|
|
208
|
-
:param rel: Relation type — ``CONTAINS``, ``CALLS``, ``IMPORTS``,
|
|
209
|
-
``INHERITS``.
|
|
210
|
-
:param dst: Destination node ID.
|
|
211
|
-
"""
|
|
212
|
-
|
|
213
|
-
src: str
|
|
214
|
-
rel: str
|
|
215
|
-
dst: str
|
|
216
|
-
|
|
217
|
-
@classmethod
|
|
218
|
-
def from_dict(cls, d: dict) -> LayoutEdge:
|
|
219
|
-
"""Construct from a GraphStore edge dict.
|
|
220
|
-
|
|
221
|
-
:param d: Dict with keys ``src``, ``rel``, ``dst``.
|
|
222
|
-
:return: New :class:`LayoutEdge`.
|
|
223
|
-
"""
|
|
224
|
-
return cls(src=d["src"], rel=d["rel"], dst=d["dst"])
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
# ---------------------------------------------------------------------------
|
|
228
|
-
# Abstract base
|
|
229
|
-
# ---------------------------------------------------------------------------
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
class Layout3D(ABC):
|
|
233
|
-
"""
|
|
234
|
-
Abstract base class for 3-D graph layout strategies.
|
|
235
|
-
|
|
236
|
-
Subclasses implement :meth:`compute` to assign a 3-D position to every
|
|
237
|
-
node, returning a ``{node_id: np.ndarray([x, y, z])}`` mapping that the
|
|
238
|
-
:class:`~pycode_kg.viz3d.KGViz3D` renderer consumes.
|
|
239
|
-
"""
|
|
240
|
-
|
|
241
|
-
@abstractmethod
|
|
242
|
-
def compute(
|
|
243
|
-
self,
|
|
244
|
-
nodes: list[LayoutNode],
|
|
245
|
-
edges: list[LayoutEdge],
|
|
246
|
-
) -> dict[str, np.ndarray]:
|
|
247
|
-
"""
|
|
248
|
-
Compute 3-D positions for all *nodes*.
|
|
249
|
-
|
|
250
|
-
:param nodes: All nodes in the graph.
|
|
251
|
-
:param edges: All edges in the graph (used to derive hierarchy).
|
|
252
|
-
:return: Mapping from node ID to ``[x, y, z]`` position.
|
|
253
|
-
"""
|
|
254
|
-
...
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
# ---------------------------------------------------------------------------
|
|
258
|
-
# AlliumLayout
|
|
259
|
-
# ---------------------------------------------------------------------------
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
class AlliumLayout(Layout3D):
|
|
263
|
-
"""
|
|
264
|
-
Allium-plant layout: each module is visualised as a Giant Allium flower.
|
|
265
|
-
|
|
266
|
-
Spatial structure:
|
|
267
|
-
|
|
268
|
-
- **Stem base** — module node sits at XY position in a Fibonacci annulus
|
|
269
|
-
at ``Z = 0``.
|
|
270
|
-
- **Head** — classes and top-level functions are distributed on a
|
|
271
|
-
Fibonacci sphere centred at the stem apex (``Z = stem_height``).
|
|
272
|
-
Head radius scales with ``sqrt(n_children)``.
|
|
273
|
-
- **Florets** — methods orbit their parent class on a smaller Fibonacci
|
|
274
|
-
sphere, slightly above the head.
|
|
275
|
-
- **Orphans** — nodes with no CONTAINS parent cluster on a small sphere
|
|
276
|
-
at the origin.
|
|
277
|
-
|
|
278
|
-
Multiple module-alliums are arranged in a Fibonacci annulus in the XY
|
|
279
|
-
plane so they are evenly spaced regardless of count.
|
|
280
|
-
|
|
281
|
-
Inspired by :class:`GiantAllium` in *repo_vis/pkg_visualizer/plants3d.py*.
|
|
282
|
-
|
|
283
|
-
:param stem_height: Height of each allium stem (Z offset of the head).
|
|
284
|
-
:param base_head_radius: Minimum radius for the Fibonacci sphere head.
|
|
285
|
-
:param method_orbit_radius: Base radius for method sub-spheres.
|
|
286
|
-
:param annulus_inner_radius: Inner radius of the module placement ring.
|
|
287
|
-
:param annulus_outer_radius: Minimum outer radius (auto-scaled for large graphs).
|
|
288
|
-
"""
|
|
289
|
-
|
|
290
|
-
def __init__(
|
|
291
|
-
self,
|
|
292
|
-
stem_height: float = 8.0,
|
|
293
|
-
base_head_radius: float = 2.0,
|
|
294
|
-
method_orbit_radius: float = 0.8,
|
|
295
|
-
annulus_inner_radius: float = 8.0,
|
|
296
|
-
annulus_outer_radius: float = 20.0,
|
|
297
|
-
) -> None:
|
|
298
|
-
"""Initialise layout parameters.
|
|
299
|
-
|
|
300
|
-
:param stem_height: Vertical height of each allium stem.
|
|
301
|
-
:param base_head_radius: Minimum allium head sphere radius.
|
|
302
|
-
:param method_orbit_radius: Base orbit radius for methods.
|
|
303
|
-
:param annulus_inner_radius: Inner radius for module ring placement.
|
|
304
|
-
:param annulus_outer_radius: Minimum outer radius for module ring.
|
|
305
|
-
"""
|
|
306
|
-
self.stem_height = stem_height
|
|
307
|
-
self.base_head_radius = base_head_radius
|
|
308
|
-
self.method_orbit_radius = method_orbit_radius
|
|
309
|
-
self.annulus_inner_radius = annulus_inner_radius
|
|
310
|
-
self.annulus_outer_radius = annulus_outer_radius
|
|
311
|
-
|
|
312
|
-
def compute(
|
|
313
|
-
self,
|
|
314
|
-
nodes: list[LayoutNode],
|
|
315
|
-
edges: list[LayoutEdge],
|
|
316
|
-
) -> dict[str, np.ndarray]:
|
|
317
|
-
"""
|
|
318
|
-
Compute allium-plant 3-D positions for all nodes.
|
|
319
|
-
|
|
320
|
-
:param nodes: All nodes in the graph.
|
|
321
|
-
:param edges: All edges (``CONTAINS`` used to derive hierarchy).
|
|
322
|
-
:return: Mapping from node ID to ``[x, y, z]`` position.
|
|
323
|
-
"""
|
|
324
|
-
# Build CONTAINS hierarchy: child_id -> parent_id, parent_id -> [child_ids]
|
|
325
|
-
parent: dict[str, str] = {}
|
|
326
|
-
children: dict[str, list[str]] = {}
|
|
327
|
-
for e in edges:
|
|
328
|
-
if e.rel == "CONTAINS":
|
|
329
|
-
parent[e.dst] = e.src
|
|
330
|
-
children.setdefault(e.src, []).append(e.dst)
|
|
331
|
-
|
|
332
|
-
node_by_id: dict[str, LayoutNode] = {n.id: n for n in nodes}
|
|
333
|
-
positions: dict[str, np.ndarray] = {}
|
|
334
|
-
|
|
335
|
-
# Module nodes form the allium stems
|
|
336
|
-
modules = [n for n in nodes if n.kind == "module"]
|
|
337
|
-
if not modules:
|
|
338
|
-
# Fallback: treat nodes without a CONTAINS parent as pseudo-modules
|
|
339
|
-
modules = [n for n in nodes if n.id not in parent]
|
|
340
|
-
|
|
341
|
-
n_mods = len(modules)
|
|
342
|
-
inner = self.annulus_inner_radius
|
|
343
|
-
# Scale outer radius so stems don't crowd each other
|
|
344
|
-
outer = max(self.annulus_outer_radius, inner + np.sqrt(n_mods) * 4.0)
|
|
345
|
-
|
|
346
|
-
mod_positions = fibonacci_annulus(
|
|
347
|
-
n_mods,
|
|
348
|
-
inner_radius=inner,
|
|
349
|
-
outer_radius=outer,
|
|
350
|
-
center=np.zeros(3),
|
|
351
|
-
z_thickness=0.0, # flat ring — alliums stand vertically
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
for mod_node, mod_pos in zip(modules, mod_positions):
|
|
355
|
-
positions[mod_node.id] = np.array(mod_pos)
|
|
356
|
-
stem_apex = np.array([mod_pos[0], mod_pos[1], self.stem_height])
|
|
357
|
-
|
|
358
|
-
# Direct children (classes, top-level functions)
|
|
359
|
-
direct_ids = children.get(mod_node.id, [])
|
|
360
|
-
direct = [node_by_id[cid] for cid in direct_ids if cid in node_by_id]
|
|
361
|
-
n_direct = len(direct)
|
|
362
|
-
if not direct:
|
|
363
|
-
continue
|
|
364
|
-
|
|
365
|
-
# Head radius scales with child count
|
|
366
|
-
head_r = self.base_head_radius + np.sqrt(n_direct) * 0.4
|
|
367
|
-
head_positions = fibonacci_sphere(n_direct, radius=head_r, center=stem_apex)
|
|
368
|
-
|
|
369
|
-
for child, child_pos in zip(direct, head_positions):
|
|
370
|
-
positions[child.id] = np.array(child_pos)
|
|
371
|
-
|
|
372
|
-
# Grandchildren (methods) orbit their parent class
|
|
373
|
-
grand_ids = children.get(child.id, [])
|
|
374
|
-
grand = [node_by_id[gid] for gid in grand_ids if gid in node_by_id]
|
|
375
|
-
n_grand = len(grand)
|
|
376
|
-
if not grand:
|
|
377
|
-
continue
|
|
378
|
-
|
|
379
|
-
method_r = self.method_orbit_radius + np.sqrt(n_grand) * 0.15
|
|
380
|
-
method_positions = fibonacci_sphere(
|
|
381
|
-
n_grand, radius=method_r, center=np.array(child_pos)
|
|
382
|
-
)
|
|
383
|
-
for gc, gc_pos in zip(grand, method_positions):
|
|
384
|
-
positions[gc.id] = np.array(gc_pos)
|
|
385
|
-
|
|
386
|
-
# Orphan nodes: anything not yet placed (symbols, unrooted nodes)
|
|
387
|
-
orphans = [n for n in nodes if n.id not in positions]
|
|
388
|
-
if orphans:
|
|
389
|
-
orphan_r = 3.0
|
|
390
|
-
orphan_positions = fibonacci_sphere(
|
|
391
|
-
len(orphans), radius=orphan_r, center=np.array([0.0, 0.0, orphan_r])
|
|
392
|
-
)
|
|
393
|
-
for n, pos in zip(orphans, orphan_positions):
|
|
394
|
-
positions[n.id] = np.array(pos)
|
|
395
|
-
|
|
396
|
-
return positions
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
# ---------------------------------------------------------------------------
|
|
400
|
-
# FunnelLayout
|
|
401
|
-
# ---------------------------------------------------------------------------
|
|
402
|
-
|
|
403
|
-
# Z levels and per-level radii come from pycode_kg.theme so the layout engine,
|
|
404
|
-
# the 3-D viewer and the 2-D explorer share one definition.
|
|
405
|
-
_KIND_ZLEVEL: dict[str, int] = dict(theme.KIND_ZLEVEL)
|
|
406
|
-
_LEVEL_NODE_SIZE: dict[int, float] = dict(theme.LEVEL_NODE_SIZE)
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
class FunnelLayout(Layout3D):
|
|
410
|
-
"""
|
|
411
|
-
Stratified layout: node *kind* determines the Z layer; XY positions use a
|
|
412
|
-
golden-angle disc spiral within each layer.
|
|
413
|
-
|
|
414
|
-
Layers (bottom to top):
|
|
415
|
-
|
|
416
|
-
- **Z = 0** — modules
|
|
417
|
-
- **Z = layer_gap** — classes
|
|
418
|
-
- **Z = 2 × layer_gap** — functions and methods
|
|
419
|
-
- **Z = 3 × layer_gap** — symbol stubs
|
|
420
|
-
|
|
421
|
-
Cross-cutting edges (``CALLS``, ``IMPORTS``, ``INHERITS``) arc between
|
|
422
|
-
layers, making structural coupling immediately visible from any angle.
|
|
423
|
-
|
|
424
|
-
Disc radius is derived algorithmically:
|
|
425
|
-
``r = node_spacing * node_size * sqrt(n)``
|
|
426
|
-
so the layout scales correctly for repos of any size without hand-tuning.
|
|
427
|
-
|
|
428
|
-
:param layer_gap: Vertical distance between adjacent layers.
|
|
429
|
-
:param node_spacing: Spacing multiplier — larger spreads layers out more.
|
|
430
|
-
"""
|
|
431
|
-
|
|
432
|
-
def __init__(
|
|
433
|
-
self,
|
|
434
|
-
layer_gap: float = 12.0,
|
|
435
|
-
node_spacing: float = 2.0,
|
|
436
|
-
) -> None:
|
|
437
|
-
"""Initialise layout parameters.
|
|
438
|
-
|
|
439
|
-
:param layer_gap: Vertical separation between layers.
|
|
440
|
-
:param node_spacing: Controls minimum gap between node surfaces.
|
|
441
|
-
"""
|
|
442
|
-
self.layer_gap = layer_gap
|
|
443
|
-
self.node_spacing = node_spacing
|
|
444
|
-
|
|
445
|
-
def compute(
|
|
446
|
-
self,
|
|
447
|
-
nodes: list[LayoutNode],
|
|
448
|
-
edges: list[LayoutEdge],
|
|
449
|
-
) -> dict[str, np.ndarray]:
|
|
450
|
-
"""
|
|
451
|
-
Compute funnel 3-D positions for all nodes.
|
|
452
|
-
|
|
453
|
-
:param nodes: All nodes in the graph.
|
|
454
|
-
:param edges: Unused by this layout (present for API compatibility).
|
|
455
|
-
:return: Mapping from node ID to ``[x, y, z]`` position.
|
|
456
|
-
"""
|
|
457
|
-
# Group nodes by Z layer
|
|
458
|
-
layers: dict[int, list[LayoutNode]] = {}
|
|
459
|
-
for n in nodes:
|
|
460
|
-
level = _KIND_ZLEVEL.get(n.kind, 3)
|
|
461
|
-
layers.setdefault(level, []).append(n)
|
|
462
|
-
|
|
463
|
-
positions: dict[str, np.ndarray] = {}
|
|
464
|
-
|
|
465
|
-
for level, layer_nodes in layers.items():
|
|
466
|
-
z = level * self.layer_gap
|
|
467
|
-
node_size = _LEVEL_NODE_SIZE.get(level, 0.7)
|
|
468
|
-
# Derived radius: scales with sqrt(n) and node size so no manual
|
|
469
|
-
# tuning is needed as the repo grows
|
|
470
|
-
r = self.node_spacing * node_size * np.sqrt(len(layer_nodes))
|
|
471
|
-
r = max(r, 4.0)
|
|
472
|
-
pts = _golden_spiral_2d(len(layer_nodes), radius=r, z=z)
|
|
473
|
-
for n, pt in zip(layer_nodes, pts):
|
|
474
|
-
positions[n.id] = pt
|
|
475
|
-
|
|
476
|
-
return positions
|
|
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
|
|
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
|