@psnext/lscg 0.1.3 → 0.1.4
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.
|
@@ -2,8 +2,31 @@ import { builtinModules } from 'node:module';
|
|
|
2
2
|
import { extractGraph, hashParts } from '../graph/extract.js';
|
|
3
3
|
import { parseSource } from '../parser/treeSitter.js';
|
|
4
4
|
import { BUILTIN_GRAPH_PLUGIN } from './plugins.js';
|
|
5
|
+
// This plugin adds package-level relationships on top of the built-in source
|
|
6
|
+
// graph. Built-in extraction already creates file/import/symbol nodes; this
|
|
7
|
+
// plugin creates package nodes and connects them to the existing import nodes.
|
|
8
|
+
//
|
|
9
|
+
// The important identity rule for edges is:
|
|
10
|
+
// - package nodes use this plugin's fact-key namespace (`package:<name>`), and
|
|
11
|
+
// - built-in nodes are referenced by their final graph ID with
|
|
12
|
+
// `targetPlugin: BUILTIN_GRAPH_PLUGIN`.
|
|
13
|
+
//
|
|
14
|
+
// Keep that distinction in mind when repointing an edge to another existing
|
|
15
|
+
// node. Do not use the package node's eventual database ID as its fact key.
|
|
5
16
|
const DEPENDENCY_SECTIONS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
6
17
|
const BUILTINS = new Set(builtinModules.flatMap((name) => [name, name.replace(/^node:/, ''), `node:${name.replace(/^node:/, '')}`]));
|
|
18
|
+
/**
|
|
19
|
+
* Converts an import specifier into its package root.
|
|
20
|
+
*
|
|
21
|
+
* Examples:
|
|
22
|
+
* react -> react
|
|
23
|
+
* lodash/map -> lodash
|
|
24
|
+
* @scope/library/tool -> @scope/library
|
|
25
|
+
* ./local-file -> undefined
|
|
26
|
+
* node:fs -> undefined
|
|
27
|
+
*
|
|
28
|
+
* Returning the root is what lets several imports share one package node.
|
|
29
|
+
*/
|
|
7
30
|
function packageRoot(specifier) {
|
|
8
31
|
if (!specifier || specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('#') || /^[a-z][a-z\d+.-]*:/i.test(specifier))
|
|
9
32
|
return undefined;
|
|
@@ -55,6 +78,9 @@ export const PackagePlugin = {
|
|
|
55
78
|
declarations.set(name, list);
|
|
56
79
|
}
|
|
57
80
|
}
|
|
81
|
+
// Re-run the same extraction used by the built-in scanner. The resulting
|
|
82
|
+
// import IDs are stable graph node IDs, so they can be used directly as
|
|
83
|
+
// targets when targetPlugin is BUILTIN_GRAPH_PLUGIN.
|
|
58
84
|
const imports = [];
|
|
59
85
|
const repositoryId = hashParts(['repository', context.repoPath]);
|
|
60
86
|
for (const file of context.files) {
|
|
@@ -77,6 +103,9 @@ export const PackagePlugin = {
|
|
|
77
103
|
imports.push({ path: file.path, id: node.id, module });
|
|
78
104
|
}
|
|
79
105
|
}
|
|
106
|
+
// A package is represented once even when it is imported by many files or
|
|
107
|
+
// through many subpaths. `implied` records imports that are not declared in
|
|
108
|
+
// any dependency section of package.json.
|
|
80
109
|
const usedPackages = new Map();
|
|
81
110
|
for (const entry of imports) {
|
|
82
111
|
const root = packageRoot(entry.module);
|
|
@@ -87,6 +116,8 @@ export const PackagePlugin = {
|
|
|
87
116
|
}
|
|
88
117
|
const packageNames = new Set(usedPackages.keys());
|
|
89
118
|
const nodes = [...packageNames].sort().map((name) => {
|
|
119
|
+
// This fact key is local to PackagePlugin. The scanner host later
|
|
120
|
+
// materializes it as hashParts(['plugin-node', 'package-plugin', factKey]).
|
|
90
121
|
const packageDeclarations = declarations.get(name) ?? [];
|
|
91
122
|
const implied = !declarations.has(name);
|
|
92
123
|
const versions = new Set(packageDeclarations.map((declaration) => declaration.version));
|
|
@@ -109,6 +140,18 @@ export const PackagePlugin = {
|
|
|
109
140
|
const name = packageRoot(entry.module);
|
|
110
141
|
if (!name || !packageNames.has(name))
|
|
111
142
|
return [];
|
|
143
|
+
// The source is a node emitted by this plugin, so sourceFactKey uses
|
|
144
|
+
// PackagePlugin's namespace and can be resolved as `package:<name>`.
|
|
145
|
+
//
|
|
146
|
+
// The target is an existing node emitted by the built-in graph scanner.
|
|
147
|
+
// `entry.id` is that node's final graph ID, not a plugin fact key. The
|
|
148
|
+
// explicit built-in namespace tells the scanner host to use the ID
|
|
149
|
+
// directly instead of looking for a PackagePlugin node with that key.
|
|
150
|
+
//
|
|
151
|
+
// To repoint this edge to another existing built-in node, replace
|
|
152
|
+
// `targetFactKey` with that node's ID and keep
|
|
153
|
+
// `targetPlugin: BUILTIN_GRAPH_PLUGIN`. For a node from another plugin,
|
|
154
|
+
// use that plugin's name in targetPlugin and its factKey in targetFactKey.
|
|
112
155
|
return [{
|
|
113
156
|
factKey: `provides:${name}:${entry.path}:${entry.id}`,
|
|
114
157
|
sourceFactKey: `package:${name}`,
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
-
<g id="call_icon">
|
|
3
|
-
<circle cx="24" cy="24" r="21" fill="#
|
|
4
|
-
<path d="M37 22.0001L34 25.0001L23 14.0001L26 11.0001C27.5 9.50002 33 7.00005 37 11.0001C41 15.0001 38.5 20.5 37 22.0001Z"
|
|
5
|
-
|
|
6
|
-
<path d="
|
|
7
|
-
<path d="
|
|
8
|
-
|
|
9
|
-
<path d="
|
|
1
|
+
<svg viewBox="0 0 48 48" fill="none" stroke="#3a3a3a" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<g id="call_icon" >
|
|
3
|
+
<circle cx="24" cy="24" r="21" fill="#fb7185" stroke="inherit" stroke-width="2"></circle>
|
|
4
|
+
<path d="M37 22.0001L34 25.0001L23 14.0001L26 11.0001C27.5 9.50002 33 7.00005 37 11.0001C41 15.0001 38.5 20.5 37 22.0001Z"
|
|
5
|
+
fill="#fb7185" stroke="inherit" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
6
|
+
<path d="M42 6L37 11" stroke="inherit" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
7
|
+
<path d="M11 25.9999L14 22.9999L25 33.9999L22 36.9999C20.5 38.5 15 41 11 36.9999C7 32.9999 9.5 27.5 11 25.9999Z"
|
|
8
|
+
fill="#fb7185" stroke="inherit" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
9
|
+
<path d="M23 32L27 28" stroke="inherit" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
10
|
+
<path d="M6 42L11 37" stroke="inherit" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
11
|
+
<path d="M16 25L20 21" stroke="inherit" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
10
12
|
</g>
|
|
11
13
|
</svg>
|
|
@@ -193,20 +193,23 @@ html, body {
|
|
|
193
193
|
font-weight: 200;
|
|
194
194
|
paint-order: stroke;
|
|
195
195
|
stroke: #020617;
|
|
196
|
-
stroke-width:
|
|
196
|
+
stroke-width: 2px;
|
|
197
197
|
stroke-linejoin: round;
|
|
198
198
|
-webkit-user-select: none;
|
|
199
199
|
user-select: none;
|
|
200
200
|
pointer-events: none;
|
|
201
201
|
}
|
|
202
|
-
.node.is-highlighted .node-shape,
|
|
203
202
|
.node.is-selected .node-shape {
|
|
204
|
-
stroke:
|
|
205
|
-
stroke-width:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
203
|
+
stroke: rgb(255, 255, 255);
|
|
204
|
+
stroke-width: 1;
|
|
205
|
+
filter:
|
|
206
|
+
drop-shadow(0px 0px 12px #00a2ff)
|
|
207
|
+
drop-shadow(0px 0px 24px #00a2ff)
|
|
208
|
+
drop-shadow(0px 0px 36px #00a2ff);
|
|
209
|
+
}
|
|
210
|
+
.node.is-highlighted .node.is-selected .node-shape {
|
|
211
|
+
stroke: rgb(193, 193, 193);
|
|
212
|
+
stroke-width: 2;
|
|
210
213
|
}
|
|
211
214
|
.node.is-dimmed {
|
|
212
215
|
opacity: 0.18;
|
|
@@ -38,6 +38,18 @@
|
|
|
38
38
|
</select>
|
|
39
39
|
</label>
|
|
40
40
|
</div>
|
|
41
|
+
<div class="section">
|
|
42
|
+
<label>
|
|
43
|
+
<span class="status">Layout</span>
|
|
44
|
+
<select id="layout-select">
|
|
45
|
+
<option value="force">Force-directed</option>
|
|
46
|
+
<option value="radial">Radial</option>
|
|
47
|
+
<option value="grid">Grid</option>
|
|
48
|
+
<option value="kind">By kind</option>
|
|
49
|
+
<option value="treemap">Treemap</option>
|
|
50
|
+
</select>
|
|
51
|
+
</label>
|
|
52
|
+
</div>
|
|
41
53
|
<div class="section">
|
|
42
54
|
<label>
|
|
43
55
|
<span class="status">View box zoom</span>
|
|
@@ -68,12 +80,14 @@
|
|
|
68
80
|
|
|
69
81
|
const data = {{DATA}};
|
|
70
82
|
let display = data.display ?? 'shape';
|
|
83
|
+
let layoutMode = 'force';
|
|
71
84
|
const app = document.getElementById('app');
|
|
72
85
|
const svg = d3.select('#graph');
|
|
73
86
|
const graphPanel = document.querySelector('.graph-panel');
|
|
74
87
|
const tooltip = document.getElementById('tooltip');
|
|
75
88
|
const searchInput = document.getElementById('search');
|
|
76
89
|
const displaySelect = document.getElementById('display-select');
|
|
90
|
+
const layoutSelect = document.getElementById('layout-select');
|
|
77
91
|
const zoomInput = document.getElementById('zoom');
|
|
78
92
|
const zoomValue = document.getElementById('zoom-value');
|
|
79
93
|
const zoomAllButton = document.getElementById('zoom-all');
|
|
@@ -240,31 +254,52 @@
|
|
|
240
254
|
.force('x', d3.forceX(width / 2).strength(0.05))
|
|
241
255
|
.force('y', d3.forceY(height / 2).strength(0.05));
|
|
242
256
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
.join('line')
|
|
246
|
-
.attr('class', (d) => 'edge edge--' + d.kind)
|
|
247
|
-
.attr('marker-end', 'url(#arrow)');
|
|
248
|
-
|
|
249
|
-
const nodes = nodeLayer.selectAll('g')
|
|
250
|
-
.data(nodesData)
|
|
251
|
-
.join('g')
|
|
252
|
-
.attr('class', (d) => 'node node--' + d.kind)
|
|
253
|
-
.call(drag(simulation));
|
|
257
|
+
let links = linkLayer.selectAll('line');
|
|
258
|
+
let nodes = nodeLayer.selectAll('g');
|
|
254
259
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const group = d3.select(this);
|
|
259
|
-
appendNodeShape(group, d);
|
|
260
|
+
function renderNode(group, node) {
|
|
261
|
+
appendNodeShape(group, node);
|
|
262
|
+
group.append('title').text(node.label);
|
|
260
263
|
group.append('text')
|
|
261
264
|
.attr('class', 'node-label')
|
|
262
265
|
.attr('text-anchor', 'middle')
|
|
263
|
-
.attr('dominant-baseline', display === 'shape' &&
|
|
264
|
-
.attr('y', display === 'shape' &&
|
|
265
|
-
.text(
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
.attr('dominant-baseline', display === 'shape' && node.kind === 'file' ? 'middle' : 'hanging')
|
|
267
|
+
.attr('y', display === 'shape' && node.kind === 'file' ? 0 : (node.height / 2) + 14)
|
|
268
|
+
.text(node.label);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function renderGraph(visibleNodeIds, visibleEdgeIds) {
|
|
272
|
+
links = linkLayer.selectAll('line')
|
|
273
|
+
.data(linksData.filter((edge) => visibleEdgeIds.has(edge.id)), (edge) => edge.id)
|
|
274
|
+
.join(
|
|
275
|
+
(enter) => enter.append('line'),
|
|
276
|
+
(update) => update,
|
|
277
|
+
(exit) => exit.remove()
|
|
278
|
+
)
|
|
279
|
+
.attr('class', (d) => 'edge edge--' + d.kind)
|
|
280
|
+
.attr('marker-end', 'url(#arrow)');
|
|
281
|
+
|
|
282
|
+
nodes = nodeLayer.selectAll('g')
|
|
283
|
+
.data(nodesData.filter((node) => visibleNodeIds.has(node.id)), (node) => node.id)
|
|
284
|
+
.join(
|
|
285
|
+
(enter) => {
|
|
286
|
+
const selection = enter.append('g')
|
|
287
|
+
.attr('class', (d) => 'node node--' + d.kind)
|
|
288
|
+
if (layoutMode === 'force')
|
|
289
|
+
selection.call(drag(simulation));
|
|
290
|
+
|
|
291
|
+
selection.each(function(node) {
|
|
292
|
+
renderNode(d3.select(this), node);
|
|
293
|
+
});
|
|
294
|
+
return selection;
|
|
295
|
+
},
|
|
296
|
+
(update) => update,
|
|
297
|
+
(exit) => exit.remove()
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
nodeLayer.raise();
|
|
301
|
+
bindNodeInteractions();
|
|
302
|
+
}
|
|
268
303
|
|
|
269
304
|
function updateDisplay() {
|
|
270
305
|
nodes.each(function(d) {
|
|
@@ -273,7 +308,8 @@
|
|
|
273
308
|
appendNodeShape(group, d);
|
|
274
309
|
group.select('.node-label')
|
|
275
310
|
.attr('dominant-baseline', display === 'shape' && d.kind === 'file' ? 'middle' : 'hanging')
|
|
276
|
-
.attr('y', display === 'shape' && d.kind === 'file' ? 0 : (d.height / 2) + 14)
|
|
311
|
+
.attr('y', display === 'shape' && d.kind === 'file' ? 0 : (d.height / 2) + 14)
|
|
312
|
+
.raise();
|
|
277
313
|
});
|
|
278
314
|
}
|
|
279
315
|
|
|
@@ -361,6 +397,74 @@
|
|
|
361
397
|
return typeof endpoint === 'string' ? endpoint : endpoint?.id;
|
|
362
398
|
}
|
|
363
399
|
|
|
400
|
+
function applyLayout(visibleNodeIds) {
|
|
401
|
+
if (layoutMode === 'force') {
|
|
402
|
+
for (const node of nodesData) {
|
|
403
|
+
node.fx = null;
|
|
404
|
+
node.fy = null;
|
|
405
|
+
}
|
|
406
|
+
simulation.alpha(0.8).restart();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
simulation.stop();
|
|
411
|
+
const visibleNodes = nodesData.filter((node) => visibleNodeIds.has(node.id));
|
|
412
|
+
const { width: canvasWidth, height: canvasHeight } = canvasSize();
|
|
413
|
+
const centerX = canvasWidth / 2;
|
|
414
|
+
const centerY = canvasHeight / 2;
|
|
415
|
+
const padding = 80;
|
|
416
|
+
const radius = Math.max(40, Math.min(canvasWidth, canvasHeight) / 2 - padding);
|
|
417
|
+
const columns = Math.max(1, Math.ceil(Math.sqrt(visibleNodes.length)));
|
|
418
|
+
const rows = Math.max(1, Math.ceil(visibleNodes.length / columns));
|
|
419
|
+
|
|
420
|
+
const orderedNodes = layoutMode === 'kind'
|
|
421
|
+
? [...visibleNodes].sort((left, right) => left.kind.localeCompare(right.kind) || left.label.localeCompare(right.label))
|
|
422
|
+
: visibleNodes;
|
|
423
|
+
const treemapRoot = layoutMode === 'treemap'
|
|
424
|
+
? d3.treemap().size([canvasWidth - padding * 2, canvasHeight - padding * 2]).paddingInner(12)(
|
|
425
|
+
d3.hierarchy({ children: visibleNodes }).sum((node) => node.degree ? Math.max(1, node.degree + 1) : 1)
|
|
426
|
+
)
|
|
427
|
+
: null;
|
|
428
|
+
|
|
429
|
+
orderedNodes.forEach((node, index) => {
|
|
430
|
+
let x = centerX;
|
|
431
|
+
let y = centerY;
|
|
432
|
+
if (layoutMode === 'treemap') {
|
|
433
|
+
const tile = treemapRoot?.leaves().find((leaf) => leaf.data === node);
|
|
434
|
+
x = padding + ((tile?.x0 ?? 0) + (tile?.x1 ?? 0)) / 2;
|
|
435
|
+
y = padding + ((tile?.y0 ?? 0) + (tile?.y1 ?? 0)) / 2;
|
|
436
|
+
} else if (layoutMode === 'radial') {
|
|
437
|
+
const angle = (index * Math.PI * 2) / Math.max(visibleNodes.length, 1) - Math.PI / 2;
|
|
438
|
+
x = centerX + Math.cos(angle) * radius;
|
|
439
|
+
y = centerY + Math.sin(angle) * radius;
|
|
440
|
+
} else {
|
|
441
|
+
const kindIndex = layoutMode === 'kind'
|
|
442
|
+
? orderedNodes.slice(0, index).filter((candidate) => candidate.kind === node.kind).length
|
|
443
|
+
: index;
|
|
444
|
+
const kindCount = layoutMode === 'kind'
|
|
445
|
+
? orderedNodes.filter((candidate) => candidate.kind === node.kind).length
|
|
446
|
+
: visibleNodes.length;
|
|
447
|
+
const kindColumn = layoutMode === 'kind'
|
|
448
|
+
? [...new Set(orderedNodes.map((candidate) => candidate.kind))].indexOf(node.kind)
|
|
449
|
+
: 0;
|
|
450
|
+
const kindColumns = layoutMode === 'kind'
|
|
451
|
+
? Math.max(1, new Set(orderedNodes.map((candidate) => candidate.kind)).size)
|
|
452
|
+
: columns;
|
|
453
|
+
const localColumns = layoutMode === 'kind' ? Math.max(1, Math.ceil(Math.sqrt(kindCount))) : columns;
|
|
454
|
+
const localRows = layoutMode === 'kind' ? Math.max(1, Math.ceil(kindCount / localColumns)) : rows;
|
|
455
|
+
const column = layoutMode === 'kind' ? kindIndex % localColumns : index % columns;
|
|
456
|
+
const row = layoutMode === 'kind' ? Math.floor(kindIndex / localColumns) : Math.floor(index / columns);
|
|
457
|
+
x = layoutMode === 'kind'
|
|
458
|
+
? ((kindColumn + 0.5) * canvasWidth) / kindColumns
|
|
459
|
+
: ((column + 1) * canvasWidth) / (columns + 1);
|
|
460
|
+
y = ((row + 1) * canvasHeight) / (localRows + 1);
|
|
461
|
+
}
|
|
462
|
+
node.x = node.fx = x;
|
|
463
|
+
node.y = node.fy = y;
|
|
464
|
+
});
|
|
465
|
+
ticked();
|
|
466
|
+
}
|
|
467
|
+
|
|
364
468
|
function updateStatus() {
|
|
365
469
|
if (anchorId && focusVisibleOnly) {
|
|
366
470
|
subtitle.textContent = 'Focus on ' + (nodeById.get(anchorId)?.label ?? anchorId);
|
|
@@ -416,16 +520,17 @@
|
|
|
416
520
|
const visibleEdgeIds = visibleSets.visibleEdgeIds;
|
|
417
521
|
const activeAnchorVisible = Boolean(anchorId && visibleNodeIds.has(anchorId));
|
|
418
522
|
|
|
523
|
+
renderGraph(visibleNodeIds, visibleEdgeIds);
|
|
524
|
+
applyLayout(visibleNodeIds);
|
|
525
|
+
|
|
419
526
|
nodes
|
|
420
|
-
.classed('is-
|
|
421
|
-
.classed('is-dimmed', (node) => !focusVisibleOnly && anchorId ? !visibleNodeIds.has(node.id) : false)
|
|
527
|
+
.classed('is-dimmed', () => false)
|
|
422
528
|
.classed('is-selected', (node) => anchorId === node.id)
|
|
423
529
|
.classed('is-highlighted', (node) => activeAnchorVisible && visibleNodeIds.has(node.id));
|
|
424
530
|
|
|
425
531
|
links
|
|
426
|
-
.classed('is-
|
|
427
|
-
.classed('is-
|
|
428
|
-
.classed('is-highlighted', (edge) => visibleEdgeIds.has(edge.id));
|
|
532
|
+
.classed('is-dimmed', () => false)
|
|
533
|
+
.classed('is-highlighted', () => true);
|
|
429
534
|
|
|
430
535
|
updateStatus();
|
|
431
536
|
updateInfo();
|
|
@@ -554,25 +659,27 @@
|
|
|
554
659
|
nodes.attr('transform', (d) => 'translate(' + d.x + ',' + d.y + ')');
|
|
555
660
|
}
|
|
556
661
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
662
|
+
function bindNodeInteractions() {
|
|
663
|
+
nodes
|
|
664
|
+
.on('click', (event, node) => {
|
|
665
|
+
event.stopPropagation();
|
|
666
|
+
setAnchor(node.id);
|
|
667
|
+
})
|
|
668
|
+
.on('mouseenter', function(event, node) {
|
|
669
|
+
if (!tooltip) return;
|
|
670
|
+
tooltip.style.display = 'block';
|
|
671
|
+
tooltip.textContent = node.label + ' · ' + node.kind;
|
|
672
|
+
})
|
|
673
|
+
.on('mousemove', function(event) {
|
|
674
|
+
if (!tooltip) return;
|
|
675
|
+
tooltip.style.left = event.clientX + 14 + 'px';
|
|
676
|
+
tooltip.style.top = event.clientY + 14 + 'px';
|
|
677
|
+
})
|
|
678
|
+
.on('mouseleave', function() {
|
|
679
|
+
if (!tooltip) return;
|
|
680
|
+
tooltip.style.display = 'none';
|
|
681
|
+
});
|
|
682
|
+
}
|
|
576
683
|
|
|
577
684
|
svg.on('click', (event) => {
|
|
578
685
|
if (event.target === svg.node()) {
|
|
@@ -589,6 +696,13 @@
|
|
|
589
696
|
updateDisplay();
|
|
590
697
|
});
|
|
591
698
|
|
|
699
|
+
layoutSelect?.addEventListener('change', (event) => {
|
|
700
|
+
const value = event.target instanceof HTMLSelectElement ? event.target.value : 'force';
|
|
701
|
+
if (value !== 'force' && value !== 'radial' && value !== 'grid' && value !== 'kind' && value !== 'treemap') return;
|
|
702
|
+
layoutMode = value;
|
|
703
|
+
applyState();
|
|
704
|
+
});
|
|
705
|
+
|
|
592
706
|
searchInput.value = filterText;
|
|
593
707
|
searchInput.addEventListener('input', (event) => {
|
|
594
708
|
filterText = event.target.value;
|