graphitect 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graphify/__init__.py +30 -0
- graphify/__main__.py +757 -0
- graphify/_minhash.py +107 -0
- graphify/affected.py +318 -0
- graphify/always_on/agents-md.md +12 -0
- graphify/always_on/antigravity-rules.md +14 -0
- graphify/always_on/claude-md.md +9 -0
- graphify/always_on/gemini-md.md +9 -0
- graphify/always_on/kiro-steering.md +5 -0
- graphify/always_on/vscode-instructions.md +17 -0
- graphify/analyze.py +769 -0
- graphify/benchmark.py +152 -0
- graphify/build.py +2300 -0
- graphify/cache.py +1746 -0
- graphify/callflow_html.py +2051 -0
- graphify/cargo_introspect.py +109 -0
- graphify/cli.py +4745 -0
- graphify/cluster.py +409 -0
- graphify/command-kilo.md +15 -0
- graphify/cross_repo_calls.py +216 -0
- graphify/cross_repo_types.py +75 -0
- graphify/csharp_dispatch.py +154 -0
- graphify/dedup.py +1213 -0
- graphify/detect.py +2566 -0
- graphify/diagnostics.py +406 -0
- graphify/export.py +1349 -0
- graphify/exporters/__init__.py +1 -0
- graphify/exporters/base.py +14 -0
- graphify/exporters/graphdb.py +173 -0
- graphify/exporters/html.py +637 -0
- graphify/extract.py +7856 -0
- graphify/extractors/MIGRATION.md +107 -0
- graphify/extractors/__init__.py +66 -0
- graphify/extractors/apex.py +215 -0
- graphify/extractors/base.py +85 -0
- graphify/extractors/bash.py +579 -0
- graphify/extractors/blade.py +53 -0
- graphify/extractors/commonlisp.py +540 -0
- graphify/extractors/csharp.py +448 -0
- graphify/extractors/dart.py +564 -0
- graphify/extractors/dm.py +494 -0
- graphify/extractors/elixir.py +241 -0
- graphify/extractors/engine.py +6509 -0
- graphify/extractors/fortran.py +311 -0
- graphify/extractors/go.py +527 -0
- graphify/extractors/json_config.py +240 -0
- graphify/extractors/julia.py +289 -0
- graphify/extractors/markdown.py +408 -0
- graphify/extractors/models.py +131 -0
- graphify/extractors/objc.py +566 -0
- graphify/extractors/ocaml.py +289 -0
- graphify/extractors/pascal.py +688 -0
- graphify/extractors/pascal_forms.py +196 -0
- graphify/extractors/powershell.py +522 -0
- graphify/extractors/razor.py +192 -0
- graphify/extractors/resolution.py +3584 -0
- graphify/extractors/robot.py +296 -0
- graphify/extractors/rust.py +470 -0
- graphify/extractors/sln.py +92 -0
- graphify/extractors/sql.py +720 -0
- graphify/extractors/terraform.py +181 -0
- graphify/extractors/verilog.py +329 -0
- graphify/extractors/zig.py +181 -0
- graphify/file_slice.py +246 -0
- graphify/global_graph.py +194 -0
- graphify/google_workspace.py +237 -0
- graphify/hooks.py +933 -0
- graphify/ids.py +93 -0
- graphify/ingest.py +358 -0
- graphify/install.py +2366 -0
- graphify/llm.py +3544 -0
- graphify/manifest.py +4 -0
- graphify/manifest_ingest.py +311 -0
- graphify/mcp_ingest.py +386 -0
- graphify/multigraph_compat.py +212 -0
- graphify/pascal_resolution.py +129 -0
- graphify/paths.py +436 -0
- graphify/pg_introspect.py +165 -0
- graphify/prs.py +770 -0
- graphify/querylog.py +80 -0
- graphify/reflect.py +882 -0
- graphify/report.py +346 -0
- graphify/resolver_registry.py +85 -0
- graphify/ruby_resolution.py +242 -0
- graphify/scip_ingest.py +363 -0
- graphify/security.py +460 -0
- graphify/semantic_cleanup.py +336 -0
- graphify/serve.py +2608 -0
- graphify/skill-agents.md +710 -0
- graphify/skill-aider.md +1283 -0
- graphify/skill-amp.md +710 -0
- graphify/skill-claw.md +713 -0
- graphify/skill-codex.md +710 -0
- graphify/skill-copilot.md +713 -0
- graphify/skill-devin.md +1410 -0
- graphify/skill-droid.md +710 -0
- graphify/skill-kilo.md +722 -0
- graphify/skill-kiro.md +713 -0
- graphify/skill-opencode.md +705 -0
- graphify/skill-pi.md +713 -0
- graphify/skill-trae.md +711 -0
- graphify/skill-vscode.md +709 -0
- graphify/skill-windows.md +755 -0
- graphify/skill.md +713 -0
- graphify/skills/agents/references/add-watch.md +56 -0
- graphify/skills/agents/references/exports.md +87 -0
- graphify/skills/agents/references/extraction-spec.md +70 -0
- graphify/skills/agents/references/github-and-merge.md +46 -0
- graphify/skills/agents/references/hooks.md +33 -0
- graphify/skills/agents/references/query.md +311 -0
- graphify/skills/agents/references/transcribe.md +52 -0
- graphify/skills/agents/references/update.md +210 -0
- graphify/skills/amp/references/add-watch.md +56 -0
- graphify/skills/amp/references/exports.md +87 -0
- graphify/skills/amp/references/extraction-spec.md +70 -0
- graphify/skills/amp/references/github-and-merge.md +46 -0
- graphify/skills/amp/references/hooks.md +33 -0
- graphify/skills/amp/references/query.md +311 -0
- graphify/skills/amp/references/transcribe.md +52 -0
- graphify/skills/amp/references/update.md +210 -0
- graphify/skills/claude/references/add-watch.md +56 -0
- graphify/skills/claude/references/exports.md +87 -0
- graphify/skills/claude/references/extraction-spec.md +70 -0
- graphify/skills/claude/references/github-and-merge.md +46 -0
- graphify/skills/claude/references/hooks.md +33 -0
- graphify/skills/claude/references/query.md +311 -0
- graphify/skills/claude/references/transcribe.md +52 -0
- graphify/skills/claude/references/update.md +210 -0
- graphify/skills/claw/references/add-watch.md +56 -0
- graphify/skills/claw/references/exports.md +87 -0
- graphify/skills/claw/references/extraction-spec.md +31 -0
- graphify/skills/claw/references/github-and-merge.md +46 -0
- graphify/skills/claw/references/hooks.md +33 -0
- graphify/skills/claw/references/query.md +311 -0
- graphify/skills/claw/references/transcribe.md +52 -0
- graphify/skills/claw/references/update.md +210 -0
- graphify/skills/codex/references/add-watch.md +56 -0
- graphify/skills/codex/references/exports.md +87 -0
- graphify/skills/codex/references/extraction-spec.md +31 -0
- graphify/skills/codex/references/github-and-merge.md +46 -0
- graphify/skills/codex/references/hooks.md +33 -0
- graphify/skills/codex/references/query.md +311 -0
- graphify/skills/codex/references/transcribe.md +52 -0
- graphify/skills/codex/references/update.md +210 -0
- graphify/skills/copilot/references/add-watch.md +56 -0
- graphify/skills/copilot/references/exports.md +87 -0
- graphify/skills/copilot/references/extraction-spec.md +70 -0
- graphify/skills/copilot/references/github-and-merge.md +46 -0
- graphify/skills/copilot/references/hooks.md +33 -0
- graphify/skills/copilot/references/query.md +311 -0
- graphify/skills/copilot/references/transcribe.md +52 -0
- graphify/skills/copilot/references/update.md +210 -0
- graphify/skills/droid/references/add-watch.md +56 -0
- graphify/skills/droid/references/exports.md +87 -0
- graphify/skills/droid/references/extraction-spec.md +70 -0
- graphify/skills/droid/references/github-and-merge.md +46 -0
- graphify/skills/droid/references/hooks.md +33 -0
- graphify/skills/droid/references/query.md +311 -0
- graphify/skills/droid/references/transcribe.md +52 -0
- graphify/skills/droid/references/update.md +210 -0
- graphify/skills/kilo/references/add-watch.md +56 -0
- graphify/skills/kilo/references/exports.md +87 -0
- graphify/skills/kilo/references/extraction-spec.md +70 -0
- graphify/skills/kilo/references/github-and-merge.md +46 -0
- graphify/skills/kilo/references/hooks.md +33 -0
- graphify/skills/kilo/references/query.md +311 -0
- graphify/skills/kilo/references/transcribe.md +52 -0
- graphify/skills/kilo/references/update.md +210 -0
- graphify/skills/kiro/references/add-watch.md +56 -0
- graphify/skills/kiro/references/exports.md +87 -0
- graphify/skills/kiro/references/extraction-spec.md +31 -0
- graphify/skills/kiro/references/github-and-merge.md +46 -0
- graphify/skills/kiro/references/hooks.md +33 -0
- graphify/skills/kiro/references/query.md +311 -0
- graphify/skills/kiro/references/transcribe.md +52 -0
- graphify/skills/kiro/references/update.md +210 -0
- graphify/skills/opencode/references/add-watch.md +56 -0
- graphify/skills/opencode/references/exports.md +87 -0
- graphify/skills/opencode/references/extraction-spec.md +70 -0
- graphify/skills/opencode/references/github-and-merge.md +46 -0
- graphify/skills/opencode/references/hooks.md +33 -0
- graphify/skills/opencode/references/query.md +311 -0
- graphify/skills/opencode/references/transcribe.md +52 -0
- graphify/skills/opencode/references/update.md +210 -0
- graphify/skills/pi/references/add-watch.md +56 -0
- graphify/skills/pi/references/exports.md +87 -0
- graphify/skills/pi/references/extraction-spec.md +31 -0
- graphify/skills/pi/references/github-and-merge.md +46 -0
- graphify/skills/pi/references/hooks.md +33 -0
- graphify/skills/pi/references/query.md +311 -0
- graphify/skills/pi/references/transcribe.md +52 -0
- graphify/skills/pi/references/update.md +210 -0
- graphify/skills/trae/references/add-watch.md +56 -0
- graphify/skills/trae/references/exports.md +87 -0
- graphify/skills/trae/references/extraction-spec.md +70 -0
- graphify/skills/trae/references/github-and-merge.md +46 -0
- graphify/skills/trae/references/hooks.md +35 -0
- graphify/skills/trae/references/query.md +311 -0
- graphify/skills/trae/references/transcribe.md +52 -0
- graphify/skills/trae/references/update.md +210 -0
- graphify/skills/vscode/references/add-watch.md +56 -0
- graphify/skills/vscode/references/exports.md +87 -0
- graphify/skills/vscode/references/extraction-spec.md +70 -0
- graphify/skills/vscode/references/github-and-merge.md +46 -0
- graphify/skills/vscode/references/hooks.md +33 -0
- graphify/skills/vscode/references/query.md +311 -0
- graphify/skills/vscode/references/transcribe.md +52 -0
- graphify/skills/vscode/references/update.md +210 -0
- graphify/skills/windows/references/add-watch.md +56 -0
- graphify/skills/windows/references/exports.md +87 -0
- graphify/skills/windows/references/extraction-spec.md +70 -0
- graphify/skills/windows/references/github-and-merge.md +46 -0
- graphify/skills/windows/references/hooks.md +33 -0
- graphify/skills/windows/references/query.md +311 -0
- graphify/skills/windows/references/transcribe.md +52 -0
- graphify/skills/windows/references/update.md +210 -0
- graphify/symbol_resolution.py +556 -0
- graphify/transcribe.py +186 -0
- graphify/tree_html.py +603 -0
- graphify/validate.py +95 -0
- graphify/watch.py +2280 -0
- graphify/wiki.py +405 -0
- graphitect/__init__.py +28 -0
- graphitect/__main__.py +4 -0
- graphitect/_vendor/__init__.py +2 -0
- graphitect/_vendor/archify/LICENSE +22 -0
- graphitect/_vendor/archify/SKILL.md +137 -0
- graphitect/_vendor/archify/THIRD_PARTY_NOTICES.md +69 -0
- graphitect/_vendor/archify/assets/JetBrainsMono-OFL.txt +93 -0
- graphitect/_vendor/archify/assets/template.html +14935 -0
- graphitect/_vendor/archify/bin/archify.mjs +2091 -0
- graphitect/_vendor/archify/bin/open-artifact.mjs +86 -0
- graphitect/_vendor/archify/bin/preview.mjs +653 -0
- graphitect/_vendor/archify/bin/visual-check.mjs +829 -0
- graphitect/_vendor/archify/brand-marks/README.md +31 -0
- graphitect/_vendor/archify/brand-marks/catalog.json +131 -0
- graphitect/_vendor/archify/delta/architecture-delta.mjs +1221 -0
- graphitect/_vendor/archify/examples/agent-run.lifecycle.json +60 -0
- graphitect/_vendor/archify/examples/agent-tool-call.workflow.json +94 -0
- graphitect/_vendor/archify/examples/async-job-roundtrip.sequence.json +61 -0
- graphitect/_vendor/archify/examples/brand-aware-delivery.architecture.json +47 -0
- graphitect/_vendor/archify/examples/cache-miss-request.sequence.json +82 -0
- graphitect/_vendor/archify/examples/checkout-platform.base.architecture.json +31 -0
- graphitect/_vendor/archify/examples/checkout-platform.head.architecture.json +31 -0
- graphitect/_vendor/archify/examples/dataflow-product-analytics.html +15045 -0
- graphitect/_vendor/archify/examples/deployment-release.lifecycle.json +49 -0
- graphitect/_vendor/archify/examples/event-stream.dataflow.json +57 -0
- graphitect/_vendor/archify/examples/incident-response.workflow.json +64 -0
- graphitect/_vendor/archify/examples/lifecycle-agent-run.html +14980 -0
- graphitect/_vendor/archify/examples/product-analytics.dataflow.json +76 -0
- graphitect/_vendor/archify/examples/production-deployment.architecture.json +71 -0
- graphitect/_vendor/archify/examples/release-delivery.workflow.json +62 -0
- graphitect/_vendor/archify/examples/sequence-cache-miss-request.html +15060 -0
- graphitect/_vendor/archify/examples/web-app-rendered.html +15009 -0
- graphitect/_vendor/archify/examples/web-app.architecture.json +46 -0
- graphitect/_vendor/archify/examples/workflow-agent-tool-call-rendered.html +15051 -0
- graphitect/_vendor/archify/migrations/workflow-v2.mjs +279 -0
- graphitect/_vendor/archify/package-lock.json +149 -0
- graphitect/_vendor/archify/package.json +39 -0
- graphitect/_vendor/archify/recipes/scenarios.mjs +391 -0
- graphitect/_vendor/archify/references/authoring-contract.md +243 -0
- graphitect/_vendor/archify/references/brand-marks.md +65 -0
- graphitect/_vendor/archify/references/delivery-contract.md +120 -0
- graphitect/_vendor/archify/references/viewer-runtime.md +45 -0
- graphitect/_vendor/archify/renderers/architecture/grid.mjs +62 -0
- graphitect/_vendor/archify/renderers/architecture/render-architecture.mjs +1078 -0
- graphitect/_vendor/archify/renderers/dataflow/README.md +104 -0
- graphitect/_vendor/archify/renderers/dataflow/render-dataflow.mjs +483 -0
- graphitect/_vendor/archify/renderers/lifecycle/README.md +115 -0
- graphitect/_vendor/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
- graphitect/_vendor/archify/renderers/sequence/README.md +114 -0
- graphitect/_vendor/archify/renderers/sequence/render-sequence.mjs +464 -0
- graphitect/_vendor/archify/renderers/shared/brand-marks.mjs +563 -0
- graphitect/_vendor/archify/renderers/shared/cli.mjs +218 -0
- graphitect/_vendor/archify/renderers/shared/desktop-readability.mjs +26 -0
- graphitect/_vendor/archify/renderers/shared/diagnostics.mjs +127 -0
- graphitect/_vendor/archify/renderers/shared/engineering-profiles.mjs +157 -0
- graphitect/_vendor/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
- graphitect/_vendor/archify/renderers/shared/generated-validators.mjs +13 -0
- graphitect/_vendor/archify/renderers/shared/geometry.mjs +1423 -0
- graphitect/_vendor/archify/renderers/shared/i18n.mjs +595 -0
- graphitect/_vendor/archify/renderers/shared/layout-report.mjs +40 -0
- graphitect/_vendor/archify/renderers/shared/legend.mjs +217 -0
- graphitect/_vendor/archify/renderers/shared/output-path.mjs +340 -0
- graphitect/_vendor/archify/renderers/shared/repository-evidence.mjs +238 -0
- graphitect/_vendor/archify/renderers/shared/repository-location.mjs +58 -0
- graphitect/_vendor/archify/renderers/shared/text-fit.mjs +49 -0
- graphitect/_vendor/archify/renderers/shared/utils.mjs +232 -0
- graphitect/_vendor/archify/renderers/shared/validator.mjs +86 -0
- graphitect/_vendor/archify/renderers/workflow/README.md +223 -0
- graphitect/_vendor/archify/renderers/workflow/render-workflow.mjs +35 -0
- graphitect/_vendor/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
- graphitect/_vendor/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
- graphitect/_vendor/archify/schemas/README.md +211 -0
- graphitect/_vendor/archify/schemas/architecture.schema.json +178 -0
- graphitect/_vendor/archify/schemas/common.schema.json +115 -0
- graphitect/_vendor/archify/schemas/dataflow.schema.json +243 -0
- graphitect/_vendor/archify/schemas/lifecycle.schema.json +266 -0
- graphitect/_vendor/archify/schemas/sequence.schema.json +223 -0
- graphitect/_vendor/archify/schemas/workflow.schema.json +428 -0
- graphitect/_vendor/archify/scripts/check-render-output.mjs +836 -0
- graphitect/_vendor/archify/scripts/check-update.mjs +1667 -0
- graphitect/_vendor/archify/scripts/generate-brand-marks.mjs +141 -0
- graphitect/_vendor/archify/scripts/generate-validators.mjs +66 -0
- graphitect/_vendor/archify/scripts/render-examples.mjs +26 -0
- graphitect/_vendor/archify/scripts/update-contract.mjs +182 -0
- graphitect/_vendor/archify/skill-release.json +10 -0
- graphitect/cli.py +981 -0
- graphitect/deliver/__init__.py +5 -0
- graphitect/deliver/archify_adapter.py +1877 -0
- graphitect/deliver/archify_ir.py +160 -0
- graphitect/deliver/archify_repair.py +135 -0
- graphitect/deliver/doc_compiler.py +916 -0
- graphitect/ground/__init__.py +5 -0
- graphitect/ground/describe_source.py +27 -0
- graphitect/ground/fullread_source.py +56 -0
- graphitect/ground/graphify_source.py +107 -0
- graphitect/models.py +118 -0
- graphitect/skill/SKILL.md +80 -0
- graphitect/skill/agents/openai.yaml +4 -0
- graphitect/synthesize/__init__.py +5 -0
- graphitect/synthesize/engine.py +281 -0
- graphitect/synthesize/llm_backend.py +331 -0
- graphitect/synthesize/questions.py +139 -0
- graphitect/synthesize/rubric.py +104 -0
- graphitect-0.2.0.dist-info/METADATA +284 -0
- graphitect-0.2.0.dist-info/RECORD +336 -0
- graphitect-0.2.0.dist-info/WHEEL +5 -0
- graphitect-0.2.0.dist-info/entry_points.txt +2 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE +21 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE-ARCHIFY-MIT +22 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE-GRAPHIFY-APACHE-2.0 +202 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE-GRAPHIFY-MIT +21 -0
- graphitect-0.2.0.dist-info/licenses/NOTICE-ARCHIFY-THIRD-PARTY.md +69 -0
- graphitect-0.2.0.dist-info/licenses/NOTICE-GRAPHIFY +8 -0
- graphitect-0.2.0.dist-info/top_level.txt +2 -0
graphify/tree_html.py
ADDED
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
"""tree_html — emit a D3 v7 collapsible-tree HTML view of a graph.
|
|
2
|
+
|
|
3
|
+
A self-contained printable / browseable tree-of-modules view
|
|
4
|
+
intended to complement the existing force-directed ``graph.html``.
|
|
5
|
+
Key visual elements:
|
|
6
|
+
|
|
7
|
+
* Expand-all / collapse-all / reset-view buttons.
|
|
8
|
+
* Multi-line label wrapping (``wrapText``) with separately-coloured
|
|
9
|
+
name and descendant-count.
|
|
10
|
+
* Depth-based colour palette (top-level directories get distinct
|
|
11
|
+
accent colours; deeper levels follow a level-specific palette).
|
|
12
|
+
* Click-to-toggle subtree.
|
|
13
|
+
|
|
14
|
+
Tree-data shape:
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
"name": "<root label>",
|
|
18
|
+
"total_count": <int>,
|
|
19
|
+
"children": [ { "name", "total_count", "children": [...] }, ... ]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
CLI: ``graphify tree [--graph PATH] [--output HTML] [--root PATH]
|
|
23
|
+
[--max-children N] [--label NAME]``.
|
|
24
|
+
|
|
25
|
+
Implementation notes:
|
|
26
|
+
- ``total_count`` is the descendant leaf count, so collapsed nodes
|
|
27
|
+
can show ``(Total Count: 95)`` without needing the children loaded.
|
|
28
|
+
- ``--max-children`` (default 200) caps how many children render
|
|
29
|
+
under any one node; a synthetic ``(+N more)`` leaf appears when the
|
|
30
|
+
cap fires so very wide directories stay usable.
|
|
31
|
+
- The first-level palette is auto-populated from the live top-level
|
|
32
|
+
directories so each gets a stable accent colour.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import html as _html
|
|
38
|
+
import json
|
|
39
|
+
from collections import defaultdict
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import Any, Dict, List, Optional
|
|
42
|
+
|
|
43
|
+
DEFAULT_MAX_CHILDREN = 200
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ── Tree builder (filesystem hierarchy → JSON) ──────────────────
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _common_root(paths: List[str]) -> str:
|
|
50
|
+
if not paths:
|
|
51
|
+
return ""
|
|
52
|
+
parts = [Path(p).parts for p in paths if p]
|
|
53
|
+
if not parts:
|
|
54
|
+
return ""
|
|
55
|
+
common = parts[0]
|
|
56
|
+
for p in parts[1:]:
|
|
57
|
+
i = 0
|
|
58
|
+
while i < len(common) and i < len(p) and common[i] == p[i]:
|
|
59
|
+
i += 1
|
|
60
|
+
common = common[:i]
|
|
61
|
+
return str(Path(*common)) if common else ""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _make_truncation_leaf(extra: int) -> Dict[str, Any]:
|
|
65
|
+
return {"name": f"(+{extra} more)", "total_count": extra, "children": []}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def build_tree(
|
|
69
|
+
graph: Dict[str, Any],
|
|
70
|
+
*,
|
|
71
|
+
root: Optional[str] = None,
|
|
72
|
+
max_children: int = DEFAULT_MAX_CHILDREN,
|
|
73
|
+
project_label: Optional[str] = None,
|
|
74
|
+
) -> Dict[str, Any]:
|
|
75
|
+
"""Build a ``{name, total_count, children}`` hierarchy.
|
|
76
|
+
|
|
77
|
+
Each leaf is either a code symbol (class / top-level function) or
|
|
78
|
+
a synthetic "(+N more)" placeholder for truncated wide directories.
|
|
79
|
+
Each interior node carries ``total_count = sum of leaf counts``.
|
|
80
|
+
|
|
81
|
+
Raises ``ValueError`` when an explicit ``root`` matches none of the
|
|
82
|
+
graph's ``source_file`` paths — every file would silently attach flat
|
|
83
|
+
to the root and the hierarchy would be lost (#2534).
|
|
84
|
+
"""
|
|
85
|
+
nodes: List[Dict[str, Any]] = list(graph.get("nodes", []))
|
|
86
|
+
file_nodes = [n for n in nodes if n.get("source_file")]
|
|
87
|
+
if not file_nodes:
|
|
88
|
+
return {"name": "(empty graph)", "total_count": 0, "children": []}
|
|
89
|
+
|
|
90
|
+
explicit_root = root is not None
|
|
91
|
+
if root is None:
|
|
92
|
+
root = _common_root([n["source_file"] for n in file_nodes])
|
|
93
|
+
root_path = Path(root)
|
|
94
|
+
|
|
95
|
+
by_file: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
|
96
|
+
for n in file_nodes:
|
|
97
|
+
by_file[n["source_file"]].append(n)
|
|
98
|
+
|
|
99
|
+
# Build dir tree.
|
|
100
|
+
dir_index: Dict[str, Dict[str, Any]] = {}
|
|
101
|
+
label_root = project_label or root_path.name or root or "/"
|
|
102
|
+
root_node: Dict[str, Any] = {
|
|
103
|
+
"name": label_root, "total_count": 0, "children": [],
|
|
104
|
+
}
|
|
105
|
+
dir_index[str(root_path)] = root_node
|
|
106
|
+
|
|
107
|
+
def _ensure_dir(abs_path: Path) -> Dict[str, Any]:
|
|
108
|
+
key = str(abs_path)
|
|
109
|
+
if key in dir_index:
|
|
110
|
+
return dir_index[key]
|
|
111
|
+
if abs_path == abs_path.parent:
|
|
112
|
+
return root_node
|
|
113
|
+
parent = (_ensure_dir(abs_path.parent)
|
|
114
|
+
if abs_path.parent != abs_path else root_node)
|
|
115
|
+
node = {"name": abs_path.name, "total_count": 0, "children": []}
|
|
116
|
+
dir_index[key] = node
|
|
117
|
+
parent["children"].append(node)
|
|
118
|
+
return node
|
|
119
|
+
|
|
120
|
+
matched_files = 0
|
|
121
|
+
for src_file, syms in sorted(by_file.items()):
|
|
122
|
+
src_path = Path(src_file)
|
|
123
|
+
try:
|
|
124
|
+
rel = src_path.relative_to(root_path)
|
|
125
|
+
parent_path = (root_path / rel).parent
|
|
126
|
+
matched_files += 1
|
|
127
|
+
except ValueError:
|
|
128
|
+
parent_path = root_path
|
|
129
|
+
parent_dir = _ensure_dir(parent_path)
|
|
130
|
+
|
|
131
|
+
# File node — children are the symbols.
|
|
132
|
+
sym_children: List[Dict[str, Any]] = []
|
|
133
|
+
for n in syms:
|
|
134
|
+
label = n.get("label", n.get("id", "?"))
|
|
135
|
+
# Skip the redundant file-name node graphify emits (bare basename or
|
|
136
|
+
# the directory-qualified form from the #2032 disambiguation pass).
|
|
137
|
+
if n.get("file_type") == "code":
|
|
138
|
+
from graphify.build import _is_file_node_label
|
|
139
|
+
if _is_file_node_label(label, src_file):
|
|
140
|
+
continue
|
|
141
|
+
sym_children.append({
|
|
142
|
+
"name": label,
|
|
143
|
+
"total_count": 1,
|
|
144
|
+
"children": [],
|
|
145
|
+
})
|
|
146
|
+
# Sort: code symbols first by name, then anything else.
|
|
147
|
+
sym_children.sort(key=lambda c: (
|
|
148
|
+
c["name"].startswith("_"),
|
|
149
|
+
c["name"].lower(),
|
|
150
|
+
))
|
|
151
|
+
if len(sym_children) > max_children:
|
|
152
|
+
extra = len(sym_children) - max_children
|
|
153
|
+
sym_children = sym_children[:max_children] + [
|
|
154
|
+
_make_truncation_leaf(extra),
|
|
155
|
+
]
|
|
156
|
+
file_node = {
|
|
157
|
+
"name": src_path.name,
|
|
158
|
+
"total_count": len(sym_children) or 1,
|
|
159
|
+
"children": sym_children,
|
|
160
|
+
}
|
|
161
|
+
parent_dir["children"].append(file_node)
|
|
162
|
+
|
|
163
|
+
# An explicit --root that matches NOTHING would silently flatten the whole
|
|
164
|
+
# hierarchy (every file attaches to the root); the common case is passing an
|
|
165
|
+
# absolute checkout path while source_file is stored repo-relative (#2534).
|
|
166
|
+
# A PARTIAL match stays fine — files outside the root legitimately attach flat.
|
|
167
|
+
if explicit_root and matched_files == 0:
|
|
168
|
+
sample = min(by_file)
|
|
169
|
+
raise ValueError(
|
|
170
|
+
f"--root {root} matched 0 of {len(by_file)} source files "
|
|
171
|
+
f"(source_file paths are stored repo-relative, e.g. {sample!r})"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Sort each dir's children + propagate total_count up.
|
|
175
|
+
def _finalise(d: Dict[str, Any]) -> int:
|
|
176
|
+
kids = d.get("children") or []
|
|
177
|
+
kids.sort(key=lambda c: (
|
|
178
|
+
0 if (c.get("children") and len(c["children"]) > 0) else 1,
|
|
179
|
+
c["name"].lower(),
|
|
180
|
+
))
|
|
181
|
+
if not kids:
|
|
182
|
+
return d.get("total_count") or 1
|
|
183
|
+
n = 0
|
|
184
|
+
for c in kids:
|
|
185
|
+
n += _finalise(c)
|
|
186
|
+
d["total_count"] = n or 1
|
|
187
|
+
return d["total_count"]
|
|
188
|
+
|
|
189
|
+
_finalise(root_node)
|
|
190
|
+
return root_node
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ── HTML emitter (single-data-blob substitution) ──────────────────
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# We emit a Python f-string with literal CSS/JS braces escaped as {{ }}.
|
|
197
|
+
_HTML_TEMPLATE = r"""<!DOCTYPE html>
|
|
198
|
+
<html lang="en">
|
|
199
|
+
<head>
|
|
200
|
+
<meta charset="UTF-8">
|
|
201
|
+
<title>{title}</title>
|
|
202
|
+
<style>
|
|
203
|
+
body {{
|
|
204
|
+
font-family: 'Segoe UI', sans-serif;
|
|
205
|
+
margin: 0;
|
|
206
|
+
padding: 0;
|
|
207
|
+
background: #f9f9f9;
|
|
208
|
+
color: #333;
|
|
209
|
+
}}
|
|
210
|
+
h1 {{
|
|
211
|
+
margin: 20px 0 0 24px;
|
|
212
|
+
font-size: 2.2rem;
|
|
213
|
+
font-weight: bold;
|
|
214
|
+
color: #1e3a56;
|
|
215
|
+
}}
|
|
216
|
+
.controls {{
|
|
217
|
+
margin: 20px 0 15px 24px;
|
|
218
|
+
}}
|
|
219
|
+
button {{
|
|
220
|
+
margin-right: 10px;
|
|
221
|
+
padding: 8px 18px;
|
|
222
|
+
background: #007bff;
|
|
223
|
+
color: #fff;
|
|
224
|
+
border: none;
|
|
225
|
+
border-radius: 5px;
|
|
226
|
+
font-size: 0.95rem;
|
|
227
|
+
cursor: pointer;
|
|
228
|
+
transition: background 0.2s ease-in-out;
|
|
229
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
230
|
+
}}
|
|
231
|
+
button:hover {{
|
|
232
|
+
background: #0056b3;
|
|
233
|
+
}}
|
|
234
|
+
button:active {{
|
|
235
|
+
background: #004085;
|
|
236
|
+
}}
|
|
237
|
+
#tree-container {{
|
|
238
|
+
width: calc(100vw - 48px); /* Adjust for body margin/padding */
|
|
239
|
+
height: 85vh;
|
|
240
|
+
overflow: auto;
|
|
241
|
+
border-radius: 8px;
|
|
242
|
+
background: #fff;
|
|
243
|
+
margin-left: 24px;
|
|
244
|
+
margin-right: 24px;
|
|
245
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
|
246
|
+
border: 1px solid #ddd;
|
|
247
|
+
}}
|
|
248
|
+
svg {{
|
|
249
|
+
background: #fff;
|
|
250
|
+
border-radius: 8px;
|
|
251
|
+
display: block; /* Important for D3 */
|
|
252
|
+
}}
|
|
253
|
+
.node circle {{
|
|
254
|
+
stroke-width: 2.5px;
|
|
255
|
+
}}
|
|
256
|
+
.node text {{ /* Base style for the <text> container */
|
|
257
|
+
font: 13px 'Segoe UI', sans-serif;
|
|
258
|
+
paint-order: stroke fill; /* Ensures text is readable over lines */
|
|
259
|
+
stroke: #fff; /* White halo */
|
|
260
|
+
stroke-width: 3px; /* Halo thickness */
|
|
261
|
+
stroke-linejoin: round;
|
|
262
|
+
stroke-opacity: 0.85; /* Halo opacity */
|
|
263
|
+
}}
|
|
264
|
+
.link {{
|
|
265
|
+
fill: none;
|
|
266
|
+
stroke-opacity: 0.7;
|
|
267
|
+
stroke-width: 2px;
|
|
268
|
+
}}
|
|
269
|
+
</style>
|
|
270
|
+
</head>
|
|
271
|
+
<body>
|
|
272
|
+
<h1>{header}</h1>
|
|
273
|
+
<div class="controls">
|
|
274
|
+
<button onclick="expandAll()">Expand All</button>
|
|
275
|
+
<button onclick="collapseAll()">Collapse All</button>
|
|
276
|
+
<button onclick="resetView()">Reset View</button>
|
|
277
|
+
</div>
|
|
278
|
+
<div id="tree-container">
|
|
279
|
+
<svg id="tree-svg" width="{svg_width}" height="{svg_height}"></svg>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
283
|
+
<script>
|
|
284
|
+
const initialJsonData = {data_json};
|
|
285
|
+
|
|
286
|
+
function transformData(jsonData) {{
|
|
287
|
+
// Helper function to recursively build the children structure
|
|
288
|
+
function processNode(node, parentL1StageName) {{
|
|
289
|
+
let displayName = node.name;
|
|
290
|
+
// Append total_count if it exists and is not already in the name
|
|
291
|
+
if (node.total_count !== undefined) {{
|
|
292
|
+
if (!/\(Total Count: \d+\)$/.test(displayName)) {{
|
|
293
|
+
displayName += ` (Total Count: ${{node.total_count}})`;
|
|
294
|
+
}}
|
|
295
|
+
}}
|
|
296
|
+
|
|
297
|
+
const newNode = {{ name: displayName }};
|
|
298
|
+
|
|
299
|
+
if (parentL1StageName === "Root") {{
|
|
300
|
+
newNode.originalStageName = node.name;
|
|
301
|
+
}} else {{
|
|
302
|
+
newNode.originalStageName = parentL1StageName;
|
|
303
|
+
}}
|
|
304
|
+
|
|
305
|
+
if (node.children && node.children.length > 0) {{
|
|
306
|
+
const stageNameToPass = (parentL1StageName === "Root") ? node.name : parentL1StageName;
|
|
307
|
+
newNode.children = node.children.map(child => processNode(child, stageNameToPass));
|
|
308
|
+
}}
|
|
309
|
+
|
|
310
|
+
return newNode;
|
|
311
|
+
}}
|
|
312
|
+
|
|
313
|
+
let rootDisplayName = jsonData.name;
|
|
314
|
+
if (jsonData.total_count !== undefined && !/\(Total Count: \d+\)$/.test(rootDisplayName)) {{
|
|
315
|
+
rootDisplayName += ` (Total Count: ${{jsonData.total_count}})`;
|
|
316
|
+
}}
|
|
317
|
+
|
|
318
|
+
return {{
|
|
319
|
+
name: rootDisplayName,
|
|
320
|
+
originalStageName: "Root",
|
|
321
|
+
children: (jsonData.children || []).map(child => processNode(child, "Root"))
|
|
322
|
+
}};
|
|
323
|
+
}}
|
|
324
|
+
|
|
325
|
+
const treeData = transformData(initialJsonData);
|
|
326
|
+
|
|
327
|
+
// Auto-populated phaseColors: every depth-1 child of the root gets
|
|
328
|
+
// a stable colour from a bigger palette so all top-level dirs are
|
|
329
|
+
// distinguishable.
|
|
330
|
+
const PALETTE = [
|
|
331
|
+
["#3498DB","#2980B9","#AED6F1"], ["#2ECC71","#27AE60","#A9DFBF"],
|
|
332
|
+
["#E74C3C","#C0392B","#F5B7B1"], ["#9B59B6","#8E44AD","#D7BDE2"],
|
|
333
|
+
["#F39C12","#D68910","#FAD7A0"], ["#1ABC9C","#117864","#A2D9CE"],
|
|
334
|
+
["#34495E","#1B2631","#ABB2B9"], ["#E67E22","#BA4A00","#F5CBA7"],
|
|
335
|
+
["#16A085","#0E6655","#A2D9CE"], ["#D35400","#A04000","#EDBB99"],
|
|
336
|
+
["#7F8C8D","#566573","#D5DBDB"], ["#C0392B","#7B241C","#F5B7B1"],
|
|
337
|
+
["#2E86C1","#1B4F72","#A9CCE3"], ["#28B463","#196F3D","#A9DFBF"],
|
|
338
|
+
["#AF7AC5","#6C3483","#D2B4DE"],
|
|
339
|
+
];
|
|
340
|
+
const phaseColors = {{ "Root": {{ fill: "#4A4A4A", stroke: "#333333", collapsedFill: "#6C757D" }},
|
|
341
|
+
"Default": {{ fill: "#BDC3C7", stroke: "#95A5A6", collapsedFill: "#ECF0F1" }} }};
|
|
342
|
+
(initialJsonData.children || []).forEach((c, i) => {{
|
|
343
|
+
const pal = PALETTE[i % PALETTE.length];
|
|
344
|
+
phaseColors[c.name] = {{ fill: pal[0], stroke: pal[1], collapsedFill: pal[2] }};
|
|
345
|
+
}});
|
|
346
|
+
|
|
347
|
+
const levelSpecificPalettes = {{
|
|
348
|
+
0: {{ fill: "#4A4A4A", stroke: "#333333", collapsedFill: "#6C757D" }},
|
|
349
|
+
2: {{ fill: "#6ab04c", stroke: "#508a38", collapsedFill: "#a3d391" }},
|
|
350
|
+
3: {{ fill: "#f0932b", stroke: "#d0730f", collapsedFill: "#f6c07e" }},
|
|
351
|
+
4: {{ fill: "#be2edd", stroke: "#a01cb3", collapsedFill: "#e08bf2" }},
|
|
352
|
+
5: {{ fill: "#00a8ff", stroke: "#007ac1", collapsedFill: "#74d2ff" }},
|
|
353
|
+
6: {{ fill: "#e55039", stroke: "#c23620", collapsedFill: "#f09a8d" }},
|
|
354
|
+
default: {{ fill: "#747d8c", stroke: "#57606f", collapsedFill: "#a4b0be" }}
|
|
355
|
+
}};
|
|
356
|
+
|
|
357
|
+
const svgElement = d3.select("#tree-svg");
|
|
358
|
+
const initialSvgWidth = +svgElement.attr("width");
|
|
359
|
+
const initialSvgHeight = +svgElement.attr("height");
|
|
360
|
+
const margin = {{ top: 40, right: 120, bottom: 80, left: 450 }};
|
|
361
|
+
let width = initialSvgWidth - margin.left - margin.right;
|
|
362
|
+
let height = initialSvgHeight - margin.top - margin.bottom;
|
|
363
|
+
const duration = 500;
|
|
364
|
+
let nodeCounter = 0;
|
|
365
|
+
const g = svgElement.append("g").attr("transform", `translate(${{margin.left}},${{margin.top}})`);
|
|
366
|
+
const treemap = d3.tree().nodeSize([40, 0]);
|
|
367
|
+
let rootNode = d3.hierarchy(treeData, d => d.children);
|
|
368
|
+
rootNode.x0 = 0;
|
|
369
|
+
rootNode.y0 = 0;
|
|
370
|
+
|
|
371
|
+
if (rootNode.children) {{
|
|
372
|
+
rootNode.children.forEach(d_child => {{
|
|
373
|
+
if (d_child.children) {{ collapseBranch(d_child); }}
|
|
374
|
+
}});
|
|
375
|
+
}}
|
|
376
|
+
updateTree(rootNode);
|
|
377
|
+
|
|
378
|
+
function collapseBranch(d) {{ if (d.children) {{ d._children = d.children; d._children.forEach(collapseBranch); d.children = null; }} }}
|
|
379
|
+
function expandBranch(d) {{ if (d._children) {{ d.children = d._children; d._children = null; }} if (d.children) {{ d.children.forEach(expandBranch); }} }}
|
|
380
|
+
window.expandAll = () => {{ expandBranch(rootNode); updateTree(rootNode); }};
|
|
381
|
+
window.collapseAll = () => {{ if (rootNode.children) {{ rootNode.children.forEach(collapseBranch); }} updateTree(rootNode); }};
|
|
382
|
+
window.resetView = () => {{ if (rootNode.children) {{ rootNode.children.forEach(d_child => {{ if (d_child.children || d_child._children) {{ collapseBranch(d_child); }} }}); }} if (rootNode._children && !rootNode.children) {{ rootNode.children = rootNode._children; rootNode._children = null; }} updateTree(rootNode); }};
|
|
383
|
+
|
|
384
|
+
function updateTree(source) {{
|
|
385
|
+
const treeLayoutData = treemap(rootNode);
|
|
386
|
+
let nodes = treeLayoutData.descendants();
|
|
387
|
+
let links = treeLayoutData.descendants().slice(1);
|
|
388
|
+
|
|
389
|
+
let minX = 0;
|
|
390
|
+
let maxX = 0;
|
|
391
|
+
if (nodes.length > 0) {{
|
|
392
|
+
minX = d3.min(nodes, d => d.x);
|
|
393
|
+
maxX = d3.max(nodes, d => d.x);
|
|
394
|
+
}}
|
|
395
|
+
|
|
396
|
+
let neededHeight = Math.max(initialSvgHeight, maxX - minX + margin.top + margin.bottom + 100);
|
|
397
|
+
svgElement.transition().duration(duration / 2).attr("height", neededHeight);
|
|
398
|
+
g.transition().duration(duration / 2).attr("transform", `translate(${{margin.left}},${{margin.top - minX + 40}})`);
|
|
399
|
+
|
|
400
|
+
nodes.forEach(d => {{ d.y = d.depth * 400; }}); // Adjust horizontal separation if needed
|
|
401
|
+
|
|
402
|
+
const node = g.selectAll('g.node').data(nodes, d => d.id || (d.id = ++nodeCounter));
|
|
403
|
+
const nodeEnter = node.enter().append('g')
|
|
404
|
+
.attr('class', d => "node" + (d.children || d._children ? " node--internal" : " node--leaf") + (d._children ? " _children" : ""))
|
|
405
|
+
.attr('transform', d => `translate(${{source.y0}},${{source.x0}})`)
|
|
406
|
+
.on('click', (event, d) => {{ if (d.children) {{ d._children = d.children; d.children = null; }} else if (d._children) {{ d.children = d._children; d._children = null; }} updateTree(d); }})
|
|
407
|
+
.style('cursor', d => (d.children || d._children) ? 'pointer' : 'default');
|
|
408
|
+
|
|
409
|
+
nodeEnter.append('circle').attr('r', 1e-6);
|
|
410
|
+
|
|
411
|
+
nodeEnter.append('text')
|
|
412
|
+
.attr('dy', '.35em')
|
|
413
|
+
.attr('x', d => d.children || d._children ? -14 : 14)
|
|
414
|
+
.attr('text-anchor', d => d.children || d._children ? 'end' : 'start')
|
|
415
|
+
.style("fill-opacity", 1e-6)
|
|
416
|
+
.call(wrapText, 380);
|
|
417
|
+
|
|
418
|
+
const nodeUpdate = nodeEnter.merge(node);
|
|
419
|
+
nodeUpdate.transition().duration(duration)
|
|
420
|
+
.attr('transform', d => `translate(${{d.y}},${{d.x}})`)
|
|
421
|
+
.attr('class', d => "node" + (d.children ? " node--internal" : " node--leaf") + (d._children ? " node--internal _children" : ""));
|
|
422
|
+
|
|
423
|
+
nodeUpdate.select('circle').attr('r', 8.5)
|
|
424
|
+
.style('fill', d => {{
|
|
425
|
+
let palette;
|
|
426
|
+
if (d.depth === 0) {{
|
|
427
|
+
palette = levelSpecificPalettes[0];
|
|
428
|
+
}} else if (d.depth === 1) {{
|
|
429
|
+
palette = phaseColors[d.data.originalStageName] || phaseColors.Default;
|
|
430
|
+
}} else {{
|
|
431
|
+
palette = levelSpecificPalettes[d.depth] || levelSpecificPalettes.default;
|
|
432
|
+
}}
|
|
433
|
+
if (d._children) return palette.collapsedFill;
|
|
434
|
+
if (d.children) return palette.fill;
|
|
435
|
+
return "#fff";
|
|
436
|
+
}})
|
|
437
|
+
.style('stroke', d => {{
|
|
438
|
+
let palette;
|
|
439
|
+
if (d.depth === 0) {{
|
|
440
|
+
palette = levelSpecificPalettes[0];
|
|
441
|
+
}} else if (d.depth === 1) {{
|
|
442
|
+
palette = phaseColors[d.data.originalStageName] || phaseColors.Default;
|
|
443
|
+
}} else {{
|
|
444
|
+
palette = levelSpecificPalettes[d.depth] || levelSpecificPalettes.default;
|
|
445
|
+
}}
|
|
446
|
+
return palette.stroke;
|
|
447
|
+
}});
|
|
448
|
+
nodeUpdate.select('text').style("fill-opacity", 1).call(wrapText, 380);
|
|
449
|
+
|
|
450
|
+
const nodeExit = node.exit().transition().duration(duration).attr('transform', d => `translate(${{source.y}},${{source.x}})`).remove();
|
|
451
|
+
nodeExit.select('circle').attr('r', 1e-6);
|
|
452
|
+
nodeExit.select('text').style('fill-opacity', 1e-6);
|
|
453
|
+
|
|
454
|
+
const link = g.selectAll('path.link').data(links, d => d.id);
|
|
455
|
+
const linkEnter = link.enter().insert('path', "g").attr('class', 'link').attr('d', d => {{ const o = {{ x: source.x0, y: source.y0 }}; return diagonal(o, o); }});
|
|
456
|
+
|
|
457
|
+
linkEnter.merge(link).transition().duration(duration).attr('d', d => diagonal(d, d.parent))
|
|
458
|
+
.style('stroke', d => {{
|
|
459
|
+
const sourceNode = d.parent;
|
|
460
|
+
if (!sourceNode) return phaseColors.Default.stroke;
|
|
461
|
+
const l1AncestorName = sourceNode.data.originalStageName;
|
|
462
|
+
const colorPalette = phaseColors[l1AncestorName] || phaseColors.Default;
|
|
463
|
+
return colorPalette.stroke;
|
|
464
|
+
}});
|
|
465
|
+
link.exit().transition().duration(duration).attr('d', d => {{ const o = {{ x: source.x, y: source.y }}; return diagonal(o, o); }}).remove();
|
|
466
|
+
nodes.forEach(d => {{ d.x0 = d.x; d.y0 = d.y; }});
|
|
467
|
+
}}
|
|
468
|
+
|
|
469
|
+
function diagonal(s, d) {{ return `M ${{s.y}} ${{s.x}} C ${{(s.y + d.y) / 2}} ${{s.x}}, ${{(s.y + d.y) / 2}} ${{d.x}}, ${{d.y}} ${{d.x}}`; }}
|
|
470
|
+
|
|
471
|
+
function wrapText(textElements, maxWidth) {{
|
|
472
|
+
const textPartColors = {{
|
|
473
|
+
name: '#343a40',
|
|
474
|
+
count: '#0056b3'
|
|
475
|
+
}};
|
|
476
|
+
const countRegex = /(\s\(Total Count: \d+\))$/;
|
|
477
|
+
|
|
478
|
+
textElements.each(function () {{
|
|
479
|
+
const textD3 = d3.select(this);
|
|
480
|
+
const originalNodeText = textD3.datum().data.name;
|
|
481
|
+
const x = parseFloat(textD3.attr("x") || 0);
|
|
482
|
+
const initialDy = textD3.attr("dy");
|
|
483
|
+
const textAnchor = textD3.attr("text-anchor");
|
|
484
|
+
const lineHeight = 1.1;
|
|
485
|
+
|
|
486
|
+
textD3.text(null);
|
|
487
|
+
|
|
488
|
+
let namePart = originalNodeText;
|
|
489
|
+
let countPartText = "";
|
|
490
|
+
|
|
491
|
+
const countMatch = originalNodeText.match(countRegex);
|
|
492
|
+
if (countMatch && originalNodeText.endsWith(countMatch[0])) {{
|
|
493
|
+
namePart = originalNodeText.substring(0, originalNodeText.length - countMatch[0].length).trim();
|
|
494
|
+
countPartText = countMatch[0].trim();
|
|
495
|
+
}}
|
|
496
|
+
|
|
497
|
+
const tokens = [];
|
|
498
|
+
namePart.split(/\s+/).filter(Boolean).forEach(word => {{
|
|
499
|
+
tokens.push({{ text: word, type: 'name' }});
|
|
500
|
+
}});
|
|
501
|
+
if (countPartText) {{
|
|
502
|
+
tokens.push({{ text: countPartText, type: 'count' }});
|
|
503
|
+
}}
|
|
504
|
+
|
|
505
|
+
if (tokens.length === 0 && originalNodeText) {{
|
|
506
|
+
tokens.push({{ text: originalNodeText, type: 'name' }});
|
|
507
|
+
}}
|
|
508
|
+
|
|
509
|
+
let currentTspan = textD3.append("tspan").attr("x", x).attr("dy", initialDy);
|
|
510
|
+
if (textAnchor === "end") currentTspan.attr("text-anchor", "end");
|
|
511
|
+
|
|
512
|
+
let lineTokens = [];
|
|
513
|
+
|
|
514
|
+
for (let i = 0; i < tokens.length; i++) {{
|
|
515
|
+
const tokenObj = tokens[i];
|
|
516
|
+
|
|
517
|
+
lineTokens.push(tokenObj);
|
|
518
|
+
currentTspan.text(lineTokens.map(t => t.text).join(" "));
|
|
519
|
+
|
|
520
|
+
if (currentTspan.node().getComputedTextLength() > maxWidth && lineTokens.length > 1) {{
|
|
521
|
+
lineTokens.pop();
|
|
522
|
+
|
|
523
|
+
currentTspan.text(null);
|
|
524
|
+
lineTokens.forEach((prevToken, idx) => {{
|
|
525
|
+
currentTspan.append("tspan")
|
|
526
|
+
.text((idx > 0 ? " " : "") + prevToken.text)
|
|
527
|
+
.style("fill", textPartColors[prevToken.type] || textPartColors.name)
|
|
528
|
+
.style("font-weight", prevToken.type === 'count' ? "bold" : "normal");
|
|
529
|
+
}});
|
|
530
|
+
|
|
531
|
+
lineTokens = [tokenObj];
|
|
532
|
+
currentTspan = textD3.append("tspan").attr("x", x).attr("dy", lineHeight + "em");
|
|
533
|
+
if (textAnchor === "end") currentTspan.attr("text-anchor", "end");
|
|
534
|
+
}}
|
|
535
|
+
}}
|
|
536
|
+
|
|
537
|
+
currentTspan.text(null);
|
|
538
|
+
lineTokens.forEach((token, idx) => {{
|
|
539
|
+
currentTspan.append("tspan")
|
|
540
|
+
.text((idx > 0 ? " " : "") + token.text)
|
|
541
|
+
.style("fill", textPartColors[token.type] || textPartColors.name)
|
|
542
|
+
.style("font-weight", token.type === 'count' ? "bold" : "normal");
|
|
543
|
+
}});
|
|
544
|
+
|
|
545
|
+
if (textD3.selectAll("tspan > tspan").empty() && textD3.select("tspan").text().length === 0 && originalNodeText) {{
|
|
546
|
+
let t = textD3.select("tspan");
|
|
547
|
+
let displayText = originalNodeText;
|
|
548
|
+
t.text(displayText).style("fill", textPartColors.name);
|
|
549
|
+
if (t.node() && t.node().getComputedTextLength() > maxWidth && displayText.length > 20) {{
|
|
550
|
+
let estimatedChars = Math.floor(maxWidth / (t.node().getComputedTextLength()/displayText.length) );
|
|
551
|
+
displayText = displayText.substring(0, Math.max(0, estimatedChars - 3)) + "...";
|
|
552
|
+
t.text(displayText);
|
|
553
|
+
}}
|
|
554
|
+
}}
|
|
555
|
+
}});
|
|
556
|
+
}}
|
|
557
|
+
</script>
|
|
558
|
+
</body>
|
|
559
|
+
</html>
|
|
560
|
+
"""
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def emit_html(
|
|
564
|
+
tree: Dict[str, Any],
|
|
565
|
+
*,
|
|
566
|
+
title: str,
|
|
567
|
+
header: str,
|
|
568
|
+
svg_width: int = 6000,
|
|
569
|
+
svg_height: int = 8000,
|
|
570
|
+
) -> str:
|
|
571
|
+
# Escape </script> sequences so embedded JSON cannot break out of the
|
|
572
|
+
# <script> tag, and HTML-escape values that land in <title>/<h1>.
|
|
573
|
+
data_json = json.dumps(tree, ensure_ascii=True, separators=(",", ":")).replace("</", "<\\/")
|
|
574
|
+
return _HTML_TEMPLATE.format(
|
|
575
|
+
title=_html.escape(title),
|
|
576
|
+
header=_html.escape(header),
|
|
577
|
+
svg_width=svg_width,
|
|
578
|
+
svg_height=svg_height,
|
|
579
|
+
data_json=data_json,
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def write_tree_html(
|
|
584
|
+
graph_path: Path,
|
|
585
|
+
output_path: Path,
|
|
586
|
+
*,
|
|
587
|
+
root: Optional[str] = None,
|
|
588
|
+
max_children: int = DEFAULT_MAX_CHILDREN,
|
|
589
|
+
project_label: Optional[str] = None,
|
|
590
|
+
# kept for CLI compatibility with the older signature; ignored now
|
|
591
|
+
top_k_edges: int = 0,
|
|
592
|
+
) -> Path:
|
|
593
|
+
from graphify.security import check_graph_file_size_cap
|
|
594
|
+
check_graph_file_size_cap(graph_path)
|
|
595
|
+
graph = json.loads(graph_path.read_text(encoding="utf-8"))
|
|
596
|
+
tree = build_tree(graph, root=root, max_children=max_children,
|
|
597
|
+
project_label=project_label)
|
|
598
|
+
title = f"{tree['name']} — graphify tree viewer"
|
|
599
|
+
header = f"{tree['name']} — Knowledge Graph"
|
|
600
|
+
html = emit_html(tree, title=title, header=header)
|
|
601
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
602
|
+
output_path.write_text(html, encoding="utf-8")
|
|
603
|
+
return output_path
|