cg-code-graph 0.10.1__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.
- cg_code_graph-0.10.1.dist-info/METADATA +678 -0
- cg_code_graph-0.10.1.dist-info/RECORD +174 -0
- cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
- cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
- cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
- cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
- codegraph/__init__.py +2 -0
- codegraph/aitools.py +129 -0
- codegraph/apps.py +76 -0
- codegraph/blindspots.py +428 -0
- codegraph/bridges.py +1701 -0
- codegraph/cli.py +725 -0
- codegraph/concepts.py +362 -0
- codegraph/config.py +559 -0
- codegraph/core/__init__.py +0 -0
- codegraph/core/cache.py +375 -0
- codegraph/core/detect.py +80 -0
- codegraph/core/extractors.py +187 -0
- codegraph/core/fsutil.py +61 -0
- codegraph/core/generated.py +575 -0
- codegraph/core/model.py +174 -0
- codegraph/core/paths.py +175 -0
- codegraph/core/plugin.py +160 -0
- codegraph/core/store.py +80 -0
- codegraph/core/syntax_errors.py +132 -0
- codegraph/coverage.py +928 -0
- codegraph/doctor.py +453 -0
- codegraph/external.py +613 -0
- codegraph/indexer.py +336 -0
- codegraph/link.py +434 -0
- codegraph/lint_async.py +524 -0
- codegraph/mcp_server.py +1303 -0
- codegraph/parity.py +473 -0
- codegraph/parity_structure.py +307 -0
- codegraph/payload.py +321 -0
- codegraph/plans.py +1285 -0
- codegraph/platform_scan.py +643 -0
- codegraph/platforms.py +1369 -0
- codegraph/plugins/__init__.py +0 -0
- codegraph/plugins/cfamily/__init__.py +0 -0
- codegraph/plugins/cfamily/plugin.py +930 -0
- codegraph/plugins/cfamily/syntax.py +881 -0
- codegraph/plugins/dart/__init__.py +0 -0
- codegraph/plugins/dart/bridges.py +345 -0
- codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
- codegraph/plugins/dart/extractor/pubspec.lock +149 -0
- codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
- codegraph/plugins/dart/http.py +904 -0
- codegraph/plugins/dart/models.py +308 -0
- codegraph/plugins/dart/plugin.py +625 -0
- codegraph/plugins/dart/program.py +907 -0
- codegraph/plugins/django/__init__.py +0 -0
- codegraph/plugins/django/extras.py +378 -0
- codegraph/plugins/django/models.py +508 -0
- codegraph/plugins/django/plugin.py +728 -0
- codegraph/plugins/django/schemas.py +339 -0
- codegraph/plugins/django/shapes.py +216 -0
- codegraph/plugins/django/urls.py +603 -0
- codegraph/plugins/express/__init__.py +0 -0
- codegraph/plugins/express/plugin.py +428 -0
- codegraph/plugins/flutter/__init__.py +0 -0
- codegraph/plugins/flutter/plugin.py +538 -0
- codegraph/plugins/kotlin/__init__.py +0 -0
- codegraph/plugins/kotlin/exact.py +457 -0
- codegraph/plugins/kotlin/plugin.py +1961 -0
- codegraph/plugins/kotlin/reparse.py +234 -0
- codegraph/plugins/laravel/__init__.py +0 -0
- codegraph/plugins/laravel/broadcast.py +351 -0
- codegraph/plugins/laravel/plugin.py +863 -0
- codegraph/plugins/laravel/tests.py +262 -0
- codegraph/plugins/laravel/values.py +728 -0
- codegraph/plugins/native/__init__.py +0 -0
- codegraph/plugins/native/gates.py +286 -0
- codegraph/plugins/native/runner.py +183 -0
- codegraph/plugins/native/scipread.py +194 -0
- codegraph/plugins/native/ts.py +54 -0
- codegraph/plugins/nest/__init__.py +0 -0
- codegraph/plugins/nest/plugin.py +654 -0
- codegraph/plugins/nextjs/__init__.py +0 -0
- codegraph/plugins/nextjs/plugin.py +336 -0
- codegraph/plugins/nuxt/__init__.py +0 -0
- codegraph/plugins/nuxt/plugin.py +308 -0
- codegraph/plugins/php/__init__.py +0 -0
- codegraph/plugins/php/extractor/composer.json +5 -0
- codegraph/plugins/php/extractor/composer.lock +76 -0
- codegraph/plugins/php/extractor/extract.php +743 -0
- codegraph/plugins/php/gating.py +573 -0
- codegraph/plugins/php/plugin.py +668 -0
- codegraph/plugins/php/strings.py +197 -0
- codegraph/plugins/python/__init__.py +0 -0
- codegraph/plugins/python/aitools.py +664 -0
- codegraph/plugins/python/external.py +245 -0
- codegraph/plugins/python/fields.py +107 -0
- codegraph/plugins/python/plugin.py +1733 -0
- codegraph/plugins/python/refs.py +485 -0
- codegraph/plugins/python/roots.py +412 -0
- codegraph/plugins/python/socketio.py +210 -0
- codegraph/plugins/python/subproc.py +864 -0
- codegraph/plugins/python/tests.py +1040 -0
- codegraph/plugins/python/values.py +179 -0
- codegraph/plugins/pyweb/__init__.py +0 -0
- codegraph/plugins/pyweb/plugin.py +1334 -0
- codegraph/plugins/pyweb/values.py +68 -0
- codegraph/plugins/rust/__init__.py +0 -0
- codegraph/plugins/rust/cargo.py +226 -0
- codegraph/plugins/rust/plugin.py +980 -0
- codegraph/plugins/rust/syntax.py +678 -0
- codegraph/plugins/scip/__init__.py +0 -0
- codegraph/plugins/scip/importer.py +129 -0
- codegraph/plugins/scip/scip.proto +962 -0
- codegraph/plugins/scip/scip_pb2.py +97 -0
- codegraph/plugins/stubs/__init__.py +0 -0
- codegraph/plugins/stubs/plugins.py +38 -0
- codegraph/plugins/swift/__init__.py +0 -0
- codegraph/plugins/swift/baseurl.py +109 -0
- codegraph/plugins/swift/exact.py +415 -0
- codegraph/plugins/swift/indexstore.py +209 -0
- codegraph/plugins/swift/packages.py +174 -0
- codegraph/plugins/swift/plugin.py +2890 -0
- codegraph/plugins/ts/__init__.py +0 -0
- codegraph/plugins/ts/baseurl.py +185 -0
- codegraph/plugins/ts/extractor/extract.mjs +2652 -0
- codegraph/plugins/ts/extractor/fw.mjs +685 -0
- codegraph/plugins/ts/extractor/package-lock.json +205 -0
- codegraph/plugins/ts/extractor/package.json +9 -0
- codegraph/plugins/ts/plugin.py +480 -0
- codegraph/plugins/tsweb/__init__.py +0 -0
- codegraph/plugins/tsweb/common.py +290 -0
- codegraph/plugins/tsweb/data.py +276 -0
- codegraph/presets/__init__.py +146 -0
- codegraph/presets/c_cpp.yaml +9 -0
- codegraph/presets/common.yaml +66 -0
- codegraph/presets/dart.yaml +9 -0
- codegraph/presets/django-ninja.yaml +15 -0
- codegraph/presets/django.yaml +25 -0
- codegraph/presets/djangorestframework.yaml +17 -0
- codegraph/presets/express.yaml +17 -0
- codegraph/presets/kotlin.yaml +11 -0
- codegraph/presets/laravel.yaml +40 -0
- codegraph/presets/nest.yaml +11 -0
- codegraph/presets/nextjs.yaml +15 -0
- codegraph/presets/nuxt.yaml +9 -0
- codegraph/presets/php.yaml +5 -0
- codegraph/presets/python.yaml +10 -0
- codegraph/presets/rust.yaml +5 -0
- codegraph/presets/swift.yaml +10 -0
- codegraph/presets/typescript.yaml +13 -0
- codegraph/process_runs.py +328 -0
- codegraph/protocols/__init__.py +299 -0
- codegraph/protocols/builtin.py +67 -0
- codegraph/protocols/matchers.py +144 -0
- codegraph/protocols/view.py +334 -0
- codegraph/query.py +2089 -0
- codegraph/realtime.py +260 -0
- codegraph/roundtrip.py +346 -0
- codegraph/routes.py +442 -0
- codegraph/starters.py +218 -0
- codegraph/tests_index.py +117 -0
- codegraph/viz/__init__.py +0 -0
- codegraph/viz/graph.py +369 -0
- codegraph/viz/server.py +198 -0
- codegraph/viz/static/app.css +148 -0
- codegraph/viz/static/app.js +1082 -0
- codegraph/viz/static/index.html +81 -0
- codegraph/viz/static/layered.js +237 -0
- codegraph/viz/static/vendor/VERSIONS.txt +4 -0
- codegraph/viz/static/vendor/cose-base.js +3214 -0
- codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
- codegraph/viz/static/vendor/cytoscape.min.js +31 -0
- codegraph/viz/static/vendor/layout-base.js +5230 -0
- codegraph/viz/tools/package-lock.json +303 -0
- codegraph/viz/tools/package.json +7 -0
- codegraph/viz/tools/shoot.mjs +165 -0
- codegraph/xcode.py +251 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>code-graph view</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 8L12 3.5M4 8L12 12.5' stroke='%231d2330' stroke-width='1.4'/%3E%3Ccircle cx='4' cy='8' r='2.8' fill='%230072B2'/%3E%3Ccircle cx='12' cy='3.5' r='2.4' fill='%23009E73'/%3E%3Ccircle cx='12' cy='12.5' r='2.4' fill='%23D55E00'/%3E%3C/svg%3E">
|
|
8
|
+
<link rel="stylesheet" href="app.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<header>
|
|
12
|
+
<a class="brand" id="home" href="#" title="start page: overview, search and starter queries">code-graph</a>
|
|
13
|
+
<div class="grp query">
|
|
14
|
+
<select id="mode" title="query" aria-label="query mode">
|
|
15
|
+
<option value="impact" title="callers of the spec, up to entry points">impact</option>
|
|
16
|
+
<option value="reaches" title="what depends on the spec (tables, connections, keys), with entry kinds and gates">reaches</option>
|
|
17
|
+
<option value="downstream" title="what the spec depends on, down to sinks">downstream</option>
|
|
18
|
+
<option value="path" title="one evidence path: source, [via…,] target">path</option>
|
|
19
|
+
<option value="plan" title="planned-change overlay (plans/<name>.yaml)">plan</option>
|
|
20
|
+
</select>
|
|
21
|
+
<div class="specbox"><input id="spec" aria-label="query spec" list="sugg" placeholder="Search a symbol, table, route…" title="spec(s), comma separated: Class::method, table.column, page:/route, connection:x, request_key:timezone; path: source, [via…,] target" autocomplete="off"><datalist id="sugg"></datalist></div>
|
|
22
|
+
<button id="go">Show</button>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="grp filters">
|
|
25
|
+
<select id="minconf" title="minimum edge confidence" aria-label="minimum edge confidence">
|
|
26
|
+
<option value="heuristic">≥ heuristic</option><option value="resolved">≥ resolved</option><option value="exact">exact only</option>
|
|
27
|
+
</select>
|
|
28
|
+
<select id="sinks" title="downstream sinks" aria-label="downstream sinks">
|
|
29
|
+
<option value="table,column">sinks: tables (via columns)</option>
|
|
30
|
+
<option value="table">sinks: tables (direct only)</option>
|
|
31
|
+
<option value="table,column,route">sinks: tables + routes</option>
|
|
32
|
+
<option value="route,http">sinks: routes + http</option>
|
|
33
|
+
<option value="">sinks: all</option>
|
|
34
|
+
</select>
|
|
35
|
+
</div>
|
|
36
|
+
<button id="more" class="more" title="view options" aria-haspopup="true" aria-expanded="false" aria-controls="viewgrp">⋯</button>
|
|
37
|
+
<div class="grp view" id="viewgrp">
|
|
38
|
+
<select id="layout" title="layout" aria-label="layout">
|
|
39
|
+
<option value="auto">layout: auto</option><option value="layered">layout: layered</option><option value="fcose">layout: clustered</option>
|
|
40
|
+
</select>
|
|
41
|
+
<button id="collapse" title="fold every module / caller cluster">collapse all</button>
|
|
42
|
+
<button id="expand" title="open every module / caller cluster">expand all</button>
|
|
43
|
+
<button id="fit" title="fit the whole view">fit</button>
|
|
44
|
+
<button id="copylink" title="copy a link to this view (query, layout, open clusters, selection)">copy link</button>
|
|
45
|
+
<button id="exportpng" title="download the whole drawing as a 2x PNG">PNG</button>
|
|
46
|
+
<button id="exportjson" title="download the visible subgraph as JSON">JSON</button>
|
|
47
|
+
<button id="listbtn" aria-pressed="false" title="list view of the same subgraph (L)">list</button>
|
|
48
|
+
<button id="theme" title="colour theme: auto (follows the system), light, dark">theme: auto</button>
|
|
49
|
+
</div>
|
|
50
|
+
</header>
|
|
51
|
+
<main>
|
|
52
|
+
<div id="status" aria-live="polite"></div>
|
|
53
|
+
<div id="cy" role="img" aria-label="graph view (empty until a query runs)"></div>
|
|
54
|
+
<div id="landing" hidden></div>
|
|
55
|
+
<button id="fitall" hidden></button>
|
|
56
|
+
<details id="legendbox"><summary>legend</summary><div id="legend"></div></details>
|
|
57
|
+
<div id="tip" hidden></div>
|
|
58
|
+
<div id="listview" role="tree" aria-label="list view of the graph" hidden></div>
|
|
59
|
+
<div id="truncbanner" role="status" hidden>The result was truncated: narrow the query (a more specific symbol, min confidence, sinks).</div>
|
|
60
|
+
<div id="help" role="dialog" aria-label="keyboard shortcuts" hidden><button id="helpclose" aria-label="close">×</button><b>Keyboard</b>
|
|
61
|
+
<table><tr><td><kbd>/</kbd></td><td>search</td></tr><tr><td><kbd>Enter</kbd></td><td>run the search; on a cluster: open it; on a node: details</td></tr>
|
|
62
|
+
<tr><td><kbd>Esc</kbd></td><td>fold the last cluster / clear the highlight / close</td></tr><tr><td><kbd>Backspace</kbd></td><td>fold the last opened cluster</td></tr>
|
|
63
|
+
<tr><td><kbd>←</kbd> <kbd>→</kbd></td><td>move to a caller / callee</td></tr><tr><td><kbd>↑</kbd> <kbd>↓</kbd></td><td>move within the column</td></tr>
|
|
64
|
+
<tr><td><kbd>Space</kbd></td><td>open the selected cluster</td></tr>
|
|
65
|
+
<tr><td><kbd>f</kbd> <kbd>+</kbd> <kbd>-</kbd> <kbd>0</kbd></td><td>fit, zoom in, zoom out, reset</td></tr><tr><td><kbd>l</kbd></td><td>list view</td></tr><tr><td><kbd>?</kbd></td><td>this help</td></tr>
|
|
66
|
+
<tr><td>Back / Forward</td><td>previous / next query</td></tr></table></div>
|
|
67
|
+
<aside id="panel" aria-label="details"><div id="phandle" role="separator" aria-orientation="vertical" aria-label="resize the panel" aria-valuemin="260" aria-valuemax="960" aria-valuenow="360" tabindex="0" title="drag to resize (← → keys too)"></div>
|
|
68
|
+
<button id="pcollapse" type="button" aria-expanded="true" title="collapse / expand the panel">⟩</button>
|
|
69
|
+
<div id="panelbody"><div class="hint">Search a node and pick a query, or start from a starter query.<br><br>
|
|
70
|
+
Caller clusters and modules fold their members: click a cluster to open it, click again for the next 20, Esc folds.
|
|
71
|
+
Click a node for its docblock, file:line evidence and source. Press <kbd>?</kbd> for the keyboard shortcuts.</div></div></aside>
|
|
72
|
+
<div id="ctxmenu" role="menu" hidden></div>
|
|
73
|
+
</main>
|
|
74
|
+
<script src="vendor/cytoscape.min.js"></script>
|
|
75
|
+
<script src="vendor/layout-base.js"></script>
|
|
76
|
+
<script src="vendor/cose-base.js"></script>
|
|
77
|
+
<script src="vendor/cytoscape-fcose.js"></script>
|
|
78
|
+
<script src="layered.js"></script>
|
|
79
|
+
<script src="app.js"></script>
|
|
80
|
+
</body>
|
|
81
|
+
</html>
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/* Layered left-to-right layout and per-layer caller clusters for the code-graph view (#82).
|
|
2
|
+
Pure functions over the /api/graph payload (no DOM, no Cytoscape): loaded by the page as window.CGLayered and by
|
|
3
|
+
the unit tests under node (module.exports).
|
|
4
|
+
|
|
5
|
+
layers(data) node id -> layer (0 = leftmost). impact: entry side left, target right (from `depth`);
|
|
6
|
+
downstream / path: longest path from the sources over the evidence edges.
|
|
7
|
+
build(data, opts) visible units (nodes and clusters), node -> unit map, positions.
|
|
8
|
+
opts.open Map cluster id -> members shown (an open cluster keeps its slot; members go into the lane
|
|
9
|
+
to its right, so nothing outside it moves)
|
|
10
|
+
opts.flat no clusters (expand all)
|
|
11
|
+
opts.threshold a layer with more nodes than this is folded into clusters (default 12)
|
|
12
|
+
opts.budget at most this many units in a folded layer (default 7)
|
|
13
|
+
opts.maxItems at most this many top-level units over all layers (default 30): the largest layers get
|
|
14
|
+
smaller budgets (at least MIN_BUDGET each) until the total fits
|
|
15
|
+
opts.heightOf unit -> row height (node + its wrapped label); rows are at least ROW / ROW_CLUSTER */
|
|
16
|
+
(function (root) {
|
|
17
|
+
'use strict'
|
|
18
|
+
const GENERIC = new Set(['packages', 'package', 'sources', 'source', 'src', 'lib', 'libs', 'app', 'apps', 'main',
|
|
19
|
+
'java', 'kotlin', 'swift', 'internal', 'pkg', 'modules', 'code'])
|
|
20
|
+
// geometry (model px): labels are at most 110 (leaves) / 130 (clusters) wide, so a member lane at LANE_DX fits
|
|
21
|
+
// between two layers without touching their labels
|
|
22
|
+
const GAP_X = 260
|
|
23
|
+
const MIN_GAP_X = 160 // narrowest layer gap (leaf labels wrap at 110 px), used when nothing is clustered
|
|
24
|
+
const MIN_GAP_CLUSTER = 170 // narrowest gap next to a closed cluster (its label wraps at 130 px)
|
|
25
|
+
const LANE_DX = 135
|
|
26
|
+
const ROW = 64 // leaf row pitch (node + up to three label lines)
|
|
27
|
+
const ROW_CLUSTER = 90 // cluster box + up to three label lines
|
|
28
|
+
const LANE_ROW = 66
|
|
29
|
+
const MAX_ITEMS = 30 // top-level units in one view (#82 test plan: IceCubesApp `impact MastodonClient.get` <= 30)
|
|
30
|
+
const MIN_BUDGET = 3
|
|
31
|
+
|
|
32
|
+
function folderKey (n) {
|
|
33
|
+
const segs = String(n.file || '').split('/').slice(0, -1)
|
|
34
|
+
for (const s of segs) if (s && !GENERIC.has(s.toLowerCase())) return s
|
|
35
|
+
return segs.length ? segs[segs.length - 1] : (n.module || n.kind || '?')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function layers (data) {
|
|
39
|
+
const nodes = data.nodes; const out = new Map()
|
|
40
|
+
if (data.meta && data.meta.mode === 'impact' && nodes.every((n) => typeof n.depth === 'number')) {
|
|
41
|
+
const maxD = Math.max(0, ...nodes.map((n) => n.depth))
|
|
42
|
+
for (const n of nodes) out.set(n.id, maxD - n.depth)
|
|
43
|
+
return out
|
|
44
|
+
}
|
|
45
|
+
// longest path from the sources (Kahn); a node on a cycle takes one past its deepest placed predecessor
|
|
46
|
+
const ids = new Set(nodes.map((n) => n.id)); const preds = new Map(); const succ = new Map(); const indeg = new Map()
|
|
47
|
+
for (const id of ids) { preds.set(id, []); succ.set(id, []); indeg.set(id, 0) }
|
|
48
|
+
const seen = new Set()
|
|
49
|
+
for (const e of data.edges) {
|
|
50
|
+
if (!ids.has(e.src) || !ids.has(e.dst) || e.src === e.dst) continue
|
|
51
|
+
const k = e.src + '\u0000' + e.dst; if (seen.has(k)) continue; seen.add(k)
|
|
52
|
+
succ.get(e.src).push(e.dst); preds.get(e.dst).push(e.src); indeg.set(e.dst, indeg.get(e.dst) + 1)
|
|
53
|
+
}
|
|
54
|
+
const order = [...ids].sort()
|
|
55
|
+
const queue = order.filter((id) => indeg.get(id) === 0)
|
|
56
|
+
const deg = new Map(indeg)
|
|
57
|
+
for (const id of queue) out.set(id, 0)
|
|
58
|
+
for (let i = 0; i < queue.length; i++) {
|
|
59
|
+
const id = queue[i]
|
|
60
|
+
for (const d of succ.get(id)) {
|
|
61
|
+
out.set(d, Math.max(out.get(d) || 0, out.get(id) + 1))
|
|
62
|
+
deg.set(d, deg.get(d) - 1)
|
|
63
|
+
if (deg.get(d) === 0) queue.push(d)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const id of order) {
|
|
67
|
+
if (out.has(id) && deg.get(id) <= 0) continue
|
|
68
|
+
const placed = preds.get(id).filter((p) => out.has(p)).map((p) => out.get(p))
|
|
69
|
+
out.set(id, placed.length ? Math.max(...placed) + 1 : (out.get(id) || 0))
|
|
70
|
+
}
|
|
71
|
+
return out
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function clusterLayer (layer, ns, opts) {
|
|
75
|
+
// module buckets (one-node modules pool by top-level package / folder); when that is still more than the budget,
|
|
76
|
+
// the whole layer buckets by package / folder; the smallest buckets then pool into "+N"
|
|
77
|
+
const budget = opts.budget || 7
|
|
78
|
+
const bucketBy = (list, keyOf, how) => {
|
|
79
|
+
const m = new Map()
|
|
80
|
+
for (const n of list) { const k = keyOf(n); if (!m.has(k)) m.set(k, []); m.get(k).push(n) }
|
|
81
|
+
return [...m].map(([k, members]) => ({ key: how[0] + ':' + k, how, label: k, members }))
|
|
82
|
+
}
|
|
83
|
+
let buckets = bucketBy(ns, (n) => n.group || '?', 'module')
|
|
84
|
+
let singles = []
|
|
85
|
+
const pooled = bucketBy(buckets.filter((b) => b.members.length === 1).map((b) => b.members[0]), folderKey, 'folder')
|
|
86
|
+
buckets = buckets.filter((b) => b.members.length > 1).concat(pooled)
|
|
87
|
+
if (buckets.length > budget) buckets = bucketBy(ns, folderKey, 'folder')
|
|
88
|
+
singles = buckets.filter((b) => b.members.length === 1).map((b) => b.members[0])
|
|
89
|
+
buckets = buckets.filter((b) => b.members.length > 1)
|
|
90
|
+
const size = (b) => b.members.length
|
|
91
|
+
buckets.sort((a, b) => size(b) - size(a) || (a.key < b.key ? -1 : 1))
|
|
92
|
+
singles.sort((a, b) => (a.id < b.id ? -1 : 1))
|
|
93
|
+
let rest = []
|
|
94
|
+
while (buckets.length + singles.length + (rest.length ? 1 : 0) > budget && (singles.length || buckets.length > 1)) {
|
|
95
|
+
if (singles.length) rest.push(singles.pop())
|
|
96
|
+
else rest = rest.concat(buckets.pop().members)
|
|
97
|
+
}
|
|
98
|
+
if (rest.length === 1) { singles.push(rest[0]); rest = [] }
|
|
99
|
+
if (rest.length) buckets.push({ key: 'rest', how: 'rest', label: '', members: rest })
|
|
100
|
+
return { buckets: buckets.map((b) => ({ ...b, id: 'C:' + layer + ':' + b.key })), singles }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function build (data, opts) {
|
|
104
|
+
opts = opts || {}
|
|
105
|
+
const open = opts.open || new Map()
|
|
106
|
+
const threshold = opts.threshold || 12
|
|
107
|
+
const lay = layers(data)
|
|
108
|
+
const byLayer = new Map()
|
|
109
|
+
for (const n of [...data.nodes].sort((a, b) => (a.id < b.id ? -1 : 1))) {
|
|
110
|
+
const l = lay.get(n.id) || 0
|
|
111
|
+
if (!byLayer.has(l)) byLayer.set(l, []); byLayer.get(l).push(n)
|
|
112
|
+
}
|
|
113
|
+
const units = []; const rep = new Map()
|
|
114
|
+
const budgets = layerBudgets(byLayer, threshold, opts)
|
|
115
|
+
for (const l of [...byLayer.keys()].sort((a, b) => a - b)) {
|
|
116
|
+
const ns = byLayer.get(l)
|
|
117
|
+
const nt = ns.filter((n) => n.is_target).length
|
|
118
|
+
const folded = !opts.flat && ns.length > budgets.get(l) && ns.length - nt > 1
|
|
119
|
+
const { buckets, singles } = folded ? clusterLayer(l, ns.filter((n) => !n.is_target), { ...opts, budget: Math.max(1, budgets.get(l) - nt) })
|
|
120
|
+
: { buckets: [], singles: ns }
|
|
121
|
+
const tgt = folded ? ns.filter((n) => n.is_target) : []
|
|
122
|
+
for (const n of tgt.concat(singles)) { units.push({ id: n.id, type: 'node', layer: l, node: n }); rep.set(n.id, n.id) }
|
|
123
|
+
for (const b of buckets) {
|
|
124
|
+
const members = b.members.slice().sort((a, c) => (a.id < c.id ? -1 : 1))
|
|
125
|
+
const shown = open.has(b.id) ? Math.min(members.length, open.get(b.id)) : 0
|
|
126
|
+
const u = { id: b.id, type: 'cluster', layer: l, how: b.how, key: b.label, members: members.map((m) => m.id),
|
|
127
|
+
count: members.length, entries: members.filter((m) => m.entry_kind).length, gated: members.filter(gatedNode).length,
|
|
128
|
+
kinds: countBy(members, (m) => m.kind), groups: new Set(members.map((m) => m.group)).size, open: shown > 0, shown }
|
|
129
|
+
units.push(u)
|
|
130
|
+
members.forEach((m, i) => {
|
|
131
|
+
if (i < shown) { units.push({ id: m.id, type: 'node', layer: l, node: m, lane: b.id }); rep.set(m.id, m.id) } else rep.set(m.id, b.id)
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// with no open cluster (no member lanes to make room for) a drawing that is too wide narrows its layer gaps to
|
|
136
|
+
// fit opts.fitWidth, so the first fit shows every column at a legible zoom where it can
|
|
137
|
+
let gap = GAP_X
|
|
138
|
+
const nl = new Set(units.map((u) => u.layer)).size
|
|
139
|
+
if (opts.fitWidth && nl > 1 && !units.some((u) => u.type === 'cluster' && u.open)) {
|
|
140
|
+
const floor = units.some((u) => u.type === 'cluster') ? MIN_GAP_CLUSTER : MIN_GAP_X
|
|
141
|
+
gap = Math.max(floor, Math.min(GAP_X, Math.floor((opts.fitWidth - 140) / (nl - 1))))
|
|
142
|
+
}
|
|
143
|
+
return { units, rep, pos: positions(units, data.edges, rep, opts.heightOf, gap), layers: lay, gap }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// units allowed per layer: a layer up to `threshold` nodes is drawn whole, a larger one folds into `budget` units;
|
|
147
|
+
// while the total is above maxItems, the layer with the most units gives one up (not below MIN_BUDGET, and a
|
|
148
|
+
// layer whose nodes all fit stays whole once nothing else can shrink)
|
|
149
|
+
function layerBudgets (byLayer, threshold, opts) {
|
|
150
|
+
const budget = opts.budget || 7; const max = opts.maxItems || MAX_ITEMS
|
|
151
|
+
const b = new Map()
|
|
152
|
+
for (const [l, ns] of byLayer) b.set(l, ns.length <= threshold ? ns.length : budget)
|
|
153
|
+
if (opts.flat) return new Map([...byLayer].map(([l, ns]) => [l, ns.length]))
|
|
154
|
+
let total = [...b.values()].reduce((a, c) => a + c, 0)
|
|
155
|
+
while (total > max) {
|
|
156
|
+
let best = null
|
|
157
|
+
for (const [l, v] of b) if (v > MIN_BUDGET && (best === null || v > b.get(best) || (v === b.get(best) && l > best))) best = l
|
|
158
|
+
if (best === null) break
|
|
159
|
+
b.set(best, b.get(best) - 1); total--
|
|
160
|
+
}
|
|
161
|
+
return b
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function gatedNode (n) { return (n.gate_status && n.gate_status !== 'live') || n.live === false }
|
|
165
|
+
function countBy (xs, f) { const o = {}; for (const x of xs) { const k = f(x); o[k] = (o[k] || 0) + 1 } return o }
|
|
166
|
+
|
|
167
|
+
function positions (units, edges, rep, heightOf, gap) {
|
|
168
|
+
gap = gap || GAP_X
|
|
169
|
+
const hOf = (u) => Math.max(heightOf ? heightOf(u) : 0, u.lane ? LANE_ROW : (u.type === 'cluster' ? ROW_CLUSTER : ROW))
|
|
170
|
+
// column units (not lane members), ordered by barycenter sweeps over the unit edges
|
|
171
|
+
const col = units.filter((u) => !u.lane)
|
|
172
|
+
const host = new Map(units.map((u) => [u.id, u.lane || u.id]))
|
|
173
|
+
const layerOf = new Map(units.map((u) => [u.id, u.layer]))
|
|
174
|
+
const nb = new Map(col.map((u) => [u.id, { left: [], right: [] }]))
|
|
175
|
+
const seen = new Set()
|
|
176
|
+
for (const e of edges) {
|
|
177
|
+
let s = rep.get(e.src); let d = rep.get(e.dst)
|
|
178
|
+
if (!s || !d || s === d) continue
|
|
179
|
+
s = host.get(s); d = host.get(d)
|
|
180
|
+
if (s === d || !nb.has(s) || !nb.has(d)) continue
|
|
181
|
+
const k = s + '\u0000' + d; if (seen.has(k)) continue; seen.add(k)
|
|
182
|
+
const [a, b] = layerOf.get(s) <= layerOf.get(d) ? [s, d] : [d, s]
|
|
183
|
+
nb.get(a).right.push(b); nb.get(b).left.push(a)
|
|
184
|
+
}
|
|
185
|
+
const byLayer = new Map()
|
|
186
|
+
for (const u of col) { if (!byLayer.has(u.layer)) byLayer.set(u.layer, []); byLayer.get(u.layer).push(u) }
|
|
187
|
+
const ls = [...byLayer.keys()].sort((a, b) => a - b)
|
|
188
|
+
const idx = new Map()
|
|
189
|
+
for (const l of ls) {
|
|
190
|
+
byLayer.get(l).sort((a, b) => weight(b) - weight(a) || (a.id < b.id ? -1 : 1))
|
|
191
|
+
byLayer.get(l).forEach((u, i) => idx.set(u.id, i))
|
|
192
|
+
}
|
|
193
|
+
const bary = (u, side) => {
|
|
194
|
+
const xs = nb.get(u.id)[side].map((v) => idx.get(v)).filter((v) => v !== undefined)
|
|
195
|
+
return xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : idx.get(u.id)
|
|
196
|
+
}
|
|
197
|
+
for (let sweep = 0; sweep < 6; sweep++) {
|
|
198
|
+
const fwd = sweep % 2 === 0
|
|
199
|
+
for (const l of fwd ? ls : ls.slice().reverse()) {
|
|
200
|
+
const arr = byLayer.get(l)
|
|
201
|
+
const key = new Map(arr.map((u) => [u.id, bary(u, fwd ? 'left' : 'right')]))
|
|
202
|
+
arr.sort((a, b) => key.get(a.id) - key.get(b.id) || (a.id < b.id ? -1 : 1))
|
|
203
|
+
arr.forEach((u, i) => idx.set(u.id, i))
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const pos = new Map()
|
|
207
|
+
for (const l of ls) {
|
|
208
|
+
const arr = byLayer.get(l)
|
|
209
|
+
const hs = arr.map(hOf)
|
|
210
|
+
const total = hs.reduce((a, b) => a + b, 0)
|
|
211
|
+
let y = -total / 2
|
|
212
|
+
arr.forEach((u, i) => { pos.set(u.id, { x: l * gap, y: y + anchor(u) }); y += hs[i] })
|
|
213
|
+
}
|
|
214
|
+
// lanes: an open cluster's members stack to the right of it, below any lane already placed at that x
|
|
215
|
+
const laneBottom = new Map()
|
|
216
|
+
for (const u of units) {
|
|
217
|
+
if (u.type !== 'cluster' || !u.open) continue
|
|
218
|
+
const p = pos.get(u.id); const x = p.x + LANE_DX
|
|
219
|
+
let y = Math.max(p.y - anchor(u), (laneBottom.has(x) ? laneBottom.get(x) : -Infinity))
|
|
220
|
+
for (const m of units) {
|
|
221
|
+
if (m.lane !== u.id) continue
|
|
222
|
+
const h = hOf(m)
|
|
223
|
+
pos.set(m.id, { x, y: y + anchor(m) }); y += h; laneBottom.set(x, y)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return pos
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// node centre below its row top (labels hang below the node)
|
|
230
|
+
function anchor (u) { return u.type === 'cluster' ? 26 : 12 }
|
|
231
|
+
|
|
232
|
+
function weight (u) { return u.type === 'cluster' ? u.count : 1 }
|
|
233
|
+
|
|
234
|
+
const api = { folderKey, layers, build, clusterLayer, layerBudgets, GENERIC, GAP_X, MIN_GAP_X, MIN_GAP_CLUSTER, LANE_DX, MAX_ITEMS }
|
|
235
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = api
|
|
236
|
+
else root.CGLayered = api
|
|
237
|
+
})(typeof window !== 'undefined' ? window : this)
|