@bfra.me/workspace-analyzer 0.1.0 → 0.2.0
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.
- package/README.md +218 -0
- package/lib/{chunk-WOJ4C7N7.js → chunk-4V5KYQED.js} +3697 -10
- package/lib/cli.js +315 -9
- package/lib/index.d.ts +566 -5
- package/lib/index.js +49 -1
- package/package.json +2 -1
- package/src/cli/commands/analyze.ts +8 -5
- package/src/cli/commands/visualize.ts +406 -0
- package/src/cli/index.ts +45 -1
- package/src/cli/types.ts +43 -0
- package/src/config/schema.ts +3 -3
- package/src/index.ts +49 -0
- package/src/visualizer/graph-builder.ts +397 -0
- package/src/visualizer/html-renderer.ts +556 -0
- package/src/visualizer/index.ts +83 -0
- package/src/visualizer/mermaid-exporter.ts +234 -0
- package/src/visualizer/templates/d3-bundle.ts +1566 -0
- package/src/visualizer/templates/graph-template.ts +959 -0
- package/src/visualizer/templates/styles.ts +928 -0
- package/src/visualizer/types.ts +226 -0
- package/src/visualizer/violation-collector.ts +246 -0
|
@@ -1310,12 +1310,12 @@ function findCycles(graph) {
|
|
|
1310
1310
|
const cycles = [];
|
|
1311
1311
|
const visited = /* @__PURE__ */ new Set();
|
|
1312
1312
|
const recursionStack = /* @__PURE__ */ new Set();
|
|
1313
|
-
const
|
|
1313
|
+
const path17 = [];
|
|
1314
1314
|
function dfs(nodeId) {
|
|
1315
1315
|
if (recursionStack.has(nodeId)) {
|
|
1316
|
-
const cycleStart =
|
|
1316
|
+
const cycleStart = path17.indexOf(nodeId);
|
|
1317
1317
|
if (cycleStart !== -1) {
|
|
1318
|
-
const cycleNodes =
|
|
1318
|
+
const cycleNodes = path17.slice(cycleStart);
|
|
1319
1319
|
cycleNodes.push(nodeId);
|
|
1320
1320
|
const cycleEdges = [];
|
|
1321
1321
|
for (let i = 0; i < cycleNodes.length - 1; i++) {
|
|
@@ -1339,14 +1339,14 @@ function findCycles(graph) {
|
|
|
1339
1339
|
}
|
|
1340
1340
|
visited.add(nodeId);
|
|
1341
1341
|
recursionStack.add(nodeId);
|
|
1342
|
-
|
|
1342
|
+
path17.push(nodeId);
|
|
1343
1343
|
const node = graph.nodes.get(nodeId);
|
|
1344
1344
|
if (node !== void 0) {
|
|
1345
1345
|
for (const importId of node.imports) {
|
|
1346
1346
|
dfs(importId);
|
|
1347
1347
|
}
|
|
1348
1348
|
}
|
|
1349
|
-
|
|
1349
|
+
path17.pop();
|
|
1350
1350
|
recursionStack.delete(nodeId);
|
|
1351
1351
|
}
|
|
1352
1352
|
for (const nodeId of graph.nodes.keys()) {
|
|
@@ -5528,15 +5528,15 @@ var architecturalRulesSchema = z.object({
|
|
|
5528
5528
|
enforcePublicApi: z.boolean().optional().describe("Enforce explicit public API exports")
|
|
5529
5529
|
}).strict();
|
|
5530
5530
|
var analyzerConfigSchema = z.object({
|
|
5531
|
-
include: z.array(z.string()).optional().default(DEFAULT_ANALYZER_CONFIG.include).describe("Glob patterns for files to include in analysis"),
|
|
5532
|
-
exclude: z.array(z.string()).optional().default(DEFAULT_ANALYZER_CONFIG.exclude).describe("Glob patterns for files to exclude from analysis"),
|
|
5531
|
+
include: z.array(z.string()).optional().default([...DEFAULT_ANALYZER_CONFIG.include]).describe("Glob patterns for files to include in analysis"),
|
|
5532
|
+
exclude: z.array(z.string()).optional().default([...DEFAULT_ANALYZER_CONFIG.exclude]).describe("Glob patterns for files to exclude from analysis"),
|
|
5533
5533
|
minSeverity: severitySchema.optional().default(DEFAULT_ANALYZER_CONFIG.minSeverity).describe("Minimum severity level to report"),
|
|
5534
5534
|
categories: z.array(categorySchema).optional().default([]).describe("Categories of issues to check (empty means all)"),
|
|
5535
5535
|
cache: z.boolean().optional().default(true).describe("Enable incremental analysis caching"),
|
|
5536
5536
|
rules: ruleConfigSchema.optional().default({}).describe("Custom rules configuration")
|
|
5537
5537
|
}).strict();
|
|
5538
5538
|
var workspaceOptionsSchema = z.object({
|
|
5539
|
-
packagePatterns: z.array(z.string()).optional().default(DEFAULT_PACKAGE_PATTERNS).describe("Glob patterns for package locations"),
|
|
5539
|
+
packagePatterns: z.array(z.string()).optional().default([...DEFAULT_PACKAGE_PATTERNS]).describe("Glob patterns for package locations"),
|
|
5540
5540
|
concurrency: z.number().int().min(1).max(16).optional().default(DEFAULT_CONCURRENCY).describe("Maximum parallel analysis operations"),
|
|
5541
5541
|
cacheDir: z.string().optional().default(DEFAULT_CACHE_DIR).describe("Directory for analysis cache files"),
|
|
5542
5542
|
maxCacheAge: z.number().int().min(0).optional().default(DEFAULT_MAX_CACHE_AGE).describe("Maximum cache age in milliseconds"),
|
|
@@ -6987,6 +6987,3669 @@ function getStatusEmoji(severity, useEmoji) {
|
|
|
6987
6987
|
return SEVERITY_CONFIG[severity].emoji;
|
|
6988
6988
|
}
|
|
6989
6989
|
|
|
6990
|
+
// src/visualizer/types.ts
|
|
6991
|
+
var DEFAULT_VISUALIZER_OPTIONS = {
|
|
6992
|
+
outputPath: "./workspace-graph.html",
|
|
6993
|
+
format: "html",
|
|
6994
|
+
autoOpen: true,
|
|
6995
|
+
title: "Workspace Dependency Graph",
|
|
6996
|
+
filters: {},
|
|
6997
|
+
includeTypeImports: true,
|
|
6998
|
+
maxNodes: 1e3
|
|
6999
|
+
};
|
|
7000
|
+
var SEVERITY_ORDER2 = ["critical", "error", "warning", "info"];
|
|
7001
|
+
function getHighestSeverity(severities) {
|
|
7002
|
+
if (severities.length === 0) {
|
|
7003
|
+
return void 0;
|
|
7004
|
+
}
|
|
7005
|
+
for (const level of SEVERITY_ORDER2) {
|
|
7006
|
+
if (severities.includes(level)) {
|
|
7007
|
+
return level;
|
|
7008
|
+
}
|
|
7009
|
+
}
|
|
7010
|
+
return severities[0];
|
|
7011
|
+
}
|
|
7012
|
+
|
|
7013
|
+
// src/visualizer/graph-builder.ts
|
|
7014
|
+
import { ok as ok23 } from "@bfra.me/es/result";
|
|
7015
|
+
var DEFAULT_BUILD_OPTIONS = {
|
|
7016
|
+
includeTypeImports: true,
|
|
7017
|
+
maxNodes: 1e3,
|
|
7018
|
+
title: "Workspace Dependency Graph"
|
|
7019
|
+
};
|
|
7020
|
+
function transformNodeToVisualization(node, context, nodeIdsInCycles) {
|
|
7021
|
+
const { cycles, issues, layerConfig, packageMap } = context;
|
|
7022
|
+
const cycleNodeSet = nodeIdsInCycles ?? buildCycleNodeSet(cycles);
|
|
7023
|
+
const isInCycle = cycleNodeSet.has(node.id);
|
|
7024
|
+
const nodeViolations = mapIssuesToViolations(node.id, issues);
|
|
7025
|
+
const severities = nodeViolations.map((v) => v.severity);
|
|
7026
|
+
const highestSeverity = getHighestSeverity(severities);
|
|
7027
|
+
const layer = layerConfig === void 0 ? void 0 : getFileLayer(node.filePath, layerConfig);
|
|
7028
|
+
const packageName = packageMap.get(node.filePath) ?? node.packageName;
|
|
7029
|
+
return {
|
|
7030
|
+
id: node.id,
|
|
7031
|
+
name: node.name,
|
|
7032
|
+
filePath: node.filePath,
|
|
7033
|
+
packageName,
|
|
7034
|
+
layer,
|
|
7035
|
+
importsCount: node.imports.length,
|
|
7036
|
+
importedByCount: node.importedBy.length,
|
|
7037
|
+
isInCycle,
|
|
7038
|
+
violations: nodeViolations,
|
|
7039
|
+
highestViolationSeverity: highestSeverity
|
|
7040
|
+
};
|
|
7041
|
+
}
|
|
7042
|
+
function transformEdgeToVisualization(edge, cycleEdgeSet, edgeToCycleId) {
|
|
7043
|
+
const edgeKey = `${edge.from}->${edge.to}`;
|
|
7044
|
+
const isInCycle = cycleEdgeSet.has(edgeKey);
|
|
7045
|
+
const cycleId = edgeToCycleId.get(edgeKey);
|
|
7046
|
+
const type = mapImportType(edge.type, edge.isTypeOnly);
|
|
7047
|
+
return {
|
|
7048
|
+
source: edge.from,
|
|
7049
|
+
target: edge.to,
|
|
7050
|
+
type,
|
|
7051
|
+
isInCycle,
|
|
7052
|
+
cycleId
|
|
7053
|
+
};
|
|
7054
|
+
}
|
|
7055
|
+
function transformCycleToVisualization(cycle, index) {
|
|
7056
|
+
const id = `cycle-${index + 1}`;
|
|
7057
|
+
const edges = cycle.edges.map((edge) => ({
|
|
7058
|
+
from: edge.from,
|
|
7059
|
+
to: edge.to
|
|
7060
|
+
}));
|
|
7061
|
+
return {
|
|
7062
|
+
id,
|
|
7063
|
+
nodes: cycle.nodes,
|
|
7064
|
+
edges,
|
|
7065
|
+
length: cycle.nodes.length,
|
|
7066
|
+
description: cycle.description
|
|
7067
|
+
};
|
|
7068
|
+
}
|
|
7069
|
+
function buildVisualizationData(context, options = {}) {
|
|
7070
|
+
const opts = { ...DEFAULT_BUILD_OPTIONS, ...options };
|
|
7071
|
+
const { graph, cycles, layerConfig, analyzerVersion } = context;
|
|
7072
|
+
const vizCycles = cycles.map((cycle, index) => transformCycleToVisualization(cycle, index));
|
|
7073
|
+
const { cycleEdgeSet, edgeToCycleId } = buildCycleEdgeMaps(vizCycles);
|
|
7074
|
+
const cycleNodeIds = buildCycleNodeSet(cycles);
|
|
7075
|
+
const filteredEdges = opts.includeTypeImports ? graph.edges : graph.edges.filter((e) => !e.isTypeOnly);
|
|
7076
|
+
const vizEdges = filteredEdges.map(
|
|
7077
|
+
(edge) => transformEdgeToVisualization(edge, cycleEdgeSet, edgeToCycleId)
|
|
7078
|
+
);
|
|
7079
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
7080
|
+
for (const edge of filteredEdges) {
|
|
7081
|
+
nodeIds.add(edge.from);
|
|
7082
|
+
nodeIds.add(edge.to);
|
|
7083
|
+
}
|
|
7084
|
+
let vizNodes = [];
|
|
7085
|
+
for (const nodeId of nodeIds) {
|
|
7086
|
+
const node = graph.nodes.get(nodeId);
|
|
7087
|
+
if (node !== void 0) {
|
|
7088
|
+
vizNodes.push(transformNodeToVisualization(node, context, cycleNodeIds));
|
|
7089
|
+
}
|
|
7090
|
+
}
|
|
7091
|
+
if (vizNodes.length > opts.maxNodes) {
|
|
7092
|
+
vizNodes = prioritizeNodes(vizNodes, opts.maxNodes);
|
|
7093
|
+
}
|
|
7094
|
+
const includedNodeIds = new Set(vizNodes.map((n) => n.id));
|
|
7095
|
+
const filteredVizEdges = vizEdges.filter(
|
|
7096
|
+
(e) => includedNodeIds.has(e.source) && includedNodeIds.has(e.target)
|
|
7097
|
+
);
|
|
7098
|
+
const statistics = computeVisualizationStatistics(vizNodes, filteredVizEdges, vizCycles, context);
|
|
7099
|
+
const layers = layerConfig?.layers.map((layer) => ({
|
|
7100
|
+
name: layer.name,
|
|
7101
|
+
allowedDependencies: layer.allowedDependencies
|
|
7102
|
+
})) ?? [];
|
|
7103
|
+
const metadata = {
|
|
7104
|
+
workspacePath: graph.rootPath,
|
|
7105
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7106
|
+
analyzerVersion
|
|
7107
|
+
};
|
|
7108
|
+
return ok23({
|
|
7109
|
+
nodes: vizNodes,
|
|
7110
|
+
edges: filteredVizEdges,
|
|
7111
|
+
cycles: vizCycles,
|
|
7112
|
+
statistics,
|
|
7113
|
+
layers,
|
|
7114
|
+
metadata
|
|
7115
|
+
});
|
|
7116
|
+
}
|
|
7117
|
+
function buildCycleEdgeMaps(cycles) {
|
|
7118
|
+
const cycleEdgeSet = /* @__PURE__ */ new Set();
|
|
7119
|
+
const edgeToCycleId = /* @__PURE__ */ new Map();
|
|
7120
|
+
for (const cycle of cycles) {
|
|
7121
|
+
for (const edge of cycle.edges) {
|
|
7122
|
+
const key = `${edge.from}->${edge.to}`;
|
|
7123
|
+
cycleEdgeSet.add(key);
|
|
7124
|
+
edgeToCycleId.set(key, cycle.id);
|
|
7125
|
+
}
|
|
7126
|
+
}
|
|
7127
|
+
return { cycleEdgeSet, edgeToCycleId };
|
|
7128
|
+
}
|
|
7129
|
+
function buildCycleNodeSet(cycles) {
|
|
7130
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
7131
|
+
for (const cycle of cycles) {
|
|
7132
|
+
for (const nodeId of cycle.nodes) {
|
|
7133
|
+
nodeIds.add(nodeId);
|
|
7134
|
+
}
|
|
7135
|
+
}
|
|
7136
|
+
return nodeIds;
|
|
7137
|
+
}
|
|
7138
|
+
function mapImportType(type, isTypeOnly) {
|
|
7139
|
+
if (isTypeOnly || type === "type-only") {
|
|
7140
|
+
return "type-only";
|
|
7141
|
+
}
|
|
7142
|
+
if (type === "dynamic") {
|
|
7143
|
+
return "dynamic";
|
|
7144
|
+
}
|
|
7145
|
+
if (type === "require") {
|
|
7146
|
+
return "require";
|
|
7147
|
+
}
|
|
7148
|
+
return "static";
|
|
7149
|
+
}
|
|
7150
|
+
function mapIssuesToViolations(nodeId, issues) {
|
|
7151
|
+
const violations = [];
|
|
7152
|
+
for (const issue of issues) {
|
|
7153
|
+
const normalizedIssuePath = issue.location.filePath.replaceAll("\\", "/");
|
|
7154
|
+
const normalizedNodeId = nodeId.replaceAll("\\", "/");
|
|
7155
|
+
if (normalizedIssuePath.endsWith(normalizedNodeId) || normalizedIssuePath === normalizedNodeId) {
|
|
7156
|
+
violations.push({
|
|
7157
|
+
id: `${issue.id}-${violations.length}`,
|
|
7158
|
+
message: issue.description,
|
|
7159
|
+
severity: issue.severity,
|
|
7160
|
+
ruleId: issue.id
|
|
7161
|
+
});
|
|
7162
|
+
}
|
|
7163
|
+
}
|
|
7164
|
+
return violations;
|
|
7165
|
+
}
|
|
7166
|
+
function prioritizeNodes(nodes, maxNodes) {
|
|
7167
|
+
const scored = nodes.map((node) => {
|
|
7168
|
+
let score = 0;
|
|
7169
|
+
if (node.isInCycle) {
|
|
7170
|
+
score += 100;
|
|
7171
|
+
}
|
|
7172
|
+
if (node.highestViolationSeverity !== void 0) {
|
|
7173
|
+
const severityIndex = SEVERITY_ORDER2.indexOf(node.highestViolationSeverity);
|
|
7174
|
+
score += (SEVERITY_ORDER2.length - severityIndex) * 20;
|
|
7175
|
+
}
|
|
7176
|
+
score += node.importsCount + node.importedByCount;
|
|
7177
|
+
return { node, score };
|
|
7178
|
+
});
|
|
7179
|
+
scored.sort((a, b) => b.score - a.score);
|
|
7180
|
+
return scored.slice(0, maxNodes).map((s) => s.node);
|
|
7181
|
+
}
|
|
7182
|
+
function computeVisualizationStatistics(nodes, edges, cycles, _context) {
|
|
7183
|
+
const nodesByLayer = {};
|
|
7184
|
+
const violationsBySeverity = {
|
|
7185
|
+
info: 0,
|
|
7186
|
+
warning: 0,
|
|
7187
|
+
error: 0,
|
|
7188
|
+
critical: 0
|
|
7189
|
+
};
|
|
7190
|
+
const packagesSet = /* @__PURE__ */ new Set();
|
|
7191
|
+
const filesSet = /* @__PURE__ */ new Set();
|
|
7192
|
+
for (const node of nodes) {
|
|
7193
|
+
filesSet.add(node.filePath);
|
|
7194
|
+
if (node.packageName !== void 0) {
|
|
7195
|
+
packagesSet.add(node.packageName);
|
|
7196
|
+
}
|
|
7197
|
+
if (node.layer !== void 0) {
|
|
7198
|
+
nodesByLayer[node.layer] = (nodesByLayer[node.layer] ?? 0) + 1;
|
|
7199
|
+
}
|
|
7200
|
+
for (const violation of node.violations) {
|
|
7201
|
+
violationsBySeverity[violation.severity]++;
|
|
7202
|
+
}
|
|
7203
|
+
}
|
|
7204
|
+
return {
|
|
7205
|
+
totalNodes: nodes.length,
|
|
7206
|
+
totalEdges: edges.length,
|
|
7207
|
+
totalCycles: cycles.length,
|
|
7208
|
+
nodesByLayer,
|
|
7209
|
+
violationsBySeverity,
|
|
7210
|
+
packagesAnalyzed: packagesSet.size,
|
|
7211
|
+
filesAnalyzed: filesSet.size
|
|
7212
|
+
};
|
|
7213
|
+
}
|
|
7214
|
+
|
|
7215
|
+
// src/visualizer/html-renderer.ts
|
|
7216
|
+
import { err as err6, ok as ok24 } from "@bfra.me/es/result";
|
|
7217
|
+
|
|
7218
|
+
// src/visualizer/templates/graph-template.ts
|
|
7219
|
+
function generateGraphInitScript() {
|
|
7220
|
+
return `
|
|
7221
|
+
/**
|
|
7222
|
+
* Dependency Graph Visualization
|
|
7223
|
+
* Generated by @bfra.me/workspace-analyzer
|
|
7224
|
+
*/
|
|
7225
|
+
(function() {
|
|
7226
|
+
'use strict';
|
|
7227
|
+
|
|
7228
|
+
// Get visualization data from global scope
|
|
7229
|
+
const visualizationData = window.VISUALIZATION_DATA;
|
|
7230
|
+
if (!visualizationData) {
|
|
7231
|
+
console.error('Visualization data not found');
|
|
7232
|
+
return;
|
|
7233
|
+
}
|
|
7234
|
+
|
|
7235
|
+
// Extract data
|
|
7236
|
+
const { nodes, edges, cycles, statistics, layers, metadata } = visualizationData;
|
|
7237
|
+
|
|
7238
|
+
// Configuration
|
|
7239
|
+
const config = {
|
|
7240
|
+
nodeRadius: 8,
|
|
7241
|
+
linkDistance: 80,
|
|
7242
|
+
chargeStrength: -300,
|
|
7243
|
+
centerStrength: 0.05,
|
|
7244
|
+
collisionRadiusMultiplier: 1.5,
|
|
7245
|
+
zoomExtent: [0.1, 4],
|
|
7246
|
+
alphaDecay: 0.0228,
|
|
7247
|
+
velocityDecay: 0.4
|
|
7248
|
+
};
|
|
7249
|
+
|
|
7250
|
+
// State management
|
|
7251
|
+
const state = {
|
|
7252
|
+
filters: {
|
|
7253
|
+
layers: new Set(layers.map(l => l.name)),
|
|
7254
|
+
severities: new Set(['critical', 'error', 'warning', 'info']),
|
|
7255
|
+
packages: new Set(),
|
|
7256
|
+
showCyclesOnly: false,
|
|
7257
|
+
showViolationsOnly: false,
|
|
7258
|
+
searchQuery: ''
|
|
7259
|
+
},
|
|
7260
|
+
selectedNode: null,
|
|
7261
|
+
highlightedNodes: new Set(),
|
|
7262
|
+
transform: d3.zoomIdentity
|
|
7263
|
+
};
|
|
7264
|
+
|
|
7265
|
+
// Color scales
|
|
7266
|
+
const severityColors = {
|
|
7267
|
+
critical: '#dc2626',
|
|
7268
|
+
error: '#ea580c',
|
|
7269
|
+
warning: '#ca8a04',
|
|
7270
|
+
info: '#2563eb'
|
|
7271
|
+
};
|
|
7272
|
+
|
|
7273
|
+
const layerColors = {
|
|
7274
|
+
domain: '#8b5cf6',
|
|
7275
|
+
application: '#06b6d4',
|
|
7276
|
+
infrastructure: '#84cc16',
|
|
7277
|
+
presentation: '#f97316',
|
|
7278
|
+
shared: '#6b7280',
|
|
7279
|
+
unknown: '#9ca3af'
|
|
7280
|
+
};
|
|
7281
|
+
|
|
7282
|
+
// Get container dimensions
|
|
7283
|
+
const container = document.querySelector('.graph-container');
|
|
7284
|
+
const width = container.clientWidth;
|
|
7285
|
+
const height = container.clientHeight;
|
|
7286
|
+
|
|
7287
|
+
// Create SVG
|
|
7288
|
+
const svg = d3.select('.graph-canvas')
|
|
7289
|
+
.attr('viewBox', [0, 0, width, height])
|
|
7290
|
+
.attr('preserveAspectRatio', 'xMidYMid meet');
|
|
7291
|
+
|
|
7292
|
+
// Create groups for layering
|
|
7293
|
+
const g = svg.append('g').attr('class', 'graph-layer');
|
|
7294
|
+
const edgeGroup = g.append('g').attr('class', 'edges');
|
|
7295
|
+
const nodeGroup = g.append('g').attr('class', 'nodes');
|
|
7296
|
+
|
|
7297
|
+
// Arrow marker definition
|
|
7298
|
+
svg.append('defs').append('marker')
|
|
7299
|
+
.attr('id', 'arrow')
|
|
7300
|
+
.attr('viewBox', '0 -5 10 10')
|
|
7301
|
+
.attr('refX', 20)
|
|
7302
|
+
.attr('refY', 0)
|
|
7303
|
+
.attr('markerWidth', 6)
|
|
7304
|
+
.attr('markerHeight', 6)
|
|
7305
|
+
.attr('orient', 'auto')
|
|
7306
|
+
.append('path')
|
|
7307
|
+
.attr('d', 'M0,-5L10,0L0,5')
|
|
7308
|
+
.attr('class', 'edge-arrow');
|
|
7309
|
+
|
|
7310
|
+
// Arrow marker for cycle edges
|
|
7311
|
+
svg.select('defs').append('marker')
|
|
7312
|
+
.attr('id', 'arrow-cycle')
|
|
7313
|
+
.attr('viewBox', '0 -5 10 10')
|
|
7314
|
+
.attr('refX', 20)
|
|
7315
|
+
.attr('refY', 0)
|
|
7316
|
+
.attr('markerWidth', 6)
|
|
7317
|
+
.attr('markerHeight', 6)
|
|
7318
|
+
.attr('orient', 'auto')
|
|
7319
|
+
.append('path')
|
|
7320
|
+
.attr('d', 'M0,-5L10,0L0,5')
|
|
7321
|
+
.attr('class', 'edge-arrow cycle');
|
|
7322
|
+
|
|
7323
|
+
// Create simulation data with mutable copies
|
|
7324
|
+
const simNodes = nodes.map(n => ({...n}));
|
|
7325
|
+
const simEdges = edges.map(e => ({
|
|
7326
|
+
...e,
|
|
7327
|
+
source: e.source,
|
|
7328
|
+
target: e.target
|
|
7329
|
+
}));
|
|
7330
|
+
|
|
7331
|
+
// Create node ID lookup
|
|
7332
|
+
const nodeById = new Map(simNodes.map(n => [n.id, n]));
|
|
7333
|
+
|
|
7334
|
+
// Force simulation
|
|
7335
|
+
const simulation = d3.forceSimulation(simNodes)
|
|
7336
|
+
.force('link', d3.forceLink(simEdges)
|
|
7337
|
+
.id(d => d.id)
|
|
7338
|
+
.distance(config.linkDistance))
|
|
7339
|
+
.force('charge', d3.forceManyBody()
|
|
7340
|
+
.strength(config.chargeStrength))
|
|
7341
|
+
.force('center', d3.forceCenter(width / 2, height / 2)
|
|
7342
|
+
.strength(config.centerStrength))
|
|
7343
|
+
.force('collide', d3.forceCollide()
|
|
7344
|
+
.radius(config.nodeRadius * config.collisionRadiusMultiplier))
|
|
7345
|
+
.alphaDecay(config.alphaDecay)
|
|
7346
|
+
.velocityDecay(config.velocityDecay);
|
|
7347
|
+
|
|
7348
|
+
// Zoom behavior
|
|
7349
|
+
const zoom = d3.zoom()
|
|
7350
|
+
.scaleExtent(config.zoomExtent)
|
|
7351
|
+
.on('zoom', (event) => {
|
|
7352
|
+
state.transform = event.transform;
|
|
7353
|
+
g.attr('transform', 'translate(' + event.transform.x + ',' + event.transform.y + ') scale(' + event.transform.k + ')');
|
|
7354
|
+
updateZoomDisplay();
|
|
7355
|
+
});
|
|
7356
|
+
|
|
7357
|
+
svg.call(zoom);
|
|
7358
|
+
|
|
7359
|
+
// Draw edges
|
|
7360
|
+
function drawEdges() {
|
|
7361
|
+
const filteredEdges = getFilteredEdges();
|
|
7362
|
+
|
|
7363
|
+
const edge = edgeGroup.selectAll('.graph-edge')
|
|
7364
|
+
.data(filteredEdges, d => d.source.id + '-' + d.target.id);
|
|
7365
|
+
|
|
7366
|
+
edge.exit().remove();
|
|
7367
|
+
|
|
7368
|
+
const edgeEnter = edge.enter()
|
|
7369
|
+
.append('g')
|
|
7370
|
+
.attr('class', 'graph-edge');
|
|
7371
|
+
|
|
7372
|
+
// Edge line
|
|
7373
|
+
edgeEnter.append('line')
|
|
7374
|
+
.attr('class', d => getEdgeClass(d))
|
|
7375
|
+
.attr('marker-end', d => d.isInCycle ? 'url(#arrow-cycle)' : 'url(#arrow)')
|
|
7376
|
+
.on('mouseenter', handleEdgeMouseEnter)
|
|
7377
|
+
.on('mouseleave', handleEdgeMouseLeave)
|
|
7378
|
+
.on('click', handleEdgeClick);
|
|
7379
|
+
|
|
7380
|
+
// Merge and update
|
|
7381
|
+
const edgeMerged = edgeEnter.merge(edge);
|
|
7382
|
+
|
|
7383
|
+
edgeMerged.select('line')
|
|
7384
|
+
.attr('class', d => getEdgeClass(d));
|
|
7385
|
+
|
|
7386
|
+
return edgeMerged;
|
|
7387
|
+
}
|
|
7388
|
+
|
|
7389
|
+
// Draw nodes
|
|
7390
|
+
function drawNodes() {
|
|
7391
|
+
const filteredNodes = getFilteredNodes();
|
|
7392
|
+
|
|
7393
|
+
const node = nodeGroup.selectAll('.graph-node')
|
|
7394
|
+
.data(filteredNodes, d => d.id);
|
|
7395
|
+
|
|
7396
|
+
node.exit().remove();
|
|
7397
|
+
|
|
7398
|
+
const nodeEnter = node.enter()
|
|
7399
|
+
.append('g')
|
|
7400
|
+
.attr('class', 'graph-node')
|
|
7401
|
+
.call(drag(simulation));
|
|
7402
|
+
|
|
7403
|
+
// Node circle
|
|
7404
|
+
nodeEnter.append('circle')
|
|
7405
|
+
.attr('r', config.nodeRadius)
|
|
7406
|
+
.attr('class', d => getNodeClass(d));
|
|
7407
|
+
|
|
7408
|
+
// Node label
|
|
7409
|
+
nodeEnter.append('text')
|
|
7410
|
+
.attr('class', 'node-label')
|
|
7411
|
+
.attr('dy', config.nodeRadius + 12)
|
|
7412
|
+
.text(d => getNodeLabel(d));
|
|
7413
|
+
|
|
7414
|
+
// Merge and update
|
|
7415
|
+
const nodeMerged = nodeEnter.merge(node);
|
|
7416
|
+
|
|
7417
|
+
nodeMerged.select('circle')
|
|
7418
|
+
.attr('class', d => getNodeClass(d));
|
|
7419
|
+
|
|
7420
|
+
// Event handlers
|
|
7421
|
+
nodeMerged
|
|
7422
|
+
.on('mouseenter', handleNodeMouseEnter)
|
|
7423
|
+
.on('mouseleave', handleNodeMouseLeave)
|
|
7424
|
+
.on('click', handleNodeClick);
|
|
7425
|
+
|
|
7426
|
+
return nodeMerged;
|
|
7427
|
+
}
|
|
7428
|
+
|
|
7429
|
+
// Get CSS class for edge
|
|
7430
|
+
function getEdgeClass(edge) {
|
|
7431
|
+
const classes = ['edge-line'];
|
|
7432
|
+
if (edge.isInCycle) classes.push('cycle');
|
|
7433
|
+
classes.push('type-' + edge.type);
|
|
7434
|
+
return classes.join(' ');
|
|
7435
|
+
}
|
|
7436
|
+
|
|
7437
|
+
// Get CSS class for node
|
|
7438
|
+
function getNodeClass(node) {
|
|
7439
|
+
const classes = ['node-circle'];
|
|
7440
|
+
|
|
7441
|
+
// Cycle highlighting
|
|
7442
|
+
if (node.isInCycle) classes.push('cycle');
|
|
7443
|
+
|
|
7444
|
+
// Severity-based color (takes precedence)
|
|
7445
|
+
if (node.highestViolationSeverity) {
|
|
7446
|
+
classes.push('severity-' + node.highestViolationSeverity);
|
|
7447
|
+
} else if (node.layer) {
|
|
7448
|
+
// Layer-based color
|
|
7449
|
+
classes.push('layer-' + node.layer.toLowerCase());
|
|
7450
|
+
} else {
|
|
7451
|
+
classes.push('default');
|
|
7452
|
+
}
|
|
7453
|
+
|
|
7454
|
+
return classes.join(' ');
|
|
7455
|
+
}
|
|
7456
|
+
|
|
7457
|
+
// Get display label for node
|
|
7458
|
+
function getNodeLabel(node) {
|
|
7459
|
+
// Show just the filename
|
|
7460
|
+
const parts = node.name.split('/');
|
|
7461
|
+
return parts[parts.length - 1] || node.name;
|
|
7462
|
+
}
|
|
7463
|
+
|
|
7464
|
+
// Filtering
|
|
7465
|
+
function getFilteredNodes() {
|
|
7466
|
+
return simNodes.filter(node => {
|
|
7467
|
+
// Layer filter
|
|
7468
|
+
if (node.layer && !state.filters.layers.has(node.layer)) {
|
|
7469
|
+
return false;
|
|
7470
|
+
}
|
|
7471
|
+
|
|
7472
|
+
// Severity filter - only filter out nodes with violations if their highest severity is not selected
|
|
7473
|
+
if (node.highestViolationSeverity && !state.filters.severities.has(node.highestViolationSeverity)) {
|
|
7474
|
+
return false;
|
|
7475
|
+
}
|
|
7476
|
+
|
|
7477
|
+
// Cycles only
|
|
7478
|
+
if (state.filters.showCyclesOnly && !node.isInCycle) {
|
|
7479
|
+
return false;
|
|
7480
|
+
}
|
|
7481
|
+
|
|
7482
|
+
// Violations only
|
|
7483
|
+
if (state.filters.showViolationsOnly && node.violations.length === 0) {
|
|
7484
|
+
return false;
|
|
7485
|
+
}
|
|
7486
|
+
|
|
7487
|
+
// Search query
|
|
7488
|
+
if (state.filters.searchQuery) {
|
|
7489
|
+
const query = state.filters.searchQuery.toLowerCase();
|
|
7490
|
+
if (!node.name.toLowerCase().includes(query) &&
|
|
7491
|
+
!node.filePath.toLowerCase().includes(query) &&
|
|
7492
|
+
!(node.packageName && node.packageName.toLowerCase().includes(query))) {
|
|
7493
|
+
return false;
|
|
7494
|
+
}
|
|
7495
|
+
}
|
|
7496
|
+
|
|
7497
|
+
return true;
|
|
7498
|
+
});
|
|
7499
|
+
}
|
|
7500
|
+
|
|
7501
|
+
function getFilteredEdges() {
|
|
7502
|
+
const filteredNodeIds = new Set(getFilteredNodes().map(n => n.id));
|
|
7503
|
+
return simEdges.filter(edge => {
|
|
7504
|
+
const sourceId = typeof edge.source === 'object' ? edge.source.id : edge.source;
|
|
7505
|
+
const targetId = typeof edge.target === 'object' ? edge.target.id : edge.target;
|
|
7506
|
+
return filteredNodeIds.has(sourceId) && filteredNodeIds.has(targetId);
|
|
7507
|
+
});
|
|
7508
|
+
}
|
|
7509
|
+
|
|
7510
|
+
// Drag behavior
|
|
7511
|
+
function drag(simulation) {
|
|
7512
|
+
function dragstarted(event) {
|
|
7513
|
+
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
7514
|
+
event.subject.fx = event.subject.x;
|
|
7515
|
+
event.subject.fy = event.subject.y;
|
|
7516
|
+
}
|
|
7517
|
+
|
|
7518
|
+
function dragged(event) {
|
|
7519
|
+
event.subject.fx = event.x;
|
|
7520
|
+
event.subject.fy = event.y;
|
|
7521
|
+
}
|
|
7522
|
+
|
|
7523
|
+
function dragended(event) {
|
|
7524
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
7525
|
+
event.subject.fx = null;
|
|
7526
|
+
event.subject.fy = null;
|
|
7527
|
+
}
|
|
7528
|
+
|
|
7529
|
+
return d3.drag()
|
|
7530
|
+
.on('start', dragstarted)
|
|
7531
|
+
.on('drag', dragged)
|
|
7532
|
+
.on('end', dragended);
|
|
7533
|
+
}
|
|
7534
|
+
|
|
7535
|
+
// Tooltip
|
|
7536
|
+
const tooltip = d3.select('.tooltip');
|
|
7537
|
+
|
|
7538
|
+
function handleNodeMouseEnter(event, d) {
|
|
7539
|
+
// Show tooltip
|
|
7540
|
+
showTooltip(event, d);
|
|
7541
|
+
|
|
7542
|
+
// Highlight connected nodes
|
|
7543
|
+
highlightConnectedNodes(d);
|
|
7544
|
+
}
|
|
7545
|
+
|
|
7546
|
+
function handleNodeMouseLeave(event, d) {
|
|
7547
|
+
hideTooltip();
|
|
7548
|
+
clearHighlights();
|
|
7549
|
+
}
|
|
7550
|
+
|
|
7551
|
+
function handleNodeClick(event, d) {
|
|
7552
|
+
state.selectedNode = state.selectedNode === d ? null : d;
|
|
7553
|
+
if (state.selectedNode) {
|
|
7554
|
+
centerOnNode(d);
|
|
7555
|
+
}
|
|
7556
|
+
}
|
|
7557
|
+
|
|
7558
|
+
function handleEdgeMouseEnter(event, d) {
|
|
7559
|
+
showEdgeTooltip(event, d);
|
|
7560
|
+
}
|
|
7561
|
+
|
|
7562
|
+
function handleEdgeMouseLeave(event, d) {
|
|
7563
|
+
hideTooltip();
|
|
7564
|
+
}
|
|
7565
|
+
|
|
7566
|
+
function handleEdgeClick(event, d) {
|
|
7567
|
+
event.stopPropagation();
|
|
7568
|
+
showEdgeTooltip(event, d);
|
|
7569
|
+
}
|
|
7570
|
+
|
|
7571
|
+
function showTooltip(event, node) {
|
|
7572
|
+
const iconClass = node.highestViolationSeverity
|
|
7573
|
+
? 'severity-' + node.highestViolationSeverity
|
|
7574
|
+
: node.isInCycle ? 'cycle' : 'default';
|
|
7575
|
+
|
|
7576
|
+
let violationsHtml = '';
|
|
7577
|
+
if (node.violations.length > 0) {
|
|
7578
|
+
violationsHtml = '<div class="tooltip-violations">' +
|
|
7579
|
+
'<div class="tooltip-violations-title">Violations</div>' +
|
|
7580
|
+
node.violations.slice(0, 5).map(v =>
|
|
7581
|
+
'<div class="tooltip-violation">' +
|
|
7582
|
+
'<div class="tooltip-violation-dot severity-dot ' + v.severity + '"></div>' +
|
|
7583
|
+
'<div class="tooltip-violation-text">' + escapeHtml(v.message) + '</div>' +
|
|
7584
|
+
'</div>'
|
|
7585
|
+
).join('') +
|
|
7586
|
+
(node.violations.length > 5 ? '<div class="tooltip-violation"><div class="tooltip-violation-text">... and ' + (node.violations.length - 5) + ' more</div></div>' : '') +
|
|
7587
|
+
'</div>';
|
|
7588
|
+
}
|
|
7589
|
+
|
|
7590
|
+
const html = '<div class="tooltip-header">' +
|
|
7591
|
+
'<div class="tooltip-icon ' + iconClass + '">' +
|
|
7592
|
+
(node.isInCycle ? '\u21BB' : node.violations.length > 0 ? '!' : '\u25CB') +
|
|
7593
|
+
'</div>' +
|
|
7594
|
+
'<div>' +
|
|
7595
|
+
'<div class="tooltip-title">' + escapeHtml(node.name) + '</div>' +
|
|
7596
|
+
'<div class="tooltip-subtitle">' + escapeHtml(node.filePath) + '</div>' +
|
|
7597
|
+
'</div>' +
|
|
7598
|
+
'</div>' +
|
|
7599
|
+
'<div class="tooltip-content">' +
|
|
7600
|
+
'<div class="tooltip-row">' +
|
|
7601
|
+
'<span class="tooltip-label">Package</span>' +
|
|
7602
|
+
'<span class="tooltip-value">' + escapeHtml(node.packageName || 'N/A') + '</span>' +
|
|
7603
|
+
'</div>' +
|
|
7604
|
+
'<div class="tooltip-row">' +
|
|
7605
|
+
'<span class="tooltip-label">Layer</span>' +
|
|
7606
|
+
'<span class="tooltip-value">' + escapeHtml(node.layer || 'Unknown') + '</span>' +
|
|
7607
|
+
'</div>' +
|
|
7608
|
+
'<div class="tooltip-divider"></div>' +
|
|
7609
|
+
'<div class="tooltip-row">' +
|
|
7610
|
+
'<span class="tooltip-label">Imports</span>' +
|
|
7611
|
+
'<span class="tooltip-value">' + node.importsCount + '</span>' +
|
|
7612
|
+
'</div>' +
|
|
7613
|
+
'<div class="tooltip-row">' +
|
|
7614
|
+
'<span class="tooltip-label">Imported by</span>' +
|
|
7615
|
+
'<span class="tooltip-value">' + node.importedByCount + '</span>' +
|
|
7616
|
+
'</div>' +
|
|
7617
|
+
(node.isInCycle ? '<div class="tooltip-row"><span class="tooltip-label">In Cycle</span><span class="tooltip-value" style="color: #ef4444;">Yes</span></div>' : '') +
|
|
7618
|
+
'</div>' +
|
|
7619
|
+
violationsHtml;
|
|
7620
|
+
|
|
7621
|
+
tooltip.html(html);
|
|
7622
|
+
|
|
7623
|
+
// Position tooltip
|
|
7624
|
+
const tooltipNode = tooltip.node();
|
|
7625
|
+
const tooltipRect = tooltipNode.getBoundingClientRect();
|
|
7626
|
+
const viewportWidth = window.innerWidth;
|
|
7627
|
+
const viewportHeight = window.innerHeight;
|
|
7628
|
+
|
|
7629
|
+
let left = event.clientX + 10;
|
|
7630
|
+
let top = event.clientY + 10;
|
|
7631
|
+
|
|
7632
|
+
// Adjust if tooltip would overflow
|
|
7633
|
+
if (left + tooltipRect.width > viewportWidth - 20) {
|
|
7634
|
+
left = event.clientX - tooltipRect.width - 10;
|
|
7635
|
+
}
|
|
7636
|
+
if (top + tooltipRect.height > viewportHeight - 20) {
|
|
7637
|
+
top = event.clientY - tooltipRect.height - 10;
|
|
7638
|
+
}
|
|
7639
|
+
|
|
7640
|
+
tooltip
|
|
7641
|
+
.style('left', left + 'px')
|
|
7642
|
+
.style('top', top + 'px')
|
|
7643
|
+
.classed('visible', true);
|
|
7644
|
+
}
|
|
7645
|
+
|
|
7646
|
+
function showEdgeTooltip(event, edge) {
|
|
7647
|
+
const sourceId = typeof edge.source === 'object' ? edge.source.id : edge.source;
|
|
7648
|
+
const targetId = typeof edge.target === 'object' ? edge.target.id : edge.target;
|
|
7649
|
+
const sourceNode = nodeById.get(sourceId);
|
|
7650
|
+
const targetNode = nodeById.get(targetId);
|
|
7651
|
+
|
|
7652
|
+
if (!sourceNode || !targetNode) return;
|
|
7653
|
+
|
|
7654
|
+
const iconClass = edge.isInCycle ? 'cycle' : 'default';
|
|
7655
|
+
const importType = edge.type === 'static' ? 'Static Import' :
|
|
7656
|
+
edge.type === 'dynamic' ? 'Dynamic Import' :
|
|
7657
|
+
edge.type === 'type-only' ? 'Type-Only Import' :
|
|
7658
|
+
'Require Import';
|
|
7659
|
+
|
|
7660
|
+
const html = '<div class="tooltip-header">' +
|
|
7661
|
+
'<div class="tooltip-icon ' + iconClass + '">' +
|
|
7662
|
+
(edge.isInCycle ? '\u21BB' : '\u2192') +
|
|
7663
|
+
'</div>' +
|
|
7664
|
+
'<div>' +
|
|
7665
|
+
'<div class="tooltip-title">Import Relationship</div>' +
|
|
7666
|
+
'<div class="tooltip-subtitle">' + escapeHtml(importType) + '</div>' +
|
|
7667
|
+
'</div>' +
|
|
7668
|
+
'</div>' +
|
|
7669
|
+
'<div class="tooltip-content">' +
|
|
7670
|
+
'<div class="tooltip-row">' +
|
|
7671
|
+
'<span class="tooltip-label">From</span>' +
|
|
7672
|
+
'<span class="tooltip-value">' + escapeHtml(sourceNode.name) + '</span>' +
|
|
7673
|
+
'</div>' +
|
|
7674
|
+
'<div class="tooltip-row">' +
|
|
7675
|
+
'<span class="tooltip-label">To</span>' +
|
|
7676
|
+
'<span class="tooltip-value">' + escapeHtml(targetNode.name) + '</span>' +
|
|
7677
|
+
'</div>' +
|
|
7678
|
+
'<div class="tooltip-divider"></div>' +
|
|
7679
|
+
'<div class="tooltip-row">' +
|
|
7680
|
+
'<span class="tooltip-label">Type</span>' +
|
|
7681
|
+
'<span class="tooltip-value">' + escapeHtml(edge.type) + '</span>' +
|
|
7682
|
+
'</div>' +
|
|
7683
|
+
(edge.isInCycle ? '<div class="tooltip-row"><span class="tooltip-label">Part of Cycle</span><span class="tooltip-value" style="color: #ef4444;">Yes</span></div>' : '') +
|
|
7684
|
+
(edge.cycleId ? '<div class="tooltip-row"><span class="tooltip-label">Cycle ID</span><span class="tooltip-value">' + escapeHtml(edge.cycleId) + '</span></div>' : '') +
|
|
7685
|
+
'</div>';
|
|
7686
|
+
|
|
7687
|
+
tooltip.html(html);
|
|
7688
|
+
|
|
7689
|
+
// Position tooltip
|
|
7690
|
+
const tooltipNode = tooltip.node();
|
|
7691
|
+
const tooltipRect = tooltipNode.getBoundingClientRect();
|
|
7692
|
+
const viewportWidth = window.innerWidth;
|
|
7693
|
+
const viewportHeight = window.innerHeight;
|
|
7694
|
+
|
|
7695
|
+
let left = event.clientX + 10;
|
|
7696
|
+
let top = event.clientY + 10;
|
|
7697
|
+
|
|
7698
|
+
// Adjust if tooltip would overflow
|
|
7699
|
+
if (left + tooltipRect.width > viewportWidth - 20) {
|
|
7700
|
+
left = event.clientX - tooltipRect.width - 10;
|
|
7701
|
+
}
|
|
7702
|
+
if (top + tooltipRect.height > viewportHeight - 20) {
|
|
7703
|
+
top = event.clientY - tooltipRect.height - 10;
|
|
7704
|
+
}
|
|
7705
|
+
|
|
7706
|
+
tooltip
|
|
7707
|
+
.style('left', left + 'px')
|
|
7708
|
+
.style('top', top + 'px')
|
|
7709
|
+
.classed('visible', true);
|
|
7710
|
+
}
|
|
7711
|
+
|
|
7712
|
+
function hideTooltip() {
|
|
7713
|
+
tooltip.classed('visible', false);
|
|
7714
|
+
}
|
|
7715
|
+
|
|
7716
|
+
// Highlighting
|
|
7717
|
+
function highlightConnectedNodes(node) {
|
|
7718
|
+
const connectedIds = new Set([node.id]);
|
|
7719
|
+
|
|
7720
|
+
// Find connected nodes
|
|
7721
|
+
simEdges.forEach(edge => {
|
|
7722
|
+
const sourceId = typeof edge.source === 'object' ? edge.source.id : edge.source;
|
|
7723
|
+
const targetId = typeof edge.target === 'object' ? edge.target.id : edge.target;
|
|
7724
|
+
if (sourceId === node.id) connectedIds.add(targetId);
|
|
7725
|
+
if (targetId === node.id) connectedIds.add(sourceId);
|
|
7726
|
+
});
|
|
7727
|
+
|
|
7728
|
+
state.highlightedNodes = connectedIds;
|
|
7729
|
+
|
|
7730
|
+
// Update visual state
|
|
7731
|
+
nodeGroup.selectAll('.graph-node')
|
|
7732
|
+
.classed('dimmed', d => !connectedIds.has(d.id))
|
|
7733
|
+
.classed('highlighted', d => connectedIds.has(d.id));
|
|
7734
|
+
|
|
7735
|
+
edgeGroup.selectAll('.graph-edge')
|
|
7736
|
+
.classed('dimmed', d => {
|
|
7737
|
+
const sourceId = typeof d.source === 'object' ? d.source.id : d.source;
|
|
7738
|
+
const targetId = typeof d.target === 'object' ? d.target.id : d.target;
|
|
7739
|
+
return sourceId !== node.id && targetId !== node.id;
|
|
7740
|
+
})
|
|
7741
|
+
.classed('highlighted', d => {
|
|
7742
|
+
const sourceId = typeof d.source === 'object' ? d.source.id : d.source;
|
|
7743
|
+
const targetId = typeof d.target === 'object' ? d.target.id : d.target;
|
|
7744
|
+
return sourceId === node.id || targetId === node.id;
|
|
7745
|
+
});
|
|
7746
|
+
}
|
|
7747
|
+
|
|
7748
|
+
function clearHighlights() {
|
|
7749
|
+
state.highlightedNodes.clear();
|
|
7750
|
+
nodeGroup.selectAll('.graph-node')
|
|
7751
|
+
.classed('dimmed', false)
|
|
7752
|
+
.classed('highlighted', false);
|
|
7753
|
+
edgeGroup.selectAll('.graph-edge')
|
|
7754
|
+
.classed('dimmed', false)
|
|
7755
|
+
.classed('highlighted', false);
|
|
7756
|
+
}
|
|
7757
|
+
|
|
7758
|
+
// Center on node
|
|
7759
|
+
function centerOnNode(node) {
|
|
7760
|
+
const scale = state.transform.k;
|
|
7761
|
+
const x = width / 2 - node.x * scale;
|
|
7762
|
+
const y = height / 2 - node.y * scale;
|
|
7763
|
+
svg.transition()
|
|
7764
|
+
.duration(500)
|
|
7765
|
+
.call(zoom.transform, d3.zoomIdentity.translate(x, y).scale(scale));
|
|
7766
|
+
}
|
|
7767
|
+
|
|
7768
|
+
// Zoom controls
|
|
7769
|
+
function zoomIn() {
|
|
7770
|
+
svg.transition().call(zoom.scaleBy, 1.3);
|
|
7771
|
+
}
|
|
7772
|
+
|
|
7773
|
+
function zoomOut() {
|
|
7774
|
+
svg.transition().call(zoom.scaleBy, 0.7);
|
|
7775
|
+
}
|
|
7776
|
+
|
|
7777
|
+
function zoomReset() {
|
|
7778
|
+
svg.transition().call(zoom.transform, d3.zoomIdentity);
|
|
7779
|
+
}
|
|
7780
|
+
|
|
7781
|
+
function updateZoomDisplay() {
|
|
7782
|
+
const percentage = Math.round(state.transform.k * 100);
|
|
7783
|
+
const zoomLevel = document.querySelector('.zoom-level');
|
|
7784
|
+
if (zoomLevel) {
|
|
7785
|
+
zoomLevel.textContent = percentage + '%';
|
|
7786
|
+
}
|
|
7787
|
+
}
|
|
7788
|
+
|
|
7789
|
+
// Filter controls
|
|
7790
|
+
function setLayerFilter(layer, enabled) {
|
|
7791
|
+
if (enabled) {
|
|
7792
|
+
state.filters.layers.add(layer);
|
|
7793
|
+
} else {
|
|
7794
|
+
state.filters.layers.delete(layer);
|
|
7795
|
+
}
|
|
7796
|
+
updateGraph();
|
|
7797
|
+
}
|
|
7798
|
+
|
|
7799
|
+
function setSeverityFilter(severity, enabled) {
|
|
7800
|
+
if (enabled) {
|
|
7801
|
+
state.filters.severities.add(severity);
|
|
7802
|
+
} else {
|
|
7803
|
+
state.filters.severities.delete(severity);
|
|
7804
|
+
}
|
|
7805
|
+
updateGraph();
|
|
7806
|
+
}
|
|
7807
|
+
|
|
7808
|
+
function setViewMode(mode) {
|
|
7809
|
+
state.filters.showCyclesOnly = mode === 'cycles';
|
|
7810
|
+
state.filters.showViolationsOnly = mode === 'violations';
|
|
7811
|
+
updateGraph();
|
|
7812
|
+
}
|
|
7813
|
+
|
|
7814
|
+
function setSearchQuery(query) {
|
|
7815
|
+
state.filters.searchQuery = query;
|
|
7816
|
+
updateGraph();
|
|
7817
|
+
}
|
|
7818
|
+
|
|
7819
|
+
// Update graph when filters change
|
|
7820
|
+
function updateGraph() {
|
|
7821
|
+
drawEdges();
|
|
7822
|
+
drawNodes();
|
|
7823
|
+
simulation.alpha(0.3).restart();
|
|
7824
|
+
}
|
|
7825
|
+
|
|
7826
|
+
// Simulation tick handler
|
|
7827
|
+
simulation.on('tick', () => {
|
|
7828
|
+
edgeGroup.selectAll('.graph-edge line')
|
|
7829
|
+
.attr('x1', d => d.source.x)
|
|
7830
|
+
.attr('y1', d => d.source.y)
|
|
7831
|
+
.attr('x2', d => d.target.x)
|
|
7832
|
+
.attr('y2', d => d.target.y);
|
|
7833
|
+
|
|
7834
|
+
nodeGroup.selectAll('.graph-node')
|
|
7835
|
+
.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
|
|
7836
|
+
});
|
|
7837
|
+
|
|
7838
|
+
// Helper functions
|
|
7839
|
+
function escapeHtml(str) {
|
|
7840
|
+
if (!str) return '';
|
|
7841
|
+
return str
|
|
7842
|
+
.replace(/&/g, '&')
|
|
7843
|
+
.replace(/</g, '<')
|
|
7844
|
+
.replace(/>/g, '>')
|
|
7845
|
+
.replace(/"/g, '"')
|
|
7846
|
+
.replace(/'/g, ''');
|
|
7847
|
+
}
|
|
7848
|
+
|
|
7849
|
+
// Initialize
|
|
7850
|
+
drawEdges();
|
|
7851
|
+
drawNodes();
|
|
7852
|
+
|
|
7853
|
+
// Update statistics display
|
|
7854
|
+
function updateStatistics() {
|
|
7855
|
+
const filtered = getFilteredNodes();
|
|
7856
|
+
const filteredEdges = getFilteredEdges();
|
|
7857
|
+
|
|
7858
|
+
document.getElementById('stat-nodes').textContent = filtered.length;
|
|
7859
|
+
document.getElementById('stat-edges').textContent = filteredEdges.length;
|
|
7860
|
+
document.getElementById('stat-cycles').textContent = cycles.length;
|
|
7861
|
+
|
|
7862
|
+
// Update severity counts
|
|
7863
|
+
const severityCounts = { critical: 0, error: 0, warning: 0, info: 0 };
|
|
7864
|
+
filtered.forEach(n => {
|
|
7865
|
+
n.violations.forEach(v => {
|
|
7866
|
+
severityCounts[v.severity]++;
|
|
7867
|
+
});
|
|
7868
|
+
});
|
|
7869
|
+
|
|
7870
|
+
Object.keys(severityCounts).forEach(severity => {
|
|
7871
|
+
const el = document.getElementById('stat-' + severity);
|
|
7872
|
+
if (el) el.textContent = severityCounts[severity];
|
|
7873
|
+
});
|
|
7874
|
+
}
|
|
7875
|
+
|
|
7876
|
+
updateStatistics();
|
|
7877
|
+
|
|
7878
|
+
// Expose API for control panel
|
|
7879
|
+
window.graphAPI = {
|
|
7880
|
+
zoomIn,
|
|
7881
|
+
zoomOut,
|
|
7882
|
+
zoomReset,
|
|
7883
|
+
setLayerFilter,
|
|
7884
|
+
setSeverityFilter,
|
|
7885
|
+
setViewMode,
|
|
7886
|
+
setSearchQuery,
|
|
7887
|
+
updateGraph,
|
|
7888
|
+
centerOnNode: (nodeId) => {
|
|
7889
|
+
const node = nodeById.get(nodeId);
|
|
7890
|
+
if (node) centerOnNode(node);
|
|
7891
|
+
}
|
|
7892
|
+
};
|
|
7893
|
+
|
|
7894
|
+
// Hide loading overlay
|
|
7895
|
+
const loadingOverlay = document.querySelector('.loading-overlay');
|
|
7896
|
+
if (loadingOverlay) {
|
|
7897
|
+
loadingOverlay.style.display = 'none';
|
|
7898
|
+
}
|
|
7899
|
+
|
|
7900
|
+
console.log('Dependency graph visualization initialized with', nodes.length, 'nodes and', edges.length, 'edges');
|
|
7901
|
+
})();
|
|
7902
|
+
`;
|
|
7903
|
+
}
|
|
7904
|
+
function generateControlPanelScript() {
|
|
7905
|
+
return `
|
|
7906
|
+
/**
|
|
7907
|
+
* Control Panel Script
|
|
7908
|
+
* Handles user interactions with filter controls
|
|
7909
|
+
*/
|
|
7910
|
+
(function() {
|
|
7911
|
+
'use strict';
|
|
7912
|
+
|
|
7913
|
+
// Wait for graph API to be available
|
|
7914
|
+
function waitForGraphAPI(callback) {
|
|
7915
|
+
if (window.graphAPI) {
|
|
7916
|
+
callback();
|
|
7917
|
+
} else {
|
|
7918
|
+
setTimeout(() => waitForGraphAPI(callback), 100);
|
|
7919
|
+
}
|
|
7920
|
+
}
|
|
7921
|
+
|
|
7922
|
+
waitForGraphAPI(() => {
|
|
7923
|
+
const api = window.graphAPI;
|
|
7924
|
+
|
|
7925
|
+
// Zoom controls
|
|
7926
|
+
document.getElementById('zoom-in')?.addEventListener('click', api.zoomIn);
|
|
7927
|
+
document.getElementById('zoom-out')?.addEventListener('click', api.zoomOut);
|
|
7928
|
+
document.getElementById('zoom-reset')?.addEventListener('click', api.zoomReset);
|
|
7929
|
+
|
|
7930
|
+
// View mode buttons
|
|
7931
|
+
document.querySelectorAll('.view-mode-btn').forEach(btn => {
|
|
7932
|
+
btn.addEventListener('click', () => {
|
|
7933
|
+
document.querySelectorAll('.view-mode-btn').forEach(b => b.classList.remove('active'));
|
|
7934
|
+
btn.classList.add('active');
|
|
7935
|
+
api.setViewMode(btn.dataset.mode);
|
|
7936
|
+
});
|
|
7937
|
+
});
|
|
7938
|
+
|
|
7939
|
+
// Search input
|
|
7940
|
+
const searchInput = document.getElementById('search-input');
|
|
7941
|
+
if (searchInput) {
|
|
7942
|
+
let debounceTimer;
|
|
7943
|
+
searchInput.addEventListener('input', (e) => {
|
|
7944
|
+
clearTimeout(debounceTimer);
|
|
7945
|
+
debounceTimer = setTimeout(() => {
|
|
7946
|
+
api.setSearchQuery(e.target.value);
|
|
7947
|
+
}, 300);
|
|
7948
|
+
});
|
|
7949
|
+
}
|
|
7950
|
+
|
|
7951
|
+
// Layer checkboxes
|
|
7952
|
+
document.querySelectorAll('.layer-checkbox').forEach(checkbox => {
|
|
7953
|
+
checkbox.addEventListener('change', (e) => {
|
|
7954
|
+
api.setLayerFilter(e.target.dataset.layer, e.target.checked);
|
|
7955
|
+
});
|
|
7956
|
+
});
|
|
7957
|
+
|
|
7958
|
+
// Severity checkboxes
|
|
7959
|
+
document.querySelectorAll('.severity-checkbox').forEach(checkbox => {
|
|
7960
|
+
checkbox.addEventListener('change', (e) => {
|
|
7961
|
+
api.setSeverityFilter(e.target.dataset.severity, e.target.checked);
|
|
7962
|
+
});
|
|
7963
|
+
});
|
|
7964
|
+
|
|
7965
|
+
// Keyboard shortcuts
|
|
7966
|
+
document.addEventListener('keydown', (e) => {
|
|
7967
|
+
// Only if not focused on input
|
|
7968
|
+
if (document.activeElement.tagName === 'INPUT') return;
|
|
7969
|
+
|
|
7970
|
+
switch (e.key) {
|
|
7971
|
+
case '+':
|
|
7972
|
+
case '=':
|
|
7973
|
+
api.zoomIn();
|
|
7974
|
+
break;
|
|
7975
|
+
case '-':
|
|
7976
|
+
case '_':
|
|
7977
|
+
api.zoomOut();
|
|
7978
|
+
break;
|
|
7979
|
+
case '0':
|
|
7980
|
+
api.zoomReset();
|
|
7981
|
+
break;
|
|
7982
|
+
case 'Escape':
|
|
7983
|
+
api.setSearchQuery('');
|
|
7984
|
+
if (searchInput) searchInput.value = '';
|
|
7985
|
+
break;
|
|
7986
|
+
}
|
|
7987
|
+
});
|
|
7988
|
+
|
|
7989
|
+
console.log('Control panel initialized');
|
|
7990
|
+
});
|
|
7991
|
+
})();
|
|
7992
|
+
`;
|
|
7993
|
+
}
|
|
7994
|
+
|
|
7995
|
+
// src/visualizer/templates/styles.ts
|
|
7996
|
+
var SEVERITY_COLORS = {
|
|
7997
|
+
critical: { primary: "#dc2626", background: "#fef2f2", text: "#991b1b" },
|
|
7998
|
+
error: { primary: "#ea580c", background: "#fff7ed", text: "#9a3412" },
|
|
7999
|
+
warning: { primary: "#ca8a04", background: "#fefce8", text: "#854d0e" },
|
|
8000
|
+
info: { primary: "#2563eb", background: "#eff6ff", text: "#1e40af" }
|
|
8001
|
+
};
|
|
8002
|
+
var LAYER_COLORS = {
|
|
8003
|
+
domain: "#8b5cf6",
|
|
8004
|
+
application: "#06b6d4",
|
|
8005
|
+
infrastructure: "#84cc16",
|
|
8006
|
+
presentation: "#f97316",
|
|
8007
|
+
shared: "#6b7280",
|
|
8008
|
+
unknown: "#9ca3af"
|
|
8009
|
+
};
|
|
8010
|
+
var CYCLE_COLORS = {
|
|
8011
|
+
edge: "#ef4444",
|
|
8012
|
+
node: "#fca5a5",
|
|
8013
|
+
glow: "rgba(239, 68, 68, 0.4)"
|
|
8014
|
+
};
|
|
8015
|
+
var CSS_VARIABLES = `
|
|
8016
|
+
:root {
|
|
8017
|
+
/* Layout */
|
|
8018
|
+
--sidebar-width: 280px;
|
|
8019
|
+
--control-panel-height: 60px;
|
|
8020
|
+
--tooltip-max-width: 360px;
|
|
8021
|
+
|
|
8022
|
+
/* Colors - Light theme */
|
|
8023
|
+
--bg-primary: #ffffff;
|
|
8024
|
+
--bg-secondary: #f9fafb;
|
|
8025
|
+
--bg-tertiary: #f3f4f6;
|
|
8026
|
+
--text-primary: #111827;
|
|
8027
|
+
--text-secondary: #4b5563;
|
|
8028
|
+
--text-muted: #9ca3af;
|
|
8029
|
+
--border-color: #e5e7eb;
|
|
8030
|
+
--focus-ring: #3b82f6;
|
|
8031
|
+
|
|
8032
|
+
/* Graph colors */
|
|
8033
|
+
--node-default: #6b7280;
|
|
8034
|
+
--node-hover: #374151;
|
|
8035
|
+
--edge-default: #d1d5db;
|
|
8036
|
+
--edge-hover: #9ca3af;
|
|
8037
|
+
|
|
8038
|
+
/* Severity colors */
|
|
8039
|
+
--severity-critical: ${SEVERITY_COLORS.critical.primary};
|
|
8040
|
+
--severity-critical-bg: ${SEVERITY_COLORS.critical.background};
|
|
8041
|
+
--severity-error: ${SEVERITY_COLORS.error.primary};
|
|
8042
|
+
--severity-error-bg: ${SEVERITY_COLORS.error.background};
|
|
8043
|
+
--severity-warning: ${SEVERITY_COLORS.warning.primary};
|
|
8044
|
+
--severity-warning-bg: ${SEVERITY_COLORS.warning.background};
|
|
8045
|
+
--severity-info: ${SEVERITY_COLORS.info.primary};
|
|
8046
|
+
--severity-info-bg: ${SEVERITY_COLORS.info.background};
|
|
8047
|
+
|
|
8048
|
+
/* Cycle colors */
|
|
8049
|
+
--cycle-edge: ${CYCLE_COLORS.edge};
|
|
8050
|
+
--cycle-node: ${CYCLE_COLORS.node};
|
|
8051
|
+
--cycle-glow: ${CYCLE_COLORS.glow};
|
|
8052
|
+
|
|
8053
|
+
/* Layer colors */
|
|
8054
|
+
--layer-domain: ${LAYER_COLORS.domain};
|
|
8055
|
+
--layer-application: ${LAYER_COLORS.application};
|
|
8056
|
+
--layer-infrastructure: ${LAYER_COLORS.infrastructure};
|
|
8057
|
+
--layer-presentation: ${LAYER_COLORS.presentation};
|
|
8058
|
+
--layer-shared: ${LAYER_COLORS.shared};
|
|
8059
|
+
--layer-unknown: ${LAYER_COLORS.unknown};
|
|
8060
|
+
|
|
8061
|
+
/* Shadows */
|
|
8062
|
+
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
8063
|
+
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
8064
|
+
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
8065
|
+
|
|
8066
|
+
/* Transitions */
|
|
8067
|
+
--transition-fast: 150ms ease;
|
|
8068
|
+
--transition-normal: 200ms ease;
|
|
8069
|
+
}
|
|
8070
|
+
`;
|
|
8071
|
+
var BASE_STYLES = `
|
|
8072
|
+
*, *::before, *::after {
|
|
8073
|
+
box-sizing: border-box;
|
|
8074
|
+
margin: 0;
|
|
8075
|
+
padding: 0;
|
|
8076
|
+
}
|
|
8077
|
+
|
|
8078
|
+
html, body {
|
|
8079
|
+
height: 100%;
|
|
8080
|
+
overflow: hidden;
|
|
8081
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
8082
|
+
font-size: 14px;
|
|
8083
|
+
line-height: 1.5;
|
|
8084
|
+
color: var(--text-primary);
|
|
8085
|
+
background-color: var(--bg-primary);
|
|
8086
|
+
-webkit-font-smoothing: antialiased;
|
|
8087
|
+
-moz-osx-font-smoothing: grayscale;
|
|
8088
|
+
}
|
|
8089
|
+
|
|
8090
|
+
button {
|
|
8091
|
+
font-family: inherit;
|
|
8092
|
+
font-size: inherit;
|
|
8093
|
+
cursor: pointer;
|
|
8094
|
+
border: none;
|
|
8095
|
+
background: none;
|
|
8096
|
+
}
|
|
8097
|
+
|
|
8098
|
+
button:focus-visible {
|
|
8099
|
+
outline: 2px solid var(--focus-ring);
|
|
8100
|
+
outline-offset: 2px;
|
|
8101
|
+
}
|
|
8102
|
+
|
|
8103
|
+
input {
|
|
8104
|
+
font-family: inherit;
|
|
8105
|
+
font-size: inherit;
|
|
8106
|
+
}
|
|
8107
|
+
|
|
8108
|
+
input:focus-visible {
|
|
8109
|
+
outline: 2px solid var(--focus-ring);
|
|
8110
|
+
outline-offset: 1px;
|
|
8111
|
+
}
|
|
8112
|
+
`;
|
|
8113
|
+
var LAYOUT_STYLES = `
|
|
8114
|
+
.app-container {
|
|
8115
|
+
display: flex;
|
|
8116
|
+
height: 100vh;
|
|
8117
|
+
width: 100vw;
|
|
8118
|
+
overflow: hidden;
|
|
8119
|
+
}
|
|
8120
|
+
|
|
8121
|
+
.sidebar {
|
|
8122
|
+
width: var(--sidebar-width);
|
|
8123
|
+
min-width: var(--sidebar-width);
|
|
8124
|
+
height: 100%;
|
|
8125
|
+
background-color: var(--bg-secondary);
|
|
8126
|
+
border-right: 1px solid var(--border-color);
|
|
8127
|
+
display: flex;
|
|
8128
|
+
flex-direction: column;
|
|
8129
|
+
overflow: hidden;
|
|
8130
|
+
}
|
|
8131
|
+
|
|
8132
|
+
.sidebar-header {
|
|
8133
|
+
padding: 16px;
|
|
8134
|
+
border-bottom: 1px solid var(--border-color);
|
|
8135
|
+
background-color: var(--bg-primary);
|
|
8136
|
+
}
|
|
8137
|
+
|
|
8138
|
+
.sidebar-title {
|
|
8139
|
+
font-size: 16px;
|
|
8140
|
+
font-weight: 600;
|
|
8141
|
+
color: var(--text-primary);
|
|
8142
|
+
margin-bottom: 4px;
|
|
8143
|
+
}
|
|
8144
|
+
|
|
8145
|
+
.sidebar-subtitle {
|
|
8146
|
+
font-size: 12px;
|
|
8147
|
+
color: var(--text-muted);
|
|
8148
|
+
}
|
|
8149
|
+
|
|
8150
|
+
.sidebar-content {
|
|
8151
|
+
flex: 1;
|
|
8152
|
+
overflow-y: auto;
|
|
8153
|
+
padding: 16px;
|
|
8154
|
+
}
|
|
8155
|
+
|
|
8156
|
+
.main-content {
|
|
8157
|
+
flex: 1;
|
|
8158
|
+
display: flex;
|
|
8159
|
+
flex-direction: column;
|
|
8160
|
+
overflow: hidden;
|
|
8161
|
+
position: relative;
|
|
8162
|
+
}
|
|
8163
|
+
|
|
8164
|
+
.control-panel {
|
|
8165
|
+
height: var(--control-panel-height);
|
|
8166
|
+
min-height: var(--control-panel-height);
|
|
8167
|
+
padding: 12px 16px;
|
|
8168
|
+
background-color: var(--bg-primary);
|
|
8169
|
+
border-bottom: 1px solid var(--border-color);
|
|
8170
|
+
display: flex;
|
|
8171
|
+
align-items: center;
|
|
8172
|
+
gap: 16px;
|
|
8173
|
+
}
|
|
8174
|
+
|
|
8175
|
+
.graph-container {
|
|
8176
|
+
flex: 1;
|
|
8177
|
+
position: relative;
|
|
8178
|
+
overflow: hidden;
|
|
8179
|
+
background-color: var(--bg-tertiary);
|
|
8180
|
+
}
|
|
8181
|
+
|
|
8182
|
+
.graph-canvas {
|
|
8183
|
+
width: 100%;
|
|
8184
|
+
height: 100%;
|
|
8185
|
+
display: block;
|
|
8186
|
+
}
|
|
8187
|
+
`;
|
|
8188
|
+
var CONTROL_STYLES = `
|
|
8189
|
+
.control-group {
|
|
8190
|
+
display: flex;
|
|
8191
|
+
align-items: center;
|
|
8192
|
+
gap: 8px;
|
|
8193
|
+
}
|
|
8194
|
+
|
|
8195
|
+
.control-label {
|
|
8196
|
+
font-size: 12px;
|
|
8197
|
+
font-weight: 500;
|
|
8198
|
+
color: var(--text-secondary);
|
|
8199
|
+
white-space: nowrap;
|
|
8200
|
+
}
|
|
8201
|
+
|
|
8202
|
+
.control-select {
|
|
8203
|
+
padding: 6px 28px 6px 10px;
|
|
8204
|
+
border: 1px solid var(--border-color);
|
|
8205
|
+
border-radius: 6px;
|
|
8206
|
+
background-color: var(--bg-primary);
|
|
8207
|
+
color: var(--text-primary);
|
|
8208
|
+
font-size: 13px;
|
|
8209
|
+
appearance: none;
|
|
8210
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M3 4.5L6 7.5L9 4.5'/%3E%3C/svg%3E");
|
|
8211
|
+
background-repeat: no-repeat;
|
|
8212
|
+
background-position: right 8px center;
|
|
8213
|
+
cursor: pointer;
|
|
8214
|
+
transition: border-color var(--transition-fast);
|
|
8215
|
+
}
|
|
8216
|
+
|
|
8217
|
+
.control-select:hover {
|
|
8218
|
+
border-color: var(--text-muted);
|
|
8219
|
+
}
|
|
8220
|
+
|
|
8221
|
+
.control-input {
|
|
8222
|
+
padding: 6px 10px;
|
|
8223
|
+
border: 1px solid var(--border-color);
|
|
8224
|
+
border-radius: 6px;
|
|
8225
|
+
background-color: var(--bg-primary);
|
|
8226
|
+
color: var(--text-primary);
|
|
8227
|
+
font-size: 13px;
|
|
8228
|
+
width: 180px;
|
|
8229
|
+
transition: border-color var(--transition-fast);
|
|
8230
|
+
}
|
|
8231
|
+
|
|
8232
|
+
.control-input::placeholder {
|
|
8233
|
+
color: var(--text-muted);
|
|
8234
|
+
}
|
|
8235
|
+
|
|
8236
|
+
.control-input:hover {
|
|
8237
|
+
border-color: var(--text-muted);
|
|
8238
|
+
}
|
|
8239
|
+
|
|
8240
|
+
.control-btn {
|
|
8241
|
+
display: inline-flex;
|
|
8242
|
+
align-items: center;
|
|
8243
|
+
justify-content: center;
|
|
8244
|
+
padding: 6px 12px;
|
|
8245
|
+
border: 1px solid var(--border-color);
|
|
8246
|
+
border-radius: 6px;
|
|
8247
|
+
background-color: var(--bg-primary);
|
|
8248
|
+
color: var(--text-primary);
|
|
8249
|
+
font-size: 13px;
|
|
8250
|
+
font-weight: 500;
|
|
8251
|
+
transition: all var(--transition-fast);
|
|
8252
|
+
}
|
|
8253
|
+
|
|
8254
|
+
.control-btn:hover {
|
|
8255
|
+
background-color: var(--bg-secondary);
|
|
8256
|
+
border-color: var(--text-muted);
|
|
8257
|
+
}
|
|
8258
|
+
|
|
8259
|
+
.control-btn.active {
|
|
8260
|
+
background-color: var(--focus-ring);
|
|
8261
|
+
border-color: var(--focus-ring);
|
|
8262
|
+
color: white;
|
|
8263
|
+
}
|
|
8264
|
+
|
|
8265
|
+
.control-btn-group {
|
|
8266
|
+
display: flex;
|
|
8267
|
+
border: 1px solid var(--border-color);
|
|
8268
|
+
border-radius: 6px;
|
|
8269
|
+
overflow: hidden;
|
|
8270
|
+
}
|
|
8271
|
+
|
|
8272
|
+
.control-btn-group .control-btn {
|
|
8273
|
+
border: none;
|
|
8274
|
+
border-radius: 0;
|
|
8275
|
+
border-right: 1px solid var(--border-color);
|
|
8276
|
+
}
|
|
8277
|
+
|
|
8278
|
+
.control-btn-group .control-btn:last-child {
|
|
8279
|
+
border-right: none;
|
|
8280
|
+
}
|
|
8281
|
+
|
|
8282
|
+
.control-spacer {
|
|
8283
|
+
flex: 1;
|
|
8284
|
+
}
|
|
8285
|
+
|
|
8286
|
+
.zoom-controls {
|
|
8287
|
+
display: flex;
|
|
8288
|
+
align-items: center;
|
|
8289
|
+
gap: 4px;
|
|
8290
|
+
}
|
|
8291
|
+
|
|
8292
|
+
.zoom-btn {
|
|
8293
|
+
width: 32px;
|
|
8294
|
+
height: 32px;
|
|
8295
|
+
display: inline-flex;
|
|
8296
|
+
align-items: center;
|
|
8297
|
+
justify-content: center;
|
|
8298
|
+
border: 1px solid var(--border-color);
|
|
8299
|
+
border-radius: 6px;
|
|
8300
|
+
background-color: var(--bg-primary);
|
|
8301
|
+
color: var(--text-primary);
|
|
8302
|
+
font-size: 16px;
|
|
8303
|
+
font-weight: 500;
|
|
8304
|
+
transition: all var(--transition-fast);
|
|
8305
|
+
}
|
|
8306
|
+
|
|
8307
|
+
.zoom-btn:hover {
|
|
8308
|
+
background-color: var(--bg-secondary);
|
|
8309
|
+
}
|
|
8310
|
+
|
|
8311
|
+
.zoom-level {
|
|
8312
|
+
font-size: 12px;
|
|
8313
|
+
color: var(--text-secondary);
|
|
8314
|
+
min-width: 48px;
|
|
8315
|
+
text-align: center;
|
|
8316
|
+
}
|
|
8317
|
+
`;
|
|
8318
|
+
var SIDEBAR_SECTION_STYLES = `
|
|
8319
|
+
.sidebar-section {
|
|
8320
|
+
margin-bottom: 20px;
|
|
8321
|
+
}
|
|
8322
|
+
|
|
8323
|
+
.sidebar-section:last-child {
|
|
8324
|
+
margin-bottom: 0;
|
|
8325
|
+
}
|
|
8326
|
+
|
|
8327
|
+
.section-title {
|
|
8328
|
+
font-size: 11px;
|
|
8329
|
+
font-weight: 600;
|
|
8330
|
+
text-transform: uppercase;
|
|
8331
|
+
letter-spacing: 0.5px;
|
|
8332
|
+
color: var(--text-muted);
|
|
8333
|
+
margin-bottom: 10px;
|
|
8334
|
+
}
|
|
8335
|
+
|
|
8336
|
+
.stats-grid {
|
|
8337
|
+
display: grid;
|
|
8338
|
+
grid-template-columns: repeat(2, 1fr);
|
|
8339
|
+
gap: 8px;
|
|
8340
|
+
}
|
|
8341
|
+
|
|
8342
|
+
.stat-card {
|
|
8343
|
+
padding: 10px 12px;
|
|
8344
|
+
background-color: var(--bg-primary);
|
|
8345
|
+
border-radius: 8px;
|
|
8346
|
+
border: 1px solid var(--border-color);
|
|
8347
|
+
}
|
|
8348
|
+
|
|
8349
|
+
.stat-value {
|
|
8350
|
+
font-size: 20px;
|
|
8351
|
+
font-weight: 600;
|
|
8352
|
+
color: var(--text-primary);
|
|
8353
|
+
line-height: 1.2;
|
|
8354
|
+
}
|
|
8355
|
+
|
|
8356
|
+
.stat-label {
|
|
8357
|
+
font-size: 11px;
|
|
8358
|
+
color: var(--text-muted);
|
|
8359
|
+
margin-top: 2px;
|
|
8360
|
+
}
|
|
8361
|
+
|
|
8362
|
+
.stat-card.span-full {
|
|
8363
|
+
grid-column: span 2;
|
|
8364
|
+
}
|
|
8365
|
+
|
|
8366
|
+
.severity-list {
|
|
8367
|
+
display: flex;
|
|
8368
|
+
flex-direction: column;
|
|
8369
|
+
gap: 6px;
|
|
8370
|
+
}
|
|
8371
|
+
|
|
8372
|
+
.severity-item {
|
|
8373
|
+
display: flex;
|
|
8374
|
+
align-items: center;
|
|
8375
|
+
justify-content: space-between;
|
|
8376
|
+
padding: 8px 10px;
|
|
8377
|
+
background-color: var(--bg-primary);
|
|
8378
|
+
border-radius: 6px;
|
|
8379
|
+
border: 1px solid var(--border-color);
|
|
8380
|
+
}
|
|
8381
|
+
|
|
8382
|
+
.severity-indicator {
|
|
8383
|
+
display: flex;
|
|
8384
|
+
align-items: center;
|
|
8385
|
+
gap: 8px;
|
|
8386
|
+
}
|
|
8387
|
+
|
|
8388
|
+
.severity-dot {
|
|
8389
|
+
width: 10px;
|
|
8390
|
+
height: 10px;
|
|
8391
|
+
border-radius: 50%;
|
|
8392
|
+
}
|
|
8393
|
+
|
|
8394
|
+
.severity-dot.critical { background-color: var(--severity-critical); }
|
|
8395
|
+
.severity-dot.error { background-color: var(--severity-error); }
|
|
8396
|
+
.severity-dot.warning { background-color: var(--severity-warning); }
|
|
8397
|
+
.severity-dot.info { background-color: var(--severity-info); }
|
|
8398
|
+
|
|
8399
|
+
.severity-name {
|
|
8400
|
+
font-size: 13px;
|
|
8401
|
+
color: var(--text-primary);
|
|
8402
|
+
text-transform: capitalize;
|
|
8403
|
+
}
|
|
8404
|
+
|
|
8405
|
+
.severity-count {
|
|
8406
|
+
font-size: 13px;
|
|
8407
|
+
font-weight: 600;
|
|
8408
|
+
color: var(--text-primary);
|
|
8409
|
+
}
|
|
8410
|
+
|
|
8411
|
+
.severity-filter-list {
|
|
8412
|
+
display: flex;
|
|
8413
|
+
flex-direction: column;
|
|
8414
|
+
gap: 6px;
|
|
8415
|
+
}
|
|
8416
|
+
|
|
8417
|
+
.severity-filter-item {
|
|
8418
|
+
display: flex;
|
|
8419
|
+
align-items: center;
|
|
8420
|
+
gap: 8px;
|
|
8421
|
+
padding: 6px 0;
|
|
8422
|
+
cursor: pointer;
|
|
8423
|
+
}
|
|
8424
|
+
|
|
8425
|
+
.severity-filter-item:hover {
|
|
8426
|
+
opacity: 0.8;
|
|
8427
|
+
}
|
|
8428
|
+
|
|
8429
|
+
.severity-checkbox {
|
|
8430
|
+
width: 16px;
|
|
8431
|
+
height: 16px;
|
|
8432
|
+
cursor: pointer;
|
|
8433
|
+
accent-color: var(--focus-ring);
|
|
8434
|
+
}
|
|
8435
|
+
|
|
8436
|
+
.layer-list {
|
|
8437
|
+
display: flex;
|
|
8438
|
+
flex-direction: column;
|
|
8439
|
+
gap: 6px;
|
|
8440
|
+
}
|
|
8441
|
+
|
|
8442
|
+
.layer-item {
|
|
8443
|
+
display: flex;
|
|
8444
|
+
align-items: center;
|
|
8445
|
+
gap: 8px;
|
|
8446
|
+
padding: 6px 0;
|
|
8447
|
+
}
|
|
8448
|
+
|
|
8449
|
+
.layer-checkbox {
|
|
8450
|
+
width: 16px;
|
|
8451
|
+
height: 16px;
|
|
8452
|
+
cursor: pointer;
|
|
8453
|
+
accent-color: var(--focus-ring);
|
|
8454
|
+
}
|
|
8455
|
+
|
|
8456
|
+
.layer-color {
|
|
8457
|
+
width: 12px;
|
|
8458
|
+
height: 12px;
|
|
8459
|
+
border-radius: 3px;
|
|
8460
|
+
}
|
|
8461
|
+
|
|
8462
|
+
.layer-color.domain { background-color: var(--layer-domain); }
|
|
8463
|
+
.layer-color.application { background-color: var(--layer-application); }
|
|
8464
|
+
.layer-color.infrastructure { background-color: var(--layer-infrastructure); }
|
|
8465
|
+
.layer-color.presentation { background-color: var(--layer-presentation); }
|
|
8466
|
+
.layer-color.shared { background-color: var(--layer-shared); }
|
|
8467
|
+
.layer-color.unknown { background-color: var(--layer-unknown); }
|
|
8468
|
+
|
|
8469
|
+
.layer-name {
|
|
8470
|
+
font-size: 13px;
|
|
8471
|
+
color: var(--text-primary);
|
|
8472
|
+
text-transform: capitalize;
|
|
8473
|
+
flex: 1;
|
|
8474
|
+
}
|
|
8475
|
+
|
|
8476
|
+
.layer-count {
|
|
8477
|
+
font-size: 12px;
|
|
8478
|
+
color: var(--text-muted);
|
|
8479
|
+
}
|
|
8480
|
+
|
|
8481
|
+
.legend-section {
|
|
8482
|
+
padding-top: 16px;
|
|
8483
|
+
border-top: 1px solid var(--border-color);
|
|
8484
|
+
margin-top: 16px;
|
|
8485
|
+
}
|
|
8486
|
+
|
|
8487
|
+
.legend-item {
|
|
8488
|
+
display: flex;
|
|
8489
|
+
align-items: center;
|
|
8490
|
+
gap: 8px;
|
|
8491
|
+
padding: 4px 0;
|
|
8492
|
+
}
|
|
8493
|
+
|
|
8494
|
+
.legend-icon {
|
|
8495
|
+
width: 20px;
|
|
8496
|
+
height: 20px;
|
|
8497
|
+
display: flex;
|
|
8498
|
+
align-items: center;
|
|
8499
|
+
justify-content: center;
|
|
8500
|
+
}
|
|
8501
|
+
|
|
8502
|
+
.legend-icon svg {
|
|
8503
|
+
width: 16px;
|
|
8504
|
+
height: 16px;
|
|
8505
|
+
}
|
|
8506
|
+
|
|
8507
|
+
.legend-text {
|
|
8508
|
+
font-size: 12px;
|
|
8509
|
+
color: var(--text-secondary);
|
|
8510
|
+
}
|
|
8511
|
+
`;
|
|
8512
|
+
var TOOLTIP_STYLES = `
|
|
8513
|
+
.tooltip {
|
|
8514
|
+
position: fixed;
|
|
8515
|
+
max-width: var(--tooltip-max-width);
|
|
8516
|
+
padding: 12px 14px;
|
|
8517
|
+
background-color: var(--bg-primary);
|
|
8518
|
+
border: 1px solid var(--border-color);
|
|
8519
|
+
border-radius: 8px;
|
|
8520
|
+
box-shadow: var(--shadow-lg);
|
|
8521
|
+
pointer-events: none;
|
|
8522
|
+
z-index: 1000;
|
|
8523
|
+
opacity: 0;
|
|
8524
|
+
transform: translateY(4px);
|
|
8525
|
+
transition: opacity var(--transition-fast), transform var(--transition-fast);
|
|
8526
|
+
}
|
|
8527
|
+
|
|
8528
|
+
.tooltip.visible {
|
|
8529
|
+
opacity: 1;
|
|
8530
|
+
transform: translateY(0);
|
|
8531
|
+
}
|
|
8532
|
+
|
|
8533
|
+
.tooltip-header {
|
|
8534
|
+
display: flex;
|
|
8535
|
+
align-items: flex-start;
|
|
8536
|
+
gap: 8px;
|
|
8537
|
+
margin-bottom: 8px;
|
|
8538
|
+
}
|
|
8539
|
+
|
|
8540
|
+
.tooltip-icon {
|
|
8541
|
+
width: 24px;
|
|
8542
|
+
height: 24px;
|
|
8543
|
+
border-radius: 6px;
|
|
8544
|
+
display: flex;
|
|
8545
|
+
align-items: center;
|
|
8546
|
+
justify-content: center;
|
|
8547
|
+
flex-shrink: 0;
|
|
8548
|
+
}
|
|
8549
|
+
|
|
8550
|
+
.tooltip-icon.severity-critical { background-color: var(--severity-critical-bg); color: var(--severity-critical); }
|
|
8551
|
+
.tooltip-icon.severity-error { background-color: var(--severity-error-bg); color: var(--severity-error); }
|
|
8552
|
+
.tooltip-icon.severity-warning { background-color: var(--severity-warning-bg); color: var(--severity-warning); }
|
|
8553
|
+
.tooltip-icon.severity-info { background-color: var(--severity-info-bg); color: var(--severity-info); }
|
|
8554
|
+
.tooltip-icon.cycle { background-color: #fef2f2; color: var(--cycle-edge); }
|
|
8555
|
+
.tooltip-icon.default { background-color: var(--bg-tertiary); color: var(--text-secondary); }
|
|
8556
|
+
|
|
8557
|
+
.tooltip-title {
|
|
8558
|
+
font-size: 13px;
|
|
8559
|
+
font-weight: 600;
|
|
8560
|
+
color: var(--text-primary);
|
|
8561
|
+
word-break: break-word;
|
|
8562
|
+
}
|
|
8563
|
+
|
|
8564
|
+
.tooltip-subtitle {
|
|
8565
|
+
font-size: 11px;
|
|
8566
|
+
color: var(--text-muted);
|
|
8567
|
+
margin-top: 2px;
|
|
8568
|
+
}
|
|
8569
|
+
|
|
8570
|
+
.tooltip-content {
|
|
8571
|
+
display: flex;
|
|
8572
|
+
flex-direction: column;
|
|
8573
|
+
gap: 6px;
|
|
8574
|
+
}
|
|
8575
|
+
|
|
8576
|
+
.tooltip-row {
|
|
8577
|
+
display: flex;
|
|
8578
|
+
justify-content: space-between;
|
|
8579
|
+
align-items: center;
|
|
8580
|
+
gap: 12px;
|
|
8581
|
+
}
|
|
8582
|
+
|
|
8583
|
+
.tooltip-label {
|
|
8584
|
+
font-size: 12px;
|
|
8585
|
+
color: var(--text-muted);
|
|
8586
|
+
}
|
|
8587
|
+
|
|
8588
|
+
.tooltip-value {
|
|
8589
|
+
font-size: 12px;
|
|
8590
|
+
font-weight: 500;
|
|
8591
|
+
color: var(--text-primary);
|
|
8592
|
+
}
|
|
8593
|
+
|
|
8594
|
+
.tooltip-divider {
|
|
8595
|
+
height: 1px;
|
|
8596
|
+
background-color: var(--border-color);
|
|
8597
|
+
margin: 6px 0;
|
|
8598
|
+
}
|
|
8599
|
+
|
|
8600
|
+
.tooltip-violations {
|
|
8601
|
+
margin-top: 8px;
|
|
8602
|
+
}
|
|
8603
|
+
|
|
8604
|
+
.tooltip-violations-title {
|
|
8605
|
+
font-size: 11px;
|
|
8606
|
+
font-weight: 600;
|
|
8607
|
+
color: var(--text-muted);
|
|
8608
|
+
margin-bottom: 6px;
|
|
8609
|
+
text-transform: uppercase;
|
|
8610
|
+
letter-spacing: 0.5px;
|
|
8611
|
+
}
|
|
8612
|
+
|
|
8613
|
+
.tooltip-violation {
|
|
8614
|
+
display: flex;
|
|
8615
|
+
align-items: flex-start;
|
|
8616
|
+
gap: 6px;
|
|
8617
|
+
padding: 6px 8px;
|
|
8618
|
+
background-color: var(--bg-secondary);
|
|
8619
|
+
border-radius: 4px;
|
|
8620
|
+
margin-bottom: 4px;
|
|
8621
|
+
}
|
|
8622
|
+
|
|
8623
|
+
.tooltip-violation:last-child {
|
|
8624
|
+
margin-bottom: 0;
|
|
8625
|
+
}
|
|
8626
|
+
|
|
8627
|
+
.tooltip-violation-dot {
|
|
8628
|
+
width: 6px;
|
|
8629
|
+
height: 6px;
|
|
8630
|
+
border-radius: 50%;
|
|
8631
|
+
margin-top: 5px;
|
|
8632
|
+
flex-shrink: 0;
|
|
8633
|
+
}
|
|
8634
|
+
|
|
8635
|
+
.tooltip-violation-text {
|
|
8636
|
+
font-size: 11px;
|
|
8637
|
+
color: var(--text-primary);
|
|
8638
|
+
line-height: 1.4;
|
|
8639
|
+
}
|
|
8640
|
+
`;
|
|
8641
|
+
var GRAPH_ELEMENT_STYLES = `
|
|
8642
|
+
.graph-node {
|
|
8643
|
+
cursor: pointer;
|
|
8644
|
+
transition: opacity var(--transition-fast);
|
|
8645
|
+
}
|
|
8646
|
+
|
|
8647
|
+
.graph-node:hover {
|
|
8648
|
+
opacity: 0.9;
|
|
8649
|
+
}
|
|
8650
|
+
|
|
8651
|
+
.graph-node.dimmed {
|
|
8652
|
+
opacity: 0.2;
|
|
8653
|
+
}
|
|
8654
|
+
|
|
8655
|
+
.graph-node.highlighted {
|
|
8656
|
+
opacity: 1;
|
|
8657
|
+
}
|
|
8658
|
+
|
|
8659
|
+
.node-circle {
|
|
8660
|
+
stroke: var(--bg-primary);
|
|
8661
|
+
stroke-width: 2px;
|
|
8662
|
+
transition: all var(--transition-fast);
|
|
8663
|
+
}
|
|
8664
|
+
|
|
8665
|
+
.node-circle.cycle {
|
|
8666
|
+
stroke: var(--cycle-edge);
|
|
8667
|
+
stroke-width: 3px;
|
|
8668
|
+
filter: drop-shadow(0 0 4px var(--cycle-glow));
|
|
8669
|
+
}
|
|
8670
|
+
|
|
8671
|
+
.node-circle.severity-critical { fill: var(--severity-critical); }
|
|
8672
|
+
.node-circle.severity-error { fill: var(--severity-error); }
|
|
8673
|
+
.node-circle.severity-warning { fill: var(--severity-warning); }
|
|
8674
|
+
.node-circle.severity-info { fill: var(--severity-info); }
|
|
8675
|
+
|
|
8676
|
+
.node-circle.layer-domain { fill: var(--layer-domain); }
|
|
8677
|
+
.node-circle.layer-application { fill: var(--layer-application); }
|
|
8678
|
+
.node-circle.layer-infrastructure { fill: var(--layer-infrastructure); }
|
|
8679
|
+
.node-circle.layer-presentation { fill: var(--layer-presentation); }
|
|
8680
|
+
.node-circle.layer-shared { fill: var(--layer-shared); }
|
|
8681
|
+
.node-circle.layer-unknown, .node-circle.default { fill: var(--node-default); }
|
|
8682
|
+
|
|
8683
|
+
.node-label {
|
|
8684
|
+
font-size: 10px;
|
|
8685
|
+
fill: var(--text-primary);
|
|
8686
|
+
pointer-events: none;
|
|
8687
|
+
text-anchor: middle;
|
|
8688
|
+
dominant-baseline: central;
|
|
8689
|
+
font-weight: 500;
|
|
8690
|
+
}
|
|
8691
|
+
|
|
8692
|
+
.graph-edge {
|
|
8693
|
+
transition: opacity var(--transition-fast);
|
|
8694
|
+
}
|
|
8695
|
+
|
|
8696
|
+
.graph-edge.dimmed {
|
|
8697
|
+
opacity: 0.1;
|
|
8698
|
+
}
|
|
8699
|
+
|
|
8700
|
+
.graph-edge.highlighted {
|
|
8701
|
+
opacity: 1;
|
|
8702
|
+
}
|
|
8703
|
+
|
|
8704
|
+
.edge-line {
|
|
8705
|
+
fill: none;
|
|
8706
|
+
stroke: var(--edge-default);
|
|
8707
|
+
stroke-width: 1.5px;
|
|
8708
|
+
transition: stroke var(--transition-fast);
|
|
8709
|
+
}
|
|
8710
|
+
|
|
8711
|
+
.edge-line.cycle {
|
|
8712
|
+
stroke: var(--cycle-edge);
|
|
8713
|
+
stroke-width: 2px;
|
|
8714
|
+
animation: pulse-edge 1.5s ease-in-out infinite;
|
|
8715
|
+
}
|
|
8716
|
+
|
|
8717
|
+
.edge-line.type-static { stroke-dasharray: none; }
|
|
8718
|
+
.edge-line.type-dynamic { stroke-dasharray: 4, 4; }
|
|
8719
|
+
.edge-line.type-type-only { stroke-dasharray: 2, 2; opacity: 0.6; }
|
|
8720
|
+
.edge-line.type-require { stroke-dasharray: 6, 2; }
|
|
8721
|
+
|
|
8722
|
+
.edge-arrow {
|
|
8723
|
+
fill: var(--edge-default);
|
|
8724
|
+
transition: fill var(--transition-fast);
|
|
8725
|
+
}
|
|
8726
|
+
|
|
8727
|
+
.edge-arrow.cycle {
|
|
8728
|
+
fill: var(--cycle-edge);
|
|
8729
|
+
}
|
|
8730
|
+
|
|
8731
|
+
@keyframes pulse-edge {
|
|
8732
|
+
0%, 100% {
|
|
8733
|
+
opacity: 1;
|
|
8734
|
+
}
|
|
8735
|
+
50% {
|
|
8736
|
+
opacity: 0.5;
|
|
8737
|
+
}
|
|
8738
|
+
}
|
|
8739
|
+
|
|
8740
|
+
.cycle-glow {
|
|
8741
|
+
fill: none;
|
|
8742
|
+
stroke: var(--cycle-glow);
|
|
8743
|
+
stroke-width: 8px;
|
|
8744
|
+
opacity: 0.5;
|
|
8745
|
+
animation: pulse-glow 2s ease-in-out infinite;
|
|
8746
|
+
}
|
|
8747
|
+
|
|
8748
|
+
@keyframes pulse-glow {
|
|
8749
|
+
0%, 100% {
|
|
8750
|
+
opacity: 0.3;
|
|
8751
|
+
}
|
|
8752
|
+
50% {
|
|
8753
|
+
opacity: 0.6;
|
|
8754
|
+
}
|
|
8755
|
+
}
|
|
8756
|
+
`;
|
|
8757
|
+
var STATE_STYLES = `
|
|
8758
|
+
.loading-overlay {
|
|
8759
|
+
position: absolute;
|
|
8760
|
+
inset: 0;
|
|
8761
|
+
display: flex;
|
|
8762
|
+
align-items: center;
|
|
8763
|
+
justify-content: center;
|
|
8764
|
+
background-color: var(--bg-tertiary);
|
|
8765
|
+
z-index: 100;
|
|
8766
|
+
}
|
|
8767
|
+
|
|
8768
|
+
.loading-spinner {
|
|
8769
|
+
width: 40px;
|
|
8770
|
+
height: 40px;
|
|
8771
|
+
border: 3px solid var(--border-color);
|
|
8772
|
+
border-top-color: var(--focus-ring);
|
|
8773
|
+
border-radius: 50%;
|
|
8774
|
+
animation: spin 1s linear infinite;
|
|
8775
|
+
}
|
|
8776
|
+
|
|
8777
|
+
@keyframes spin {
|
|
8778
|
+
to {
|
|
8779
|
+
transform: rotate(360deg);
|
|
8780
|
+
}
|
|
8781
|
+
}
|
|
8782
|
+
|
|
8783
|
+
.empty-state {
|
|
8784
|
+
position: absolute;
|
|
8785
|
+
inset: 0;
|
|
8786
|
+
display: flex;
|
|
8787
|
+
flex-direction: column;
|
|
8788
|
+
align-items: center;
|
|
8789
|
+
justify-content: center;
|
|
8790
|
+
color: var(--text-muted);
|
|
8791
|
+
padding: 24px;
|
|
8792
|
+
text-align: center;
|
|
8793
|
+
}
|
|
8794
|
+
|
|
8795
|
+
.empty-state-icon {
|
|
8796
|
+
width: 64px;
|
|
8797
|
+
height: 64px;
|
|
8798
|
+
margin-bottom: 16px;
|
|
8799
|
+
opacity: 0.5;
|
|
8800
|
+
}
|
|
8801
|
+
|
|
8802
|
+
.empty-state-title {
|
|
8803
|
+
font-size: 16px;
|
|
8804
|
+
font-weight: 600;
|
|
8805
|
+
color: var(--text-secondary);
|
|
8806
|
+
margin-bottom: 8px;
|
|
8807
|
+
}
|
|
8808
|
+
|
|
8809
|
+
.empty-state-text {
|
|
8810
|
+
font-size: 14px;
|
|
8811
|
+
max-width: 300px;
|
|
8812
|
+
}
|
|
8813
|
+
`;
|
|
8814
|
+
function generateStyles() {
|
|
8815
|
+
return [
|
|
8816
|
+
CSS_VARIABLES,
|
|
8817
|
+
BASE_STYLES,
|
|
8818
|
+
LAYOUT_STYLES,
|
|
8819
|
+
CONTROL_STYLES,
|
|
8820
|
+
SIDEBAR_SECTION_STYLES,
|
|
8821
|
+
TOOLTIP_STYLES,
|
|
8822
|
+
GRAPH_ELEMENT_STYLES,
|
|
8823
|
+
STATE_STYLES
|
|
8824
|
+
].join("\n");
|
|
8825
|
+
}
|
|
8826
|
+
|
|
8827
|
+
// src/visualizer/html-renderer.ts
|
|
8828
|
+
var DEFAULT_HTML_RENDER_OPTIONS = {
|
|
8829
|
+
title: "Workspace Dependency Graph",
|
|
8830
|
+
inlineD3: true,
|
|
8831
|
+
minify: false
|
|
8832
|
+
};
|
|
8833
|
+
function sanitizeHtml(str) {
|
|
8834
|
+
if (typeof str !== "string") {
|
|
8835
|
+
return "";
|
|
8836
|
+
}
|
|
8837
|
+
return str.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'").replaceAll("/", "/").replaceAll("`", "`").replaceAll("=", "=");
|
|
8838
|
+
}
|
|
8839
|
+
function sanitizeJsString(str) {
|
|
8840
|
+
if (typeof str !== "string") {
|
|
8841
|
+
return "";
|
|
8842
|
+
}
|
|
8843
|
+
return str.replaceAll("\\", "\\\\").replaceAll("'", String.raw`\'`).replaceAll('"', String.raw`\"`).replaceAll("\n", String.raw`\n`).replaceAll("\r", String.raw`\r`).replaceAll(" ", String.raw`\t`).replaceAll("\u2028", String.raw`\u2028`).replaceAll("\u2029", String.raw`\u2029`).replaceAll("</script>", String.raw`<\/script>`);
|
|
8844
|
+
}
|
|
8845
|
+
function sanitizeFilePath(path17) {
|
|
8846
|
+
if (typeof path17 !== "string") {
|
|
8847
|
+
return "";
|
|
8848
|
+
}
|
|
8849
|
+
return path17.replaceAll("\0", "").replaceAll("../", "").replaceAll(/\.\.$/g, "");
|
|
8850
|
+
}
|
|
8851
|
+
function sanitizeVisualizationData(data) {
|
|
8852
|
+
return {
|
|
8853
|
+
nodes: data.nodes.map((node) => ({
|
|
8854
|
+
...node,
|
|
8855
|
+
id: sanitizeJsString(node.id),
|
|
8856
|
+
name: sanitizeJsString(node.name),
|
|
8857
|
+
filePath: sanitizeJsString(sanitizeFilePath(node.filePath)),
|
|
8858
|
+
packageName: node.packageName === void 0 ? void 0 : sanitizeJsString(node.packageName),
|
|
8859
|
+
layer: node.layer === void 0 ? void 0 : sanitizeJsString(node.layer),
|
|
8860
|
+
violations: node.violations.map((v) => ({
|
|
8861
|
+
...v,
|
|
8862
|
+
id: sanitizeJsString(v.id),
|
|
8863
|
+
message: sanitizeJsString(v.message),
|
|
8864
|
+
ruleId: sanitizeJsString(v.ruleId)
|
|
8865
|
+
}))
|
|
8866
|
+
})),
|
|
8867
|
+
edges: data.edges.map((edge) => ({
|
|
8868
|
+
...edge,
|
|
8869
|
+
source: sanitizeJsString(edge.source),
|
|
8870
|
+
target: sanitizeJsString(edge.target),
|
|
8871
|
+
cycleId: edge.cycleId === void 0 ? void 0 : sanitizeJsString(edge.cycleId)
|
|
8872
|
+
})),
|
|
8873
|
+
cycles: data.cycles.map((cycle) => ({
|
|
8874
|
+
...cycle,
|
|
8875
|
+
id: sanitizeJsString(cycle.id),
|
|
8876
|
+
nodes: cycle.nodes.map((n) => sanitizeJsString(n)),
|
|
8877
|
+
edges: cycle.edges.map((e) => ({
|
|
8878
|
+
from: sanitizeJsString(e.from),
|
|
8879
|
+
to: sanitizeJsString(e.to)
|
|
8880
|
+
})),
|
|
8881
|
+
description: sanitizeJsString(cycle.description)
|
|
8882
|
+
})),
|
|
8883
|
+
statistics: data.statistics,
|
|
8884
|
+
layers: data.layers.map((layer) => ({
|
|
8885
|
+
...layer,
|
|
8886
|
+
name: sanitizeJsString(layer.name),
|
|
8887
|
+
allowedDependencies: layer.allowedDependencies.map((d) => sanitizeJsString(d))
|
|
8888
|
+
})),
|
|
8889
|
+
metadata: {
|
|
8890
|
+
...data.metadata,
|
|
8891
|
+
workspacePath: sanitizeJsString(sanitizeFilePath(data.metadata.workspacePath)),
|
|
8892
|
+
generatedAt: sanitizeJsString(data.metadata.generatedAt),
|
|
8893
|
+
analyzerVersion: sanitizeJsString(data.metadata.analyzerVersion)
|
|
8894
|
+
}
|
|
8895
|
+
};
|
|
8896
|
+
}
|
|
8897
|
+
function generateSidebarHtml(data, title) {
|
|
8898
|
+
const stats = data.statistics;
|
|
8899
|
+
const layers = data.layers;
|
|
8900
|
+
const layerFiltersHtml = layers.map(
|
|
8901
|
+
(layer) => `
|
|
8902
|
+
<label class="layer-item">
|
|
8903
|
+
<input type="checkbox" class="layer-checkbox" data-layer="${sanitizeHtml(layer.name)}" checked>
|
|
8904
|
+
<span class="layer-color ${sanitizeHtml(layer.name.toLowerCase())}"></span>
|
|
8905
|
+
<span class="layer-name">${sanitizeHtml(layer.name)}</span>
|
|
8906
|
+
<span class="layer-count">${stats.nodesByLayer[layer.name] ?? 0}</span>
|
|
8907
|
+
</label>
|
|
8908
|
+
`
|
|
8909
|
+
).join("");
|
|
8910
|
+
const severityStatsHtml = ["critical", "error", "warning", "info"].map(
|
|
8911
|
+
(severity) => `
|
|
8912
|
+
<div class="severity-item">
|
|
8913
|
+
<div class="severity-indicator">
|
|
8914
|
+
<div class="severity-dot ${severity}"></div>
|
|
8915
|
+
<span class="severity-name">${severity}</span>
|
|
8916
|
+
</div>
|
|
8917
|
+
<span class="severity-count" id="stat-${severity}">${stats.violationsBySeverity[severity] ?? 0}</span>
|
|
8918
|
+
</div>
|
|
8919
|
+
`
|
|
8920
|
+
).join("");
|
|
8921
|
+
return `
|
|
8922
|
+
<div class="sidebar-header">
|
|
8923
|
+
<h1 class="sidebar-title">${sanitizeHtml(title)}</h1>
|
|
8924
|
+
<p class="sidebar-subtitle">Generated ${sanitizeHtml(data.metadata.generatedAt)}</p>
|
|
8925
|
+
</div>
|
|
8926
|
+
<div class="sidebar-content">
|
|
8927
|
+
<section class="sidebar-section">
|
|
8928
|
+
<h2 class="section-title">Statistics</h2>
|
|
8929
|
+
<div class="stats-grid">
|
|
8930
|
+
<div class="stat-card">
|
|
8931
|
+
<div class="stat-value" id="stat-nodes">${stats.totalNodes}</div>
|
|
8932
|
+
<div class="stat-label">Nodes</div>
|
|
8933
|
+
</div>
|
|
8934
|
+
<div class="stat-card">
|
|
8935
|
+
<div class="stat-value" id="stat-edges">${stats.totalEdges}</div>
|
|
8936
|
+
<div class="stat-label">Edges</div>
|
|
8937
|
+
</div>
|
|
8938
|
+
<div class="stat-card">
|
|
8939
|
+
<div class="stat-value" id="stat-cycles">${stats.totalCycles}</div>
|
|
8940
|
+
<div class="stat-label">Cycles</div>
|
|
8941
|
+
</div>
|
|
8942
|
+
<div class="stat-card">
|
|
8943
|
+
<div class="stat-value">${stats.packagesAnalyzed}</div>
|
|
8944
|
+
<div class="stat-label">Packages</div>
|
|
8945
|
+
</div>
|
|
8946
|
+
</div>
|
|
8947
|
+
</section>
|
|
8948
|
+
|
|
8949
|
+
<section class="sidebar-section">
|
|
8950
|
+
<h2 class="section-title">Violations by Severity</h2>
|
|
8951
|
+
<div class="severity-list">
|
|
8952
|
+
${severityStatsHtml}
|
|
8953
|
+
</div>
|
|
8954
|
+
</section>
|
|
8955
|
+
|
|
8956
|
+
<section class="sidebar-section">
|
|
8957
|
+
<h2 class="section-title">Filter by Severity</h2>
|
|
8958
|
+
<div class="severity-filter-list">
|
|
8959
|
+
<label class="severity-filter-item">
|
|
8960
|
+
<input type="checkbox" class="severity-checkbox" data-severity="critical" checked>
|
|
8961
|
+
<div class="severity-indicator">
|
|
8962
|
+
<div class="severity-dot critical"></div>
|
|
8963
|
+
<span class="severity-name">Critical</span>
|
|
8964
|
+
</div>
|
|
8965
|
+
</label>
|
|
8966
|
+
<label class="severity-filter-item">
|
|
8967
|
+
<input type="checkbox" class="severity-checkbox" data-severity="error" checked>
|
|
8968
|
+
<div class="severity-indicator">
|
|
8969
|
+
<div class="severity-dot error"></div>
|
|
8970
|
+
<span class="severity-name">Error</span>
|
|
8971
|
+
</div>
|
|
8972
|
+
</label>
|
|
8973
|
+
<label class="severity-filter-item">
|
|
8974
|
+
<input type="checkbox" class="severity-checkbox" data-severity="warning" checked>
|
|
8975
|
+
<div class="severity-indicator">
|
|
8976
|
+
<div class="severity-dot warning"></div>
|
|
8977
|
+
<span class="severity-name">Warning</span>
|
|
8978
|
+
</div>
|
|
8979
|
+
</label>
|
|
8980
|
+
<label class="severity-filter-item">
|
|
8981
|
+
<input type="checkbox" class="severity-checkbox" data-severity="info" checked>
|
|
8982
|
+
<div class="severity-indicator">
|
|
8983
|
+
<div class="severity-dot info"></div>
|
|
8984
|
+
<span class="severity-name">Info</span>
|
|
8985
|
+
</div>
|
|
8986
|
+
</label>
|
|
8987
|
+
</div>
|
|
8988
|
+
</section>
|
|
8989
|
+
|
|
8990
|
+
<section class="sidebar-section">
|
|
8991
|
+
<h2 class="section-title">Filter by Layer</h2>
|
|
8992
|
+
<div class="layer-list">
|
|
8993
|
+
${layerFiltersHtml}
|
|
8994
|
+
</div>
|
|
8995
|
+
</section>
|
|
8996
|
+
|
|
8997
|
+
<section class="sidebar-section legend-section">
|
|
8998
|
+
<h2 class="section-title">Legend</h2>
|
|
8999
|
+
<div class="legend-item">
|
|
9000
|
+
<div class="legend-icon">
|
|
9001
|
+
<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" fill="${SEVERITY_COLORS.critical.primary}" stroke="#fff" stroke-width="2"/></svg>
|
|
9002
|
+
</div>
|
|
9003
|
+
<span class="legend-text">Critical violation</span>
|
|
9004
|
+
</div>
|
|
9005
|
+
<div class="legend-item">
|
|
9006
|
+
<div class="legend-icon">
|
|
9007
|
+
<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" fill="#ef4444" stroke="#ef4444" stroke-width="3"/></svg>
|
|
9008
|
+
</div>
|
|
9009
|
+
<span class="legend-text">Node in cycle</span>
|
|
9010
|
+
</div>
|
|
9011
|
+
<div class="legend-item">
|
|
9012
|
+
<div class="legend-icon">
|
|
9013
|
+
<svg viewBox="0 0 16 16"><line x1="0" y1="8" x2="16" y2="8" stroke="#ef4444" stroke-width="2" stroke-dasharray="0"/></svg>
|
|
9014
|
+
</div>
|
|
9015
|
+
<span class="legend-text">Cycle edge (animated)</span>
|
|
9016
|
+
</div>
|
|
9017
|
+
<div class="legend-item">
|
|
9018
|
+
<div class="legend-icon">
|
|
9019
|
+
<svg viewBox="0 0 16 16"><line x1="0" y1="8" x2="16" y2="8" stroke="#d1d5db" stroke-width="2" stroke-dasharray="4,4"/></svg>
|
|
9020
|
+
</div>
|
|
9021
|
+
<span class="legend-text">Dynamic import</span>
|
|
9022
|
+
</div>
|
|
9023
|
+
</section>
|
|
9024
|
+
</div>
|
|
9025
|
+
`;
|
|
9026
|
+
}
|
|
9027
|
+
function generateControlPanelHtml() {
|
|
9028
|
+
return `
|
|
9029
|
+
<div class="control-group">
|
|
9030
|
+
<label class="control-label">View:</label>
|
|
9031
|
+
<div class="control-btn-group">
|
|
9032
|
+
<button class="control-btn view-mode-btn active" data-mode="all">All</button>
|
|
9033
|
+
<button class="control-btn view-mode-btn" data-mode="cycles">Cycles</button>
|
|
9034
|
+
<button class="control-btn view-mode-btn" data-mode="violations">Violations</button>
|
|
9035
|
+
</div>
|
|
9036
|
+
</div>
|
|
9037
|
+
|
|
9038
|
+
<div class="control-group">
|
|
9039
|
+
<label class="control-label" for="search-input">Search:</label>
|
|
9040
|
+
<input type="text" id="search-input" class="control-input" placeholder="Filter nodes...">
|
|
9041
|
+
</div>
|
|
9042
|
+
|
|
9043
|
+
<div class="control-spacer"></div>
|
|
9044
|
+
|
|
9045
|
+
<div class="zoom-controls">
|
|
9046
|
+
<button class="zoom-btn" id="zoom-out" title="Zoom out (-)">\u2212</button>
|
|
9047
|
+
<span class="zoom-level">100%</span>
|
|
9048
|
+
<button class="zoom-btn" id="zoom-in" title="Zoom in (+)">+</button>
|
|
9049
|
+
<button class="control-btn" id="zoom-reset" title="Reset zoom (0)">Reset</button>
|
|
9050
|
+
</div>
|
|
9051
|
+
`;
|
|
9052
|
+
}
|
|
9053
|
+
function validateVisualizationData(data) {
|
|
9054
|
+
if (data === null || data === void 0 || typeof data !== "object") {
|
|
9055
|
+
return err6({
|
|
9056
|
+
code: "INVALID_DATA",
|
|
9057
|
+
message: "Visualization data must be an object"
|
|
9058
|
+
});
|
|
9059
|
+
}
|
|
9060
|
+
const d = data;
|
|
9061
|
+
if (!Array.isArray(d.nodes)) {
|
|
9062
|
+
return err6({
|
|
9063
|
+
code: "INVALID_DATA",
|
|
9064
|
+
message: "Visualization data must have a nodes array"
|
|
9065
|
+
});
|
|
9066
|
+
}
|
|
9067
|
+
if (!Array.isArray(d.edges)) {
|
|
9068
|
+
return err6({
|
|
9069
|
+
code: "INVALID_DATA",
|
|
9070
|
+
message: "Visualization data must have an edges array"
|
|
9071
|
+
});
|
|
9072
|
+
}
|
|
9073
|
+
if (d.statistics === null || d.statistics === void 0 || typeof d.statistics !== "object") {
|
|
9074
|
+
return err6({
|
|
9075
|
+
code: "INVALID_DATA",
|
|
9076
|
+
message: "Visualization data must have statistics object"
|
|
9077
|
+
});
|
|
9078
|
+
}
|
|
9079
|
+
if (d.metadata === null || d.metadata === void 0 || typeof d.metadata !== "object") {
|
|
9080
|
+
return err6({
|
|
9081
|
+
code: "INVALID_DATA",
|
|
9082
|
+
message: "Visualization data must have metadata object"
|
|
9083
|
+
});
|
|
9084
|
+
}
|
|
9085
|
+
return ok24(data);
|
|
9086
|
+
}
|
|
9087
|
+
function renderVisualizationHtml(data, options = {}) {
|
|
9088
|
+
const opts = {
|
|
9089
|
+
...DEFAULT_HTML_RENDER_OPTIONS,
|
|
9090
|
+
...options
|
|
9091
|
+
};
|
|
9092
|
+
const validationResult = validateVisualizationData(data);
|
|
9093
|
+
if (validationResult.success === false) {
|
|
9094
|
+
return validationResult;
|
|
9095
|
+
}
|
|
9096
|
+
const sanitizedData = sanitizeVisualizationData(data);
|
|
9097
|
+
const dataJson = JSON.stringify(sanitizedData);
|
|
9098
|
+
const styles = generateStyles();
|
|
9099
|
+
const sidebarHtml = generateSidebarHtml(sanitizedData, opts.title);
|
|
9100
|
+
const controlPanelHtml = generateControlPanelHtml();
|
|
9101
|
+
const graphScript = generateGraphInitScript();
|
|
9102
|
+
const controlScript = generateControlPanelScript();
|
|
9103
|
+
const html = `<!DOCTYPE html>
|
|
9104
|
+
<html lang="en">
|
|
9105
|
+
<head>
|
|
9106
|
+
<meta charset="UTF-8">
|
|
9107
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
9108
|
+
<meta name="generator" content="@bfra.me/workspace-analyzer">
|
|
9109
|
+
<meta name="description" content="Interactive dependency graph visualization">
|
|
9110
|
+
<title>${sanitizeHtml(opts.title)}</title>
|
|
9111
|
+
<style>
|
|
9112
|
+
${styles}
|
|
9113
|
+
${opts.customCss ?? ""}
|
|
9114
|
+
</style>
|
|
9115
|
+
</head>
|
|
9116
|
+
<body>
|
|
9117
|
+
<div class="app-container">
|
|
9118
|
+
<aside class="sidebar">
|
|
9119
|
+
${sidebarHtml}
|
|
9120
|
+
</aside>
|
|
9121
|
+
<main class="main-content">
|
|
9122
|
+
<div class="control-panel">
|
|
9123
|
+
${controlPanelHtml}
|
|
9124
|
+
</div>
|
|
9125
|
+
<div class="graph-container">
|
|
9126
|
+
<svg class="graph-canvas"></svg>
|
|
9127
|
+
<div class="loading-overlay">
|
|
9128
|
+
<div class="loading-spinner"></div>
|
|
9129
|
+
</div>
|
|
9130
|
+
</div>
|
|
9131
|
+
</main>
|
|
9132
|
+
</div>
|
|
9133
|
+
<div class="tooltip"></div>
|
|
9134
|
+
|
|
9135
|
+
<script>
|
|
9136
|
+
// Visualization data (sanitized)
|
|
9137
|
+
window.VISUALIZATION_DATA = ${dataJson};
|
|
9138
|
+
</script>
|
|
9139
|
+
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
|
|
9140
|
+
<script>
|
|
9141
|
+
// Graph Initialization
|
|
9142
|
+
${graphScript}
|
|
9143
|
+
</script>
|
|
9144
|
+
<script>
|
|
9145
|
+
// Control Panel
|
|
9146
|
+
${controlScript}
|
|
9147
|
+
</script>
|
|
9148
|
+
${opts.customJs === void 0 ? "" : ` <script>
|
|
9149
|
+
${opts.customJs}
|
|
9150
|
+
</script>`}
|
|
9151
|
+
</body>
|
|
9152
|
+
</html>`;
|
|
9153
|
+
return ok24(html);
|
|
9154
|
+
}
|
|
9155
|
+
function exportVisualizationJson(data) {
|
|
9156
|
+
const sanitizedData = sanitizeVisualizationData(data);
|
|
9157
|
+
return JSON.stringify(sanitizedData, null, 2);
|
|
9158
|
+
}
|
|
9159
|
+
function estimateHtmlSize(data) {
|
|
9160
|
+
const baseSize = 5e4;
|
|
9161
|
+
const dataJson = JSON.stringify(data);
|
|
9162
|
+
const dataSize = dataJson.length * 1.1;
|
|
9163
|
+
const scriptsSize = 1e4;
|
|
9164
|
+
return Math.ceil(baseSize + dataSize + scriptsSize);
|
|
9165
|
+
}
|
|
9166
|
+
function isWithinSizeLimit(data, maxSizeBytes = 5 * 1024 * 1024) {
|
|
9167
|
+
return estimateHtmlSize(data) < maxSizeBytes;
|
|
9168
|
+
}
|
|
9169
|
+
|
|
9170
|
+
// src/visualizer/mermaid-exporter.ts
|
|
9171
|
+
var DEFAULT_MERMAID_OPTIONS = {
|
|
9172
|
+
cyclesOnly: false,
|
|
9173
|
+
maxNodes: Number.POSITIVE_INFINITY,
|
|
9174
|
+
includeViolations: true,
|
|
9175
|
+
direction: "LR"
|
|
9176
|
+
};
|
|
9177
|
+
function sanitizeMermaidId(id) {
|
|
9178
|
+
return id.replaceAll(/\W/g, "_");
|
|
9179
|
+
}
|
|
9180
|
+
function sanitizeMermaidLabel(label) {
|
|
9181
|
+
return label.replaceAll('"', String.raw`\"`).replaceAll("\n", " ");
|
|
9182
|
+
}
|
|
9183
|
+
function getShortName2(filePath) {
|
|
9184
|
+
const parts = filePath.split("/");
|
|
9185
|
+
return parts.at(-1) ?? filePath;
|
|
9186
|
+
}
|
|
9187
|
+
function generateNodeDefinition(nodeId, label, violations, includeViolations) {
|
|
9188
|
+
const sanitizedId = sanitizeMermaidId(nodeId);
|
|
9189
|
+
const sanitizedLabel = sanitizeMermaidLabel(label);
|
|
9190
|
+
let nodeLabel = sanitizedLabel;
|
|
9191
|
+
if (includeViolations && violations.length > 0) {
|
|
9192
|
+
const violationSummary = violations.reduce((acc, v) => {
|
|
9193
|
+
acc[v.severity] = (acc[v.severity] ?? 0) + 1;
|
|
9194
|
+
return acc;
|
|
9195
|
+
}, {});
|
|
9196
|
+
const violationText = Object.entries(violationSummary).map(([severity, count]) => `${count} ${severity}`).join(", ");
|
|
9197
|
+
nodeLabel = `${sanitizedLabel}<br/>(${violationText})`;
|
|
9198
|
+
}
|
|
9199
|
+
const highestSeverity = violations.length > 0 ? violations.reduce((highest, v) => {
|
|
9200
|
+
if (highest === void 0) return v.severity;
|
|
9201
|
+
const severityOrder = { critical: 0, error: 1, warning: 2, info: 3 };
|
|
9202
|
+
const currentOrder = severityOrder[v.severity] ?? 999;
|
|
9203
|
+
const highestOrder = severityOrder[highest] ?? 999;
|
|
9204
|
+
return currentOrder < highestOrder ? v.severity : highest;
|
|
9205
|
+
}, void 0) : void 0;
|
|
9206
|
+
const styleClass = highestSeverity !== void 0 && highestSeverity.length > 0 ? `class-${highestSeverity}` : "class-normal";
|
|
9207
|
+
return ` ${sanitizedId}["${nodeLabel}"]:::${styleClass}`;
|
|
9208
|
+
}
|
|
9209
|
+
function generateEdgeDefinition(edge) {
|
|
9210
|
+
const fromId = sanitizeMermaidId(edge.source);
|
|
9211
|
+
const toId = sanitizeMermaidId(edge.target);
|
|
9212
|
+
const edgeStyle = edge.isInCycle ? "==>" : edge.type === "type-only" ? "-.->" : edge.type === "dynamic" ? "-..->" : "-->";
|
|
9213
|
+
const linkClass = edge.isInCycle ? "linkStyle cycle" : "";
|
|
9214
|
+
return ` ${fromId} ${edgeStyle} ${toId}${linkClass ? ` :::${linkClass}` : ""}`;
|
|
9215
|
+
}
|
|
9216
|
+
function generateStyleDefinitions() {
|
|
9217
|
+
return `
|
|
9218
|
+
classDef class-critical fill:#ff4444,stroke:#cc0000,stroke-width:3px,color:#fff
|
|
9219
|
+
classDef class-error fill:#ff8844,stroke:#cc4400,stroke-width:2px,color:#fff
|
|
9220
|
+
classDef class-warning fill:#ffcc44,stroke:#ccaa00,stroke-width:2px,color:#000
|
|
9221
|
+
classDef class-info fill:#4488ff,stroke:#0044cc,stroke-width:1px,color:#fff
|
|
9222
|
+
classDef class-normal fill:#88ccff,stroke:#0088cc,stroke-width:1px,color:#000
|
|
9223
|
+
linkStyle cycle stroke:#ff0000,stroke-width:3px,stroke-dasharray:5 5
|
|
9224
|
+
`;
|
|
9225
|
+
}
|
|
9226
|
+
function exportVisualizationMermaid(data, options = {}) {
|
|
9227
|
+
const opts = { ...DEFAULT_MERMAID_OPTIONS, ...options };
|
|
9228
|
+
const lines = [`graph ${opts.direction}`];
|
|
9229
|
+
const cycleNodeIds = /* @__PURE__ */ new Set();
|
|
9230
|
+
if (opts.cyclesOnly) {
|
|
9231
|
+
for (const cycle of data.cycles) {
|
|
9232
|
+
for (const nodeId of cycle.nodes) {
|
|
9233
|
+
cycleNodeIds.add(nodeId);
|
|
9234
|
+
}
|
|
9235
|
+
}
|
|
9236
|
+
}
|
|
9237
|
+
const filteredNodes = opts.cyclesOnly ? data.nodes.filter((n) => cycleNodeIds.has(n.id)) : data.nodes;
|
|
9238
|
+
const nodesToInclude = filteredNodes.slice(0, opts.maxNodes);
|
|
9239
|
+
const nodeIds = new Set(nodesToInclude.map((n) => n.id));
|
|
9240
|
+
for (const node of nodesToInclude) {
|
|
9241
|
+
const label = getShortName2(node.filePath);
|
|
9242
|
+
const nodeDef = generateNodeDefinition(node.id, label, node.violations, opts.includeViolations);
|
|
9243
|
+
lines.push(nodeDef);
|
|
9244
|
+
}
|
|
9245
|
+
const filteredEdges = data.edges.filter(
|
|
9246
|
+
(e) => nodeIds.has(e.source) && nodeIds.has(e.target) && (!opts.cyclesOnly || e.isInCycle)
|
|
9247
|
+
);
|
|
9248
|
+
for (const edge of filteredEdges) {
|
|
9249
|
+
lines.push(generateEdgeDefinition(edge));
|
|
9250
|
+
}
|
|
9251
|
+
lines.push(generateStyleDefinitions());
|
|
9252
|
+
return lines.join("\n");
|
|
9253
|
+
}
|
|
9254
|
+
function exportCycleMermaid(cycle, data) {
|
|
9255
|
+
const lines = ["graph LR"];
|
|
9256
|
+
const nodeMap = new Map(data.nodes.map((n) => [n.id, n]));
|
|
9257
|
+
for (const nodeId of cycle.nodes) {
|
|
9258
|
+
const node = nodeMap.get(nodeId);
|
|
9259
|
+
if (node === void 0) continue;
|
|
9260
|
+
const label = getShortName2(node.filePath);
|
|
9261
|
+
const nodeDef = generateNodeDefinition(nodeId, label, node.violations, true);
|
|
9262
|
+
lines.push(nodeDef);
|
|
9263
|
+
}
|
|
9264
|
+
for (const edge of cycle.edges) {
|
|
9265
|
+
const fromId = sanitizeMermaidId(edge.from);
|
|
9266
|
+
const toId = sanitizeMermaidId(edge.to);
|
|
9267
|
+
lines.push(` ${fromId} ==> ${toId}`);
|
|
9268
|
+
}
|
|
9269
|
+
lines.push(generateStyleDefinitions());
|
|
9270
|
+
return lines.join("\n");
|
|
9271
|
+
}
|
|
9272
|
+
|
|
9273
|
+
// src/visualizer/templates/d3-bundle.ts
|
|
9274
|
+
var D3_CDN_URLS = {
|
|
9275
|
+
/** Full D3.js bundle from jsdelivr (minified) */
|
|
9276
|
+
jsdelivr: "https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js",
|
|
9277
|
+
/** Full D3.js bundle from unpkg (minified) */
|
|
9278
|
+
unpkg: "https://unpkg.com/d3@7/dist/d3.min.js",
|
|
9279
|
+
/** Full D3.js bundle from cdnjs (minified) */
|
|
9280
|
+
cdnjs: "https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"
|
|
9281
|
+
};
|
|
9282
|
+
var D3_VERSION = "7.9.0";
|
|
9283
|
+
var D3_INLINE_MODULES = String.raw`
|
|
9284
|
+
/**
|
|
9285
|
+
* Minimal D3-compatible implementation for force-directed graphs.
|
|
9286
|
+
* This provides the core functionality needed without the full D3 library.
|
|
9287
|
+
*
|
|
9288
|
+
* For full D3.js functionality, the HTML renderer can optionally embed
|
|
9289
|
+
* the complete minified D3.js bundle (~280KB) or load from CDN.
|
|
9290
|
+
*/
|
|
9291
|
+
(function(global) {
|
|
9292
|
+
'use strict';
|
|
9293
|
+
|
|
9294
|
+
// Simple namespace
|
|
9295
|
+
const d3 = {};
|
|
9296
|
+
|
|
9297
|
+
// Selection implementation
|
|
9298
|
+
function Selection(nodes) {
|
|
9299
|
+
this._nodes = Array.isArray(nodes) ? nodes : [nodes];
|
|
9300
|
+
}
|
|
9301
|
+
|
|
9302
|
+
Selection.prototype.select = function(selector) {
|
|
9303
|
+
const node = this._nodes[0];
|
|
9304
|
+
if (!node) return new Selection([]);
|
|
9305
|
+
const selected = typeof selector === 'string'
|
|
9306
|
+
? node.querySelector(selector)
|
|
9307
|
+
: selector;
|
|
9308
|
+
return new Selection(selected ? [selected] : []);
|
|
9309
|
+
};
|
|
9310
|
+
|
|
9311
|
+
Selection.prototype.selectAll = function(selector) {
|
|
9312
|
+
const results = [];
|
|
9313
|
+
this._nodes.forEach(node => {
|
|
9314
|
+
if (node) {
|
|
9315
|
+
const selected = typeof selector === 'string'
|
|
9316
|
+
? node.querySelectorAll(selector)
|
|
9317
|
+
: selector;
|
|
9318
|
+
results.push(...Array.from(selected));
|
|
9319
|
+
}
|
|
9320
|
+
});
|
|
9321
|
+
return new Selection(results);
|
|
9322
|
+
};
|
|
9323
|
+
|
|
9324
|
+
Selection.prototype.append = function(type) {
|
|
9325
|
+
const results = [];
|
|
9326
|
+
this._nodes.forEach(node => {
|
|
9327
|
+
if (node) {
|
|
9328
|
+
const child = document.createElementNS(
|
|
9329
|
+
type === 'svg' || node.namespaceURI === 'http://www.w3.org/2000/svg'
|
|
9330
|
+
? 'http://www.w3.org/2000/svg'
|
|
9331
|
+
: 'http://www.w3.org/1999/xhtml',
|
|
9332
|
+
type
|
|
9333
|
+
);
|
|
9334
|
+
node.appendChild(child);
|
|
9335
|
+
results.push(child);
|
|
9336
|
+
}
|
|
9337
|
+
});
|
|
9338
|
+
return new Selection(results);
|
|
9339
|
+
};
|
|
9340
|
+
|
|
9341
|
+
Selection.prototype.attr = function(name, value) {
|
|
9342
|
+
if (value === undefined) {
|
|
9343
|
+
const node = this._nodes[0];
|
|
9344
|
+
return node ? node.getAttribute(name) : null;
|
|
9345
|
+
}
|
|
9346
|
+
this._nodes.forEach((node, i) => {
|
|
9347
|
+
if (node) {
|
|
9348
|
+
const v = typeof value === 'function' ? value(node.__data__, i) : value;
|
|
9349
|
+
if (v === null) {
|
|
9350
|
+
node.removeAttribute(name);
|
|
9351
|
+
} else {
|
|
9352
|
+
node.setAttribute(name, v);
|
|
9353
|
+
}
|
|
9354
|
+
}
|
|
9355
|
+
});
|
|
9356
|
+
return this;
|
|
9357
|
+
};
|
|
9358
|
+
|
|
9359
|
+
Selection.prototype.style = function(name, value) {
|
|
9360
|
+
if (value === undefined) {
|
|
9361
|
+
const node = this._nodes[0];
|
|
9362
|
+
return node ? getComputedStyle(node).getPropertyValue(name) : null;
|
|
9363
|
+
}
|
|
9364
|
+
this._nodes.forEach((node, i) => {
|
|
9365
|
+
if (node) {
|
|
9366
|
+
const v = typeof value === 'function' ? value(node.__data__, i) : value;
|
|
9367
|
+
node.style.setProperty(name, v);
|
|
9368
|
+
}
|
|
9369
|
+
});
|
|
9370
|
+
return this;
|
|
9371
|
+
};
|
|
9372
|
+
|
|
9373
|
+
Selection.prototype.classed = function(names, value) {
|
|
9374
|
+
const classes = names.split(/\s+/);
|
|
9375
|
+
if (value === undefined) {
|
|
9376
|
+
const node = this._nodes[0];
|
|
9377
|
+
return node ? classes.every(c => node.classList.contains(c)) : false;
|
|
9378
|
+
}
|
|
9379
|
+
this._nodes.forEach((node, i) => {
|
|
9380
|
+
if (node) {
|
|
9381
|
+
const v = typeof value === 'function' ? value(node.__data__, i) : value;
|
|
9382
|
+
classes.forEach(c => {
|
|
9383
|
+
if (c) node.classList.toggle(c, v);
|
|
9384
|
+
});
|
|
9385
|
+
}
|
|
9386
|
+
});
|
|
9387
|
+
return this;
|
|
9388
|
+
};
|
|
9389
|
+
|
|
9390
|
+
Selection.prototype.text = function(value) {
|
|
9391
|
+
if (value === undefined) {
|
|
9392
|
+
const node = this._nodes[0];
|
|
9393
|
+
return node ? node.textContent : null;
|
|
9394
|
+
}
|
|
9395
|
+
this._nodes.forEach((node, i) => {
|
|
9396
|
+
if (node) {
|
|
9397
|
+
node.textContent = typeof value === 'function' ? value(node.__data__, i) : value;
|
|
9398
|
+
}
|
|
9399
|
+
});
|
|
9400
|
+
return this;
|
|
9401
|
+
};
|
|
9402
|
+
|
|
9403
|
+
Selection.prototype.html = function(value) {
|
|
9404
|
+
if (value === undefined) {
|
|
9405
|
+
const node = this._nodes[0];
|
|
9406
|
+
return node ? node.innerHTML : null;
|
|
9407
|
+
}
|
|
9408
|
+
this._nodes.forEach((node, i) => {
|
|
9409
|
+
if (node) {
|
|
9410
|
+
node.innerHTML = typeof value === 'function' ? value(node.__data__, i) : value;
|
|
9411
|
+
}
|
|
9412
|
+
});
|
|
9413
|
+
return this;
|
|
9414
|
+
};
|
|
9415
|
+
|
|
9416
|
+
Selection.prototype.on = function(type, listener) {
|
|
9417
|
+
const types = type.split('.');
|
|
9418
|
+
const eventType = types[0];
|
|
9419
|
+
this._nodes.forEach(node => {
|
|
9420
|
+
if (node) {
|
|
9421
|
+
if (listener === null) {
|
|
9422
|
+
node.removeEventListener(eventType, node['__on_' + type]);
|
|
9423
|
+
} else {
|
|
9424
|
+
const handler = function(event) {
|
|
9425
|
+
listener.call(this, event, this.__data__);
|
|
9426
|
+
};
|
|
9427
|
+
node['__on_' + type] = handler;
|
|
9428
|
+
node.addEventListener(eventType, handler);
|
|
9429
|
+
}
|
|
9430
|
+
}
|
|
9431
|
+
});
|
|
9432
|
+
return this;
|
|
9433
|
+
};
|
|
9434
|
+
|
|
9435
|
+
Selection.prototype.data = function(data, key) {
|
|
9436
|
+
const nodes = this._nodes;
|
|
9437
|
+
const dataArray = typeof data === 'function' ? data() : data;
|
|
9438
|
+
|
|
9439
|
+
// Simple data join - just bind data to existing nodes
|
|
9440
|
+
const update = [];
|
|
9441
|
+
const enter = [];
|
|
9442
|
+
const exit = [];
|
|
9443
|
+
|
|
9444
|
+
dataArray.forEach((d, i) => {
|
|
9445
|
+
if (nodes[i]) {
|
|
9446
|
+
nodes[i].__data__ = d;
|
|
9447
|
+
update.push(nodes[i]);
|
|
9448
|
+
} else {
|
|
9449
|
+
enter.push({__data__: d, _index: i});
|
|
9450
|
+
}
|
|
9451
|
+
});
|
|
9452
|
+
|
|
9453
|
+
for (let i = dataArray.length; i < nodes.length; i++) {
|
|
9454
|
+
exit.push(nodes[i]);
|
|
9455
|
+
}
|
|
9456
|
+
|
|
9457
|
+
const selection = new Selection(update);
|
|
9458
|
+
selection._enter = enter;
|
|
9459
|
+
selection._exit = exit;
|
|
9460
|
+
selection._parent = this._nodes[0]?.parentNode;
|
|
9461
|
+
return selection;
|
|
9462
|
+
};
|
|
9463
|
+
|
|
9464
|
+
Selection.prototype.enter = function() {
|
|
9465
|
+
const enterSelection = new Selection([]);
|
|
9466
|
+
enterSelection._enter = this._enter || [];
|
|
9467
|
+
enterSelection._parent = this._parent;
|
|
9468
|
+
enterSelection.append = function(type) {
|
|
9469
|
+
const results = [];
|
|
9470
|
+
const parent = this._parent || document.body;
|
|
9471
|
+
this._enter.forEach(d => {
|
|
9472
|
+
const node = document.createElementNS(
|
|
9473
|
+
type === 'svg' || parent.namespaceURI === 'http://www.w3.org/2000/svg'
|
|
9474
|
+
? 'http://www.w3.org/2000/svg'
|
|
9475
|
+
: 'http://www.w3.org/1999/xhtml',
|
|
9476
|
+
type
|
|
9477
|
+
);
|
|
9478
|
+
node.__data__ = d.__data__;
|
|
9479
|
+
parent.appendChild(node);
|
|
9480
|
+
results.push(node);
|
|
9481
|
+
});
|
|
9482
|
+
return new Selection(results);
|
|
9483
|
+
};
|
|
9484
|
+
return enterSelection;
|
|
9485
|
+
};
|
|
9486
|
+
|
|
9487
|
+
Selection.prototype.exit = function() {
|
|
9488
|
+
return new Selection(this._exit || []);
|
|
9489
|
+
};
|
|
9490
|
+
|
|
9491
|
+
Selection.prototype.remove = function() {
|
|
9492
|
+
this._nodes.forEach(node => {
|
|
9493
|
+
if (node && node.parentNode) {
|
|
9494
|
+
node.parentNode.removeChild(node);
|
|
9495
|
+
}
|
|
9496
|
+
});
|
|
9497
|
+
return this;
|
|
9498
|
+
};
|
|
9499
|
+
|
|
9500
|
+
Selection.prototype.join = function(enter, update, exit) {
|
|
9501
|
+
const enterSel = this.enter().append(typeof enter === 'string' ? enter : 'g');
|
|
9502
|
+
const exitSel = this.exit();
|
|
9503
|
+
if (exit) exit(exitSel);
|
|
9504
|
+
else exitSel.remove();
|
|
9505
|
+
const merged = new Selection([...enterSel._nodes, ...this._nodes]);
|
|
9506
|
+
if (update) update(merged);
|
|
9507
|
+
return merged;
|
|
9508
|
+
};
|
|
9509
|
+
|
|
9510
|
+
Selection.prototype.each = function(callback) {
|
|
9511
|
+
this._nodes.forEach((node, i) => {
|
|
9512
|
+
if (node) callback.call(node, node.__data__, i);
|
|
9513
|
+
});
|
|
9514
|
+
return this;
|
|
9515
|
+
};
|
|
9516
|
+
|
|
9517
|
+
Selection.prototype.call = function(fn, ...args) {
|
|
9518
|
+
fn.apply(null, [this, ...args]);
|
|
9519
|
+
return this;
|
|
9520
|
+
};
|
|
9521
|
+
|
|
9522
|
+
Selection.prototype.node = function() {
|
|
9523
|
+
return this._nodes[0] || null;
|
|
9524
|
+
};
|
|
9525
|
+
|
|
9526
|
+
Selection.prototype.nodes = function() {
|
|
9527
|
+
return this._nodes.slice();
|
|
9528
|
+
};
|
|
9529
|
+
|
|
9530
|
+
Selection.prototype.empty = function() {
|
|
9531
|
+
return this._nodes.length === 0;
|
|
9532
|
+
};
|
|
9533
|
+
|
|
9534
|
+
Selection.prototype.size = function() {
|
|
9535
|
+
return this._nodes.length;
|
|
9536
|
+
};
|
|
9537
|
+
|
|
9538
|
+
Selection.prototype.raise = function() {
|
|
9539
|
+
this._nodes.forEach(node => {
|
|
9540
|
+
if (node && node.parentNode) {
|
|
9541
|
+
node.parentNode.appendChild(node);
|
|
9542
|
+
}
|
|
9543
|
+
});
|
|
9544
|
+
return this;
|
|
9545
|
+
};
|
|
9546
|
+
|
|
9547
|
+
Selection.prototype.lower = function() {
|
|
9548
|
+
this._nodes.forEach(node => {
|
|
9549
|
+
if (node && node.parentNode) {
|
|
9550
|
+
node.parentNode.insertBefore(node, node.parentNode.firstChild);
|
|
9551
|
+
}
|
|
9552
|
+
});
|
|
9553
|
+
return this;
|
|
9554
|
+
};
|
|
9555
|
+
|
|
9556
|
+
// Core selection functions
|
|
9557
|
+
d3.select = function(selector) {
|
|
9558
|
+
const node = typeof selector === 'string'
|
|
9559
|
+
? document.querySelector(selector)
|
|
9560
|
+
: selector;
|
|
9561
|
+
return new Selection(node ? [node] : []);
|
|
9562
|
+
};
|
|
9563
|
+
|
|
9564
|
+
d3.selectAll = function(selector) {
|
|
9565
|
+
const nodes = typeof selector === 'string'
|
|
9566
|
+
? document.querySelectorAll(selector)
|
|
9567
|
+
: selector;
|
|
9568
|
+
return new Selection(Array.from(nodes));
|
|
9569
|
+
};
|
|
9570
|
+
|
|
9571
|
+
d3.create = function(name) {
|
|
9572
|
+
const node = document.createElementNS(
|
|
9573
|
+
name === 'svg' ? 'http://www.w3.org/2000/svg' : 'http://www.w3.org/1999/xhtml',
|
|
9574
|
+
name
|
|
9575
|
+
);
|
|
9576
|
+
return new Selection([node]);
|
|
9577
|
+
};
|
|
9578
|
+
|
|
9579
|
+
// Force simulation
|
|
9580
|
+
function ForceSimulation(nodes) {
|
|
9581
|
+
this._nodes = nodes || [];
|
|
9582
|
+
this._forces = {};
|
|
9583
|
+
this._alpha = 1;
|
|
9584
|
+
this._alphaMin = 0.001;
|
|
9585
|
+
this._alphaDecay = 1 - Math.pow(this._alphaMin, 1 / 300);
|
|
9586
|
+
this._alphaTarget = 0;
|
|
9587
|
+
this._velocityDecay = 0.6;
|
|
9588
|
+
this._listeners = {tick: [], end: []};
|
|
9589
|
+
this._running = false;
|
|
9590
|
+
|
|
9591
|
+
// Initialize nodes
|
|
9592
|
+
this._nodes.forEach((node, i) => {
|
|
9593
|
+
if (node.x === undefined) node.x = Math.random() * 100;
|
|
9594
|
+
if (node.y === undefined) node.y = Math.random() * 100;
|
|
9595
|
+
if (node.vx === undefined) node.vx = 0;
|
|
9596
|
+
if (node.vy === undefined) node.vy = 0;
|
|
9597
|
+
node.index = i;
|
|
9598
|
+
});
|
|
9599
|
+
}
|
|
9600
|
+
|
|
9601
|
+
ForceSimulation.prototype.nodes = function(nodes) {
|
|
9602
|
+
if (nodes === undefined) return this._nodes;
|
|
9603
|
+
this._nodes = nodes;
|
|
9604
|
+
nodes.forEach((node, i) => {
|
|
9605
|
+
if (node.x === undefined) node.x = Math.random() * 100;
|
|
9606
|
+
if (node.y === undefined) node.y = Math.random() * 100;
|
|
9607
|
+
if (node.vx === undefined) node.vx = 0;
|
|
9608
|
+
if (node.vy === undefined) node.vy = 0;
|
|
9609
|
+
node.index = i;
|
|
9610
|
+
});
|
|
9611
|
+
// Re-initialize forces
|
|
9612
|
+
Object.values(this._forces).forEach(force => {
|
|
9613
|
+
if (force.initialize) force.initialize(this._nodes);
|
|
9614
|
+
});
|
|
9615
|
+
return this;
|
|
9616
|
+
};
|
|
9617
|
+
|
|
9618
|
+
ForceSimulation.prototype.force = function(name, force) {
|
|
9619
|
+
if (force === undefined) return this._forces[name];
|
|
9620
|
+
if (force === null) {
|
|
9621
|
+
delete this._forces[name];
|
|
9622
|
+
} else {
|
|
9623
|
+
this._forces[name] = force;
|
|
9624
|
+
if (force.initialize) force.initialize(this._nodes);
|
|
9625
|
+
}
|
|
9626
|
+
return this;
|
|
9627
|
+
};
|
|
9628
|
+
|
|
9629
|
+
ForceSimulation.prototype.alpha = function(alpha) {
|
|
9630
|
+
if (alpha === undefined) return this._alpha;
|
|
9631
|
+
this._alpha = alpha;
|
|
9632
|
+
return this;
|
|
9633
|
+
};
|
|
9634
|
+
|
|
9635
|
+
ForceSimulation.prototype.alphaMin = function(min) {
|
|
9636
|
+
if (min === undefined) return this._alphaMin;
|
|
9637
|
+
this._alphaMin = min;
|
|
9638
|
+
return this;
|
|
9639
|
+
};
|
|
9640
|
+
|
|
9641
|
+
ForceSimulation.prototype.alphaDecay = function(decay) {
|
|
9642
|
+
if (decay === undefined) return this._alphaDecay;
|
|
9643
|
+
this._alphaDecay = decay;
|
|
9644
|
+
return this;
|
|
9645
|
+
};
|
|
9646
|
+
|
|
9647
|
+
ForceSimulation.prototype.alphaTarget = function(target) {
|
|
9648
|
+
if (target === undefined) return this._alphaTarget;
|
|
9649
|
+
this._alphaTarget = target;
|
|
9650
|
+
return this;
|
|
9651
|
+
};
|
|
9652
|
+
|
|
9653
|
+
ForceSimulation.prototype.velocityDecay = function(decay) {
|
|
9654
|
+
if (decay === undefined) return this._velocityDecay;
|
|
9655
|
+
this._velocityDecay = decay;
|
|
9656
|
+
return this;
|
|
9657
|
+
};
|
|
9658
|
+
|
|
9659
|
+
ForceSimulation.prototype.tick = function(iterations) {
|
|
9660
|
+
iterations = iterations || 1;
|
|
9661
|
+
for (let k = 0; k < iterations; k++) {
|
|
9662
|
+
this._alpha += (this._alphaTarget - this._alpha) * this._alphaDecay;
|
|
9663
|
+
|
|
9664
|
+
// Apply forces
|
|
9665
|
+
Object.values(this._forces).forEach(force => {
|
|
9666
|
+
if (force) force(this._alpha);
|
|
9667
|
+
});
|
|
9668
|
+
|
|
9669
|
+
// Update positions
|
|
9670
|
+
this._nodes.forEach(node => {
|
|
9671
|
+
if (node.fx !== undefined && node.fx !== null) {
|
|
9672
|
+
node.x = node.fx;
|
|
9673
|
+
node.vx = 0;
|
|
9674
|
+
} else {
|
|
9675
|
+
node.vx *= this._velocityDecay;
|
|
9676
|
+
node.x += node.vx;
|
|
9677
|
+
}
|
|
9678
|
+
if (node.fy !== undefined && node.fy !== null) {
|
|
9679
|
+
node.y = node.fy;
|
|
9680
|
+
node.vy = 0;
|
|
9681
|
+
} else {
|
|
9682
|
+
node.vy *= this._velocityDecay;
|
|
9683
|
+
node.y += node.vy;
|
|
9684
|
+
}
|
|
9685
|
+
});
|
|
9686
|
+
}
|
|
9687
|
+
return this;
|
|
9688
|
+
};
|
|
9689
|
+
|
|
9690
|
+
ForceSimulation.prototype.restart = function() {
|
|
9691
|
+
if (this._running) return this;
|
|
9692
|
+
this._running = true;
|
|
9693
|
+
const self = this;
|
|
9694
|
+
function step() {
|
|
9695
|
+
if (!self._running) return;
|
|
9696
|
+
self.tick();
|
|
9697
|
+
self._listeners.tick.forEach(fn => fn());
|
|
9698
|
+
if (self._alpha < self._alphaMin) {
|
|
9699
|
+
self._running = false;
|
|
9700
|
+
self._listeners.end.forEach(fn => fn());
|
|
9701
|
+
return;
|
|
9702
|
+
}
|
|
9703
|
+
requestAnimationFrame(step);
|
|
9704
|
+
}
|
|
9705
|
+
requestAnimationFrame(step);
|
|
9706
|
+
return this;
|
|
9707
|
+
};
|
|
9708
|
+
|
|
9709
|
+
ForceSimulation.prototype.stop = function() {
|
|
9710
|
+
this._running = false;
|
|
9711
|
+
return this;
|
|
9712
|
+
};
|
|
9713
|
+
|
|
9714
|
+
ForceSimulation.prototype.on = function(type, listener) {
|
|
9715
|
+
const eventType = type.split('.')[0];
|
|
9716
|
+
if (listener === undefined) {
|
|
9717
|
+
return this._listeners[eventType] || [];
|
|
9718
|
+
}
|
|
9719
|
+
if (!this._listeners[eventType]) {
|
|
9720
|
+
this._listeners[eventType] = [];
|
|
9721
|
+
}
|
|
9722
|
+
if (listener === null) {
|
|
9723
|
+
this._listeners[eventType] = [];
|
|
9724
|
+
} else {
|
|
9725
|
+
this._listeners[eventType].push(listener);
|
|
9726
|
+
}
|
|
9727
|
+
return this;
|
|
9728
|
+
};
|
|
9729
|
+
|
|
9730
|
+
ForceSimulation.prototype.find = function(x, y, radius) {
|
|
9731
|
+
let closest = null;
|
|
9732
|
+
let minDist = radius !== undefined ? radius : Infinity;
|
|
9733
|
+
this._nodes.forEach(node => {
|
|
9734
|
+
const dx = node.x - x;
|
|
9735
|
+
const dy = node.y - y;
|
|
9736
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
9737
|
+
if (dist < minDist) {
|
|
9738
|
+
minDist = dist;
|
|
9739
|
+
closest = node;
|
|
9740
|
+
}
|
|
9741
|
+
});
|
|
9742
|
+
return closest;
|
|
9743
|
+
};
|
|
9744
|
+
|
|
9745
|
+
d3.forceSimulation = function(nodes) {
|
|
9746
|
+
return new ForceSimulation(nodes);
|
|
9747
|
+
};
|
|
9748
|
+
|
|
9749
|
+
// Force: Link
|
|
9750
|
+
d3.forceLink = function(links) {
|
|
9751
|
+
let _links = links || [];
|
|
9752
|
+
let _id = d => d.index;
|
|
9753
|
+
let _distance = 30;
|
|
9754
|
+
let _strength = 1;
|
|
9755
|
+
let _nodes;
|
|
9756
|
+
|
|
9757
|
+
function force(alpha) {
|
|
9758
|
+
_links.forEach(link => {
|
|
9759
|
+
const source = typeof link.source === 'object' ? link.source : _nodes[link.source];
|
|
9760
|
+
const target = typeof link.target === 'object' ? link.target : _nodes[link.target];
|
|
9761
|
+
if (!source || !target) return;
|
|
9762
|
+
|
|
9763
|
+
let dx = target.x - source.x;
|
|
9764
|
+
let dy = target.y - source.y;
|
|
9765
|
+
let dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
9766
|
+
let targetDist = typeof _distance === 'function' ? _distance(link) : _distance;
|
|
9767
|
+
let str = typeof _strength === 'function' ? _strength(link) : _strength;
|
|
9768
|
+
|
|
9769
|
+
const k = (dist - targetDist) / dist * alpha * str;
|
|
9770
|
+
dx *= k;
|
|
9771
|
+
dy *= k;
|
|
9772
|
+
|
|
9773
|
+
const sourceWeight = 1;
|
|
9774
|
+
const targetWeight = 1;
|
|
9775
|
+
const totalWeight = sourceWeight + targetWeight;
|
|
9776
|
+
|
|
9777
|
+
target.vx -= dx * sourceWeight / totalWeight;
|
|
9778
|
+
target.vy -= dy * sourceWeight / totalWeight;
|
|
9779
|
+
source.vx += dx * targetWeight / totalWeight;
|
|
9780
|
+
source.vy += dy * targetWeight / totalWeight;
|
|
9781
|
+
});
|
|
9782
|
+
}
|
|
9783
|
+
|
|
9784
|
+
force.initialize = function(nodes) {
|
|
9785
|
+
_nodes = nodes;
|
|
9786
|
+
const nodeById = new Map(nodes.map(d => [_id(d), d]));
|
|
9787
|
+
_links.forEach(link => {
|
|
9788
|
+
if (typeof link.source !== 'object') link.source = nodeById.get(link.source);
|
|
9789
|
+
if (typeof link.target !== 'object') link.target = nodeById.get(link.target);
|
|
9790
|
+
});
|
|
9791
|
+
};
|
|
9792
|
+
|
|
9793
|
+
force.links = function(links) {
|
|
9794
|
+
if (links === undefined) return _links;
|
|
9795
|
+
_links = links;
|
|
9796
|
+
return force;
|
|
9797
|
+
};
|
|
9798
|
+
|
|
9799
|
+
force.id = function(id) {
|
|
9800
|
+
if (id === undefined) return _id;
|
|
9801
|
+
_id = id;
|
|
9802
|
+
return force;
|
|
9803
|
+
};
|
|
9804
|
+
|
|
9805
|
+
force.distance = function(distance) {
|
|
9806
|
+
if (distance === undefined) return _distance;
|
|
9807
|
+
_distance = typeof distance === 'function' ? distance : +distance;
|
|
9808
|
+
return force;
|
|
9809
|
+
};
|
|
9810
|
+
|
|
9811
|
+
force.strength = function(strength) {
|
|
9812
|
+
if (strength === undefined) return _strength;
|
|
9813
|
+
_strength = typeof strength === 'function' ? strength : +strength;
|
|
9814
|
+
return force;
|
|
9815
|
+
};
|
|
9816
|
+
|
|
9817
|
+
return force;
|
|
9818
|
+
};
|
|
9819
|
+
|
|
9820
|
+
// Force: Many-Body (charge)
|
|
9821
|
+
d3.forceManyBody = function() {
|
|
9822
|
+
let _strength = -30;
|
|
9823
|
+
let _distanceMin = 1;
|
|
9824
|
+
let _distanceMax = Infinity;
|
|
9825
|
+
let _nodes;
|
|
9826
|
+
|
|
9827
|
+
function force(alpha) {
|
|
9828
|
+
for (let i = 0; i < _nodes.length; i++) {
|
|
9829
|
+
for (let j = i + 1; j < _nodes.length; j++) {
|
|
9830
|
+
const nodeI = _nodes[i];
|
|
9831
|
+
const nodeJ = _nodes[j];
|
|
9832
|
+
|
|
9833
|
+
let dx = nodeJ.x - nodeI.x;
|
|
9834
|
+
let dy = nodeJ.y - nodeI.y;
|
|
9835
|
+
let dist = Math.sqrt(dx * dx + dy * dy);
|
|
9836
|
+
|
|
9837
|
+
if (dist < _distanceMin) dist = _distanceMin;
|
|
9838
|
+
if (dist > _distanceMax) continue;
|
|
9839
|
+
|
|
9840
|
+
const str = typeof _strength === 'function' ? _strength(nodeI) : _strength;
|
|
9841
|
+
const k = str * alpha / (dist * dist);
|
|
9842
|
+
|
|
9843
|
+
dx *= k;
|
|
9844
|
+
dy *= k;
|
|
9845
|
+
|
|
9846
|
+
nodeJ.vx += dx;
|
|
9847
|
+
nodeJ.vy += dy;
|
|
9848
|
+
nodeI.vx -= dx;
|
|
9849
|
+
nodeI.vy -= dy;
|
|
9850
|
+
}
|
|
9851
|
+
}
|
|
9852
|
+
}
|
|
9853
|
+
|
|
9854
|
+
force.initialize = function(nodes) {
|
|
9855
|
+
_nodes = nodes;
|
|
9856
|
+
};
|
|
9857
|
+
|
|
9858
|
+
force.strength = function(strength) {
|
|
9859
|
+
if (strength === undefined) return _strength;
|
|
9860
|
+
_strength = typeof strength === 'function' ? strength : +strength;
|
|
9861
|
+
return force;
|
|
9862
|
+
};
|
|
9863
|
+
|
|
9864
|
+
force.distanceMin = function(distance) {
|
|
9865
|
+
if (distance === undefined) return _distanceMin;
|
|
9866
|
+
_distanceMin = +distance;
|
|
9867
|
+
return force;
|
|
9868
|
+
};
|
|
9869
|
+
|
|
9870
|
+
force.distanceMax = function(distance) {
|
|
9871
|
+
if (distance === undefined) return _distanceMax;
|
|
9872
|
+
_distanceMax = +distance;
|
|
9873
|
+
return force;
|
|
9874
|
+
};
|
|
9875
|
+
|
|
9876
|
+
return force;
|
|
9877
|
+
};
|
|
9878
|
+
|
|
9879
|
+
// Force: Center
|
|
9880
|
+
d3.forceCenter = function(x, y) {
|
|
9881
|
+
let _x = x || 0;
|
|
9882
|
+
let _y = y || 0;
|
|
9883
|
+
let _strength = 1;
|
|
9884
|
+
let _nodes;
|
|
9885
|
+
|
|
9886
|
+
function force(alpha) {
|
|
9887
|
+
let sx = 0, sy = 0;
|
|
9888
|
+
_nodes.forEach(node => {
|
|
9889
|
+
sx += node.x;
|
|
9890
|
+
sy += node.y;
|
|
9891
|
+
});
|
|
9892
|
+
sx = (sx / _nodes.length - _x) * _strength;
|
|
9893
|
+
sy = (sy / _nodes.length - _y) * _strength;
|
|
9894
|
+
_nodes.forEach(node => {
|
|
9895
|
+
node.x -= sx;
|
|
9896
|
+
node.y -= sy;
|
|
9897
|
+
});
|
|
9898
|
+
}
|
|
9899
|
+
|
|
9900
|
+
force.initialize = function(nodes) {
|
|
9901
|
+
_nodes = nodes;
|
|
9902
|
+
};
|
|
9903
|
+
|
|
9904
|
+
force.x = function(x) {
|
|
9905
|
+
if (x === undefined) return _x;
|
|
9906
|
+
_x = +x;
|
|
9907
|
+
return force;
|
|
9908
|
+
};
|
|
9909
|
+
|
|
9910
|
+
force.y = function(y) {
|
|
9911
|
+
if (y === undefined) return _y;
|
|
9912
|
+
_y = +y;
|
|
9913
|
+
return force;
|
|
9914
|
+
};
|
|
9915
|
+
|
|
9916
|
+
force.strength = function(strength) {
|
|
9917
|
+
if (strength === undefined) return _strength;
|
|
9918
|
+
_strength = +strength;
|
|
9919
|
+
return force;
|
|
9920
|
+
};
|
|
9921
|
+
|
|
9922
|
+
return force;
|
|
9923
|
+
};
|
|
9924
|
+
|
|
9925
|
+
// Force: Collide
|
|
9926
|
+
d3.forceCollide = function(radius) {
|
|
9927
|
+
let _radius = radius || 1;
|
|
9928
|
+
let _strength = 1;
|
|
9929
|
+
let _iterations = 1;
|
|
9930
|
+
let _nodes;
|
|
9931
|
+
|
|
9932
|
+
function force(alpha) {
|
|
9933
|
+
for (let iter = 0; iter < _iterations; iter++) {
|
|
9934
|
+
for (let i = 0; i < _nodes.length; i++) {
|
|
9935
|
+
for (let j = i + 1; j < _nodes.length; j++) {
|
|
9936
|
+
const nodeI = _nodes[i];
|
|
9937
|
+
const nodeJ = _nodes[j];
|
|
9938
|
+
|
|
9939
|
+
let dx = nodeJ.x - nodeI.x;
|
|
9940
|
+
let dy = nodeJ.y - nodeI.y;
|
|
9941
|
+
let dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
9942
|
+
|
|
9943
|
+
const ri = typeof _radius === 'function' ? _radius(nodeI) : _radius;
|
|
9944
|
+
const rj = typeof _radius === 'function' ? _radius(nodeJ) : _radius;
|
|
9945
|
+
const minDist = ri + rj;
|
|
9946
|
+
|
|
9947
|
+
if (dist < minDist) {
|
|
9948
|
+
const k = (minDist - dist) / dist * _strength * 0.5;
|
|
9949
|
+
dx *= k;
|
|
9950
|
+
dy *= k;
|
|
9951
|
+
nodeJ.x += dx;
|
|
9952
|
+
nodeJ.y += dy;
|
|
9953
|
+
nodeI.x -= dx;
|
|
9954
|
+
nodeI.y -= dy;
|
|
9955
|
+
}
|
|
9956
|
+
}
|
|
9957
|
+
}
|
|
9958
|
+
}
|
|
9959
|
+
}
|
|
9960
|
+
|
|
9961
|
+
force.initialize = function(nodes) {
|
|
9962
|
+
_nodes = nodes;
|
|
9963
|
+
};
|
|
9964
|
+
|
|
9965
|
+
force.radius = function(radius) {
|
|
9966
|
+
if (radius === undefined) return _radius;
|
|
9967
|
+
_radius = typeof radius === 'function' ? radius : +radius;
|
|
9968
|
+
return force;
|
|
9969
|
+
};
|
|
9970
|
+
|
|
9971
|
+
force.strength = function(strength) {
|
|
9972
|
+
if (strength === undefined) return _strength;
|
|
9973
|
+
_strength = +strength;
|
|
9974
|
+
return force;
|
|
9975
|
+
};
|
|
9976
|
+
|
|
9977
|
+
force.iterations = function(iterations) {
|
|
9978
|
+
if (iterations === undefined) return _iterations;
|
|
9979
|
+
_iterations = +iterations;
|
|
9980
|
+
return force;
|
|
9981
|
+
};
|
|
9982
|
+
|
|
9983
|
+
return force;
|
|
9984
|
+
};
|
|
9985
|
+
|
|
9986
|
+
// Force: X positioning
|
|
9987
|
+
d3.forceX = function(x) {
|
|
9988
|
+
let _x = x || 0;
|
|
9989
|
+
let _strength = 0.1;
|
|
9990
|
+
let _nodes;
|
|
9991
|
+
|
|
9992
|
+
function force(alpha) {
|
|
9993
|
+
_nodes.forEach(node => {
|
|
9994
|
+
const targetX = typeof _x === 'function' ? _x(node) : _x;
|
|
9995
|
+
node.vx += (targetX - node.x) * _strength * alpha;
|
|
9996
|
+
});
|
|
9997
|
+
}
|
|
9998
|
+
|
|
9999
|
+
force.initialize = function(nodes) {
|
|
10000
|
+
_nodes = nodes;
|
|
10001
|
+
};
|
|
10002
|
+
|
|
10003
|
+
force.x = function(x) {
|
|
10004
|
+
if (x === undefined) return _x;
|
|
10005
|
+
_x = typeof x === 'function' ? x : +x;
|
|
10006
|
+
return force;
|
|
10007
|
+
};
|
|
10008
|
+
|
|
10009
|
+
force.strength = function(strength) {
|
|
10010
|
+
if (strength === undefined) return _strength;
|
|
10011
|
+
_strength = +strength;
|
|
10012
|
+
return force;
|
|
10013
|
+
};
|
|
10014
|
+
|
|
10015
|
+
return force;
|
|
10016
|
+
};
|
|
10017
|
+
|
|
10018
|
+
// Force: Y positioning
|
|
10019
|
+
d3.forceY = function(y) {
|
|
10020
|
+
let _y = y || 0;
|
|
10021
|
+
let _strength = 0.1;
|
|
10022
|
+
let _nodes;
|
|
10023
|
+
|
|
10024
|
+
function force(alpha) {
|
|
10025
|
+
_nodes.forEach(node => {
|
|
10026
|
+
const targetY = typeof _y === 'function' ? _y(node) : _y;
|
|
10027
|
+
node.vy += (targetY - node.y) * _strength * alpha;
|
|
10028
|
+
});
|
|
10029
|
+
}
|
|
10030
|
+
|
|
10031
|
+
force.initialize = function(nodes) {
|
|
10032
|
+
_nodes = nodes;
|
|
10033
|
+
};
|
|
10034
|
+
|
|
10035
|
+
force.y = function(y) {
|
|
10036
|
+
if (y === undefined) return _y;
|
|
10037
|
+
_y = typeof y === 'function' ? y : +y;
|
|
10038
|
+
return force;
|
|
10039
|
+
};
|
|
10040
|
+
|
|
10041
|
+
force.strength = function(strength) {
|
|
10042
|
+
if (strength === undefined) return _strength;
|
|
10043
|
+
_strength = +strength;
|
|
10044
|
+
return force;
|
|
10045
|
+
};
|
|
10046
|
+
|
|
10047
|
+
return force;
|
|
10048
|
+
};
|
|
10049
|
+
|
|
10050
|
+
// Zoom behavior
|
|
10051
|
+
function ZoomBehavior() {
|
|
10052
|
+
this._scaleExtent = [0.1, 10];
|
|
10053
|
+
this._translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]];
|
|
10054
|
+
this._transform = {k: 1, x: 0, y: 0};
|
|
10055
|
+
this._listeners = {};
|
|
10056
|
+
this._filter = () => true;
|
|
10057
|
+
}
|
|
10058
|
+
|
|
10059
|
+
ZoomBehavior.prototype.apply = function(selection) {
|
|
10060
|
+
const self = this;
|
|
10061
|
+
selection.each(function() {
|
|
10062
|
+
const element = this;
|
|
10063
|
+
|
|
10064
|
+
// Wheel zoom
|
|
10065
|
+
element.addEventListener('wheel', function(event) {
|
|
10066
|
+
if (!self._filter(event)) return;
|
|
10067
|
+
event.preventDefault();
|
|
10068
|
+
const rect = element.getBoundingClientRect();
|
|
10069
|
+
const x = event.clientX - rect.left;
|
|
10070
|
+
const y = event.clientY - rect.top;
|
|
10071
|
+
|
|
10072
|
+
const k0 = self._transform.k;
|
|
10073
|
+
const k1 = k0 * Math.pow(2, -event.deltaY * 0.002);
|
|
10074
|
+
const k = Math.max(self._scaleExtent[0], Math.min(self._scaleExtent[1], k1));
|
|
10075
|
+
|
|
10076
|
+
// Zoom towards mouse position
|
|
10077
|
+
self._transform.x += (x - self._transform.x) * (1 - k / k0);
|
|
10078
|
+
self._transform.y += (y - self._transform.y) * (1 - k / k0);
|
|
10079
|
+
self._transform.k = k;
|
|
10080
|
+
|
|
10081
|
+
self._emit('zoom', event);
|
|
10082
|
+
});
|
|
10083
|
+
|
|
10084
|
+
// Pan with mouse drag
|
|
10085
|
+
let dragging = false;
|
|
10086
|
+
let startX, startY, startTx, startTy;
|
|
10087
|
+
|
|
10088
|
+
element.addEventListener('mousedown', function(event) {
|
|
10089
|
+
if (!self._filter(event)) return;
|
|
10090
|
+
if (event.button !== 0) return;
|
|
10091
|
+
dragging = true;
|
|
10092
|
+
startX = event.clientX;
|
|
10093
|
+
startY = event.clientY;
|
|
10094
|
+
startTx = self._transform.x;
|
|
10095
|
+
startTy = self._transform.y;
|
|
10096
|
+
event.preventDefault();
|
|
10097
|
+
});
|
|
10098
|
+
|
|
10099
|
+
document.addEventListener('mousemove', function(event) {
|
|
10100
|
+
if (!dragging) return;
|
|
10101
|
+
self._transform.x = startTx + (event.clientX - startX);
|
|
10102
|
+
self._transform.y = startTy + (event.clientY - startY);
|
|
10103
|
+
self._emit('zoom', event);
|
|
10104
|
+
});
|
|
10105
|
+
|
|
10106
|
+
document.addEventListener('mouseup', function() {
|
|
10107
|
+
dragging = false;
|
|
10108
|
+
});
|
|
10109
|
+
|
|
10110
|
+
// Touch support
|
|
10111
|
+
element.addEventListener('touchstart', function(event) {
|
|
10112
|
+
if (!self._filter(event)) return;
|
|
10113
|
+
if (event.touches.length === 1) {
|
|
10114
|
+
dragging = true;
|
|
10115
|
+
startX = event.touches[0].clientX;
|
|
10116
|
+
startY = event.touches[0].clientY;
|
|
10117
|
+
startTx = self._transform.x;
|
|
10118
|
+
startTy = self._transform.y;
|
|
10119
|
+
}
|
|
10120
|
+
}, {passive: true});
|
|
10121
|
+
|
|
10122
|
+
element.addEventListener('touchmove', function(event) {
|
|
10123
|
+
if (!dragging || event.touches.length !== 1) return;
|
|
10124
|
+
self._transform.x = startTx + (event.touches[0].clientX - startX);
|
|
10125
|
+
self._transform.y = startTy + (event.touches[0].clientY - startY);
|
|
10126
|
+
self._emit('zoom', event);
|
|
10127
|
+
}, {passive: true});
|
|
10128
|
+
|
|
10129
|
+
element.addEventListener('touchend', function() {
|
|
10130
|
+
dragging = false;
|
|
10131
|
+
});
|
|
10132
|
+
});
|
|
10133
|
+
return this;
|
|
10134
|
+
};
|
|
10135
|
+
|
|
10136
|
+
ZoomBehavior.prototype.transform = function(selection, transform) {
|
|
10137
|
+
if (transform) {
|
|
10138
|
+
this._transform = {k: transform.k, x: transform.x, y: transform.y};
|
|
10139
|
+
this._emit('zoom', null);
|
|
10140
|
+
}
|
|
10141
|
+
return this;
|
|
10142
|
+
};
|
|
10143
|
+
|
|
10144
|
+
ZoomBehavior.prototype.scaleExtent = function(extent) {
|
|
10145
|
+
if (extent === undefined) return this._scaleExtent;
|
|
10146
|
+
this._scaleExtent = extent;
|
|
10147
|
+
return this;
|
|
10148
|
+
};
|
|
10149
|
+
|
|
10150
|
+
ZoomBehavior.prototype.translateExtent = function(extent) {
|
|
10151
|
+
if (extent === undefined) return this._translateExtent;
|
|
10152
|
+
this._translateExtent = extent;
|
|
10153
|
+
return this;
|
|
10154
|
+
};
|
|
10155
|
+
|
|
10156
|
+
ZoomBehavior.prototype.on = function(type, listener) {
|
|
10157
|
+
const eventType = type.split('.')[0];
|
|
10158
|
+
if (listener === undefined) {
|
|
10159
|
+
return this._listeners[eventType] || [];
|
|
10160
|
+
}
|
|
10161
|
+
if (!this._listeners[eventType]) {
|
|
10162
|
+
this._listeners[eventType] = [];
|
|
10163
|
+
}
|
|
10164
|
+
this._listeners[eventType].push(listener);
|
|
10165
|
+
return this;
|
|
10166
|
+
};
|
|
10167
|
+
|
|
10168
|
+
ZoomBehavior.prototype._emit = function(type, event) {
|
|
10169
|
+
const listeners = this._listeners[type] || [];
|
|
10170
|
+
const self = this;
|
|
10171
|
+
listeners.forEach(fn => {
|
|
10172
|
+
fn.call(null, {
|
|
10173
|
+
type: type,
|
|
10174
|
+
transform: self._transform,
|
|
10175
|
+
sourceEvent: event
|
|
10176
|
+
});
|
|
10177
|
+
});
|
|
10178
|
+
};
|
|
10179
|
+
|
|
10180
|
+
ZoomBehavior.prototype.scaleTo = function(selection, k) {
|
|
10181
|
+
this._transform.k = Math.max(this._scaleExtent[0], Math.min(this._scaleExtent[1], k));
|
|
10182
|
+
this._emit('zoom', null);
|
|
10183
|
+
return this;
|
|
10184
|
+
};
|
|
10185
|
+
|
|
10186
|
+
ZoomBehavior.prototype.scaleBy = function(selection, k) {
|
|
10187
|
+
return this.scaleTo(selection, this._transform.k * k);
|
|
10188
|
+
};
|
|
10189
|
+
|
|
10190
|
+
ZoomBehavior.prototype.translateTo = function(selection, x, y) {
|
|
10191
|
+
this._transform.x = x;
|
|
10192
|
+
this._transform.y = y;
|
|
10193
|
+
this._emit('zoom', null);
|
|
10194
|
+
return this;
|
|
10195
|
+
};
|
|
10196
|
+
|
|
10197
|
+
ZoomBehavior.prototype.translateBy = function(selection, x, y) {
|
|
10198
|
+
this._transform.x += x;
|
|
10199
|
+
this._transform.y += y;
|
|
10200
|
+
this._emit('zoom', null);
|
|
10201
|
+
return this;
|
|
10202
|
+
};
|
|
10203
|
+
|
|
10204
|
+
ZoomBehavior.prototype.filter = function(filter) {
|
|
10205
|
+
if (filter === undefined) return this._filter;
|
|
10206
|
+
this._filter = filter;
|
|
10207
|
+
return this;
|
|
10208
|
+
};
|
|
10209
|
+
|
|
10210
|
+
d3.zoom = function() {
|
|
10211
|
+
const behavior = new ZoomBehavior();
|
|
10212
|
+
const fn = function(selection) {
|
|
10213
|
+
behavior.apply(selection);
|
|
10214
|
+
};
|
|
10215
|
+
Object.keys(ZoomBehavior.prototype).forEach(key => {
|
|
10216
|
+
fn[key] = behavior[key].bind(behavior);
|
|
10217
|
+
});
|
|
10218
|
+
return fn;
|
|
10219
|
+
};
|
|
10220
|
+
|
|
10221
|
+
d3.zoomIdentity = {k: 1, x: 0, y: 0};
|
|
10222
|
+
|
|
10223
|
+
d3.zoomTransform = function(node) {
|
|
10224
|
+
return node.__zoom || d3.zoomIdentity;
|
|
10225
|
+
};
|
|
10226
|
+
|
|
10227
|
+
// Drag behavior
|
|
10228
|
+
function DragBehavior() {
|
|
10229
|
+
this._subject = null;
|
|
10230
|
+
this._container = null;
|
|
10231
|
+
this._listeners = {};
|
|
10232
|
+
this._filter = () => true;
|
|
10233
|
+
}
|
|
10234
|
+
|
|
10235
|
+
DragBehavior.prototype.apply = function(selection) {
|
|
10236
|
+
const self = this;
|
|
10237
|
+
selection.each(function() {
|
|
10238
|
+
const element = this;
|
|
10239
|
+
|
|
10240
|
+
element.addEventListener('mousedown', function(event) {
|
|
10241
|
+
if (!self._filter(event)) return;
|
|
10242
|
+
if (event.button !== 0) return;
|
|
10243
|
+
event.stopPropagation();
|
|
10244
|
+
|
|
10245
|
+
const subject = self._subject
|
|
10246
|
+
? self._subject.call(element, event, element.__data__)
|
|
10247
|
+
: element.__data__;
|
|
10248
|
+
|
|
10249
|
+
if (subject == null) return;
|
|
10250
|
+
|
|
10251
|
+
const emitEvent = {
|
|
10252
|
+
type: 'start',
|
|
10253
|
+
subject: subject,
|
|
10254
|
+
x: event.clientX,
|
|
10255
|
+
y: event.clientY,
|
|
10256
|
+
dx: 0,
|
|
10257
|
+
dy: 0,
|
|
10258
|
+
active: 1,
|
|
10259
|
+
sourceEvent: event
|
|
10260
|
+
};
|
|
10261
|
+
|
|
10262
|
+
self._emit('start', emitEvent);
|
|
10263
|
+
|
|
10264
|
+
let lastX = event.clientX;
|
|
10265
|
+
let lastY = event.clientY;
|
|
10266
|
+
|
|
10267
|
+
function onMouseMove(event) {
|
|
10268
|
+
const moveEvent = {
|
|
10269
|
+
type: 'drag',
|
|
10270
|
+
subject: subject,
|
|
10271
|
+
x: event.clientX,
|
|
10272
|
+
y: event.clientY,
|
|
10273
|
+
dx: event.clientX - lastX,
|
|
10274
|
+
dy: event.clientY - lastY,
|
|
10275
|
+
active: 1,
|
|
10276
|
+
sourceEvent: event
|
|
10277
|
+
};
|
|
10278
|
+
lastX = event.clientX;
|
|
10279
|
+
lastY = event.clientY;
|
|
10280
|
+
self._emit('drag', moveEvent);
|
|
10281
|
+
}
|
|
10282
|
+
|
|
10283
|
+
function onMouseUp(event) {
|
|
10284
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
10285
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
10286
|
+
|
|
10287
|
+
const endEvent = {
|
|
10288
|
+
type: 'end',
|
|
10289
|
+
subject: subject,
|
|
10290
|
+
x: event.clientX,
|
|
10291
|
+
y: event.clientY,
|
|
10292
|
+
dx: 0,
|
|
10293
|
+
dy: 0,
|
|
10294
|
+
active: 0,
|
|
10295
|
+
sourceEvent: event
|
|
10296
|
+
};
|
|
10297
|
+
self._emit('end', endEvent);
|
|
10298
|
+
}
|
|
10299
|
+
|
|
10300
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
10301
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
10302
|
+
});
|
|
10303
|
+
|
|
10304
|
+
// Touch support
|
|
10305
|
+
element.addEventListener('touchstart', function(event) {
|
|
10306
|
+
if (!self._filter(event)) return;
|
|
10307
|
+
if (event.touches.length !== 1) return;
|
|
10308
|
+
event.stopPropagation();
|
|
10309
|
+
|
|
10310
|
+
const touch = event.touches[0];
|
|
10311
|
+
const subject = self._subject
|
|
10312
|
+
? self._subject.call(element, event, element.__data__)
|
|
10313
|
+
: element.__data__;
|
|
10314
|
+
|
|
10315
|
+
if (subject == null) return;
|
|
10316
|
+
|
|
10317
|
+
const emitEvent = {
|
|
10318
|
+
type: 'start',
|
|
10319
|
+
subject: subject,
|
|
10320
|
+
x: touch.clientX,
|
|
10321
|
+
y: touch.clientY,
|
|
10322
|
+
dx: 0,
|
|
10323
|
+
dy: 0,
|
|
10324
|
+
active: 1,
|
|
10325
|
+
sourceEvent: event
|
|
10326
|
+
};
|
|
10327
|
+
|
|
10328
|
+
self._emit('start', emitEvent);
|
|
10329
|
+
|
|
10330
|
+
let lastX = touch.clientX;
|
|
10331
|
+
let lastY = touch.clientY;
|
|
10332
|
+
|
|
10333
|
+
function onTouchMove(event) {
|
|
10334
|
+
if (event.touches.length !== 1) return;
|
|
10335
|
+
const touch = event.touches[0];
|
|
10336
|
+
const moveEvent = {
|
|
10337
|
+
type: 'drag',
|
|
10338
|
+
subject: subject,
|
|
10339
|
+
x: touch.clientX,
|
|
10340
|
+
y: touch.clientY,
|
|
10341
|
+
dx: touch.clientX - lastX,
|
|
10342
|
+
dy: touch.clientY - lastY,
|
|
10343
|
+
active: 1,
|
|
10344
|
+
sourceEvent: event
|
|
10345
|
+
};
|
|
10346
|
+
lastX = touch.clientX;
|
|
10347
|
+
lastY = touch.clientY;
|
|
10348
|
+
self._emit('drag', moveEvent);
|
|
10349
|
+
}
|
|
10350
|
+
|
|
10351
|
+
function onTouchEnd(event) {
|
|
10352
|
+
element.removeEventListener('touchmove', onTouchMove);
|
|
10353
|
+
element.removeEventListener('touchend', onTouchEnd);
|
|
10354
|
+
|
|
10355
|
+
const endEvent = {
|
|
10356
|
+
type: 'end',
|
|
10357
|
+
subject: subject,
|
|
10358
|
+
x: lastX,
|
|
10359
|
+
y: lastY,
|
|
10360
|
+
dx: 0,
|
|
10361
|
+
dy: 0,
|
|
10362
|
+
active: 0,
|
|
10363
|
+
sourceEvent: event
|
|
10364
|
+
};
|
|
10365
|
+
self._emit('end', endEvent);
|
|
10366
|
+
}
|
|
10367
|
+
|
|
10368
|
+
element.addEventListener('touchmove', onTouchMove, {passive: true});
|
|
10369
|
+
element.addEventListener('touchend', onTouchEnd);
|
|
10370
|
+
}, {passive: true});
|
|
10371
|
+
});
|
|
10372
|
+
return this;
|
|
10373
|
+
};
|
|
10374
|
+
|
|
10375
|
+
DragBehavior.prototype.subject = function(subject) {
|
|
10376
|
+
if (subject === undefined) return this._subject;
|
|
10377
|
+
this._subject = subject;
|
|
10378
|
+
return this;
|
|
10379
|
+
};
|
|
10380
|
+
|
|
10381
|
+
DragBehavior.prototype.container = function(container) {
|
|
10382
|
+
if (container === undefined) return this._container;
|
|
10383
|
+
this._container = container;
|
|
10384
|
+
return this;
|
|
10385
|
+
};
|
|
10386
|
+
|
|
10387
|
+
DragBehavior.prototype.filter = function(filter) {
|
|
10388
|
+
if (filter === undefined) return this._filter;
|
|
10389
|
+
this._filter = filter;
|
|
10390
|
+
return this;
|
|
10391
|
+
};
|
|
10392
|
+
|
|
10393
|
+
DragBehavior.prototype.on = function(type, listener) {
|
|
10394
|
+
const eventType = type.split('.')[0];
|
|
10395
|
+
if (listener === undefined) {
|
|
10396
|
+
return this._listeners[eventType] || [];
|
|
10397
|
+
}
|
|
10398
|
+
if (!this._listeners[eventType]) {
|
|
10399
|
+
this._listeners[eventType] = [];
|
|
10400
|
+
}
|
|
10401
|
+
this._listeners[eventType].push(listener);
|
|
10402
|
+
return this;
|
|
10403
|
+
};
|
|
10404
|
+
|
|
10405
|
+
DragBehavior.prototype._emit = function(type, event) {
|
|
10406
|
+
const listeners = this._listeners[type] || [];
|
|
10407
|
+
listeners.forEach(fn => fn(event));
|
|
10408
|
+
};
|
|
10409
|
+
|
|
10410
|
+
d3.drag = function() {
|
|
10411
|
+
const behavior = new DragBehavior();
|
|
10412
|
+
const fn = function(selection) {
|
|
10413
|
+
behavior.apply(selection);
|
|
10414
|
+
};
|
|
10415
|
+
Object.keys(DragBehavior.prototype).forEach(key => {
|
|
10416
|
+
fn[key] = behavior[key].bind(behavior);
|
|
10417
|
+
});
|
|
10418
|
+
return fn;
|
|
10419
|
+
};
|
|
10420
|
+
|
|
10421
|
+
// Utility functions
|
|
10422
|
+
d3.extent = function(values, accessor) {
|
|
10423
|
+
let min, max;
|
|
10424
|
+
for (const value of values) {
|
|
10425
|
+
const v = accessor ? accessor(value) : value;
|
|
10426
|
+
if (v != null) {
|
|
10427
|
+
if (min === undefined) {
|
|
10428
|
+
min = max = v;
|
|
10429
|
+
} else {
|
|
10430
|
+
if (v < min) min = v;
|
|
10431
|
+
if (v > max) max = v;
|
|
10432
|
+
}
|
|
10433
|
+
}
|
|
10434
|
+
}
|
|
10435
|
+
return [min, max];
|
|
10436
|
+
};
|
|
10437
|
+
|
|
10438
|
+
d3.min = function(values, accessor) {
|
|
10439
|
+
let min;
|
|
10440
|
+
for (const value of values) {
|
|
10441
|
+
const v = accessor ? accessor(value) : value;
|
|
10442
|
+
if (v != null && (min === undefined || v < min)) {
|
|
10443
|
+
min = v;
|
|
10444
|
+
}
|
|
10445
|
+
}
|
|
10446
|
+
return min;
|
|
10447
|
+
};
|
|
10448
|
+
|
|
10449
|
+
d3.max = function(values, accessor) {
|
|
10450
|
+
let max;
|
|
10451
|
+
for (const value of values) {
|
|
10452
|
+
const v = accessor ? accessor(value) : value;
|
|
10453
|
+
if (v != null && (max === undefined || v > max)) {
|
|
10454
|
+
max = v;
|
|
10455
|
+
}
|
|
10456
|
+
}
|
|
10457
|
+
return max;
|
|
10458
|
+
};
|
|
10459
|
+
|
|
10460
|
+
// Scale functions (basic linear scale)
|
|
10461
|
+
d3.scaleLinear = function() {
|
|
10462
|
+
let _domain = [0, 1];
|
|
10463
|
+
let _range = [0, 1];
|
|
10464
|
+
|
|
10465
|
+
function scale(x) {
|
|
10466
|
+
const t = (_domain[1] - _domain[0]) || 1;
|
|
10467
|
+
const normalized = (x - _domain[0]) / t;
|
|
10468
|
+
return _range[0] + normalized * (_range[1] - _range[0]);
|
|
10469
|
+
}
|
|
10470
|
+
|
|
10471
|
+
scale.domain = function(domain) {
|
|
10472
|
+
if (domain === undefined) return _domain;
|
|
10473
|
+
_domain = domain;
|
|
10474
|
+
return scale;
|
|
10475
|
+
};
|
|
10476
|
+
|
|
10477
|
+
scale.range = function(range) {
|
|
10478
|
+
if (range === undefined) return _range;
|
|
10479
|
+
_range = range;
|
|
10480
|
+
return scale;
|
|
10481
|
+
};
|
|
10482
|
+
|
|
10483
|
+
scale.invert = function(y) {
|
|
10484
|
+
const t = (_range[1] - _range[0]) || 1;
|
|
10485
|
+
const normalized = (y - _range[0]) / t;
|
|
10486
|
+
return _domain[0] + normalized * (_domain[1] - _domain[0]);
|
|
10487
|
+
};
|
|
10488
|
+
|
|
10489
|
+
return scale;
|
|
10490
|
+
};
|
|
10491
|
+
|
|
10492
|
+
// Color scale (ordinal)
|
|
10493
|
+
d3.scaleOrdinal = function(range) {
|
|
10494
|
+
const _range = range || [];
|
|
10495
|
+
const _domain = [];
|
|
10496
|
+
const _index = new Map();
|
|
10497
|
+
|
|
10498
|
+
function scale(d) {
|
|
10499
|
+
let i = _index.get(d);
|
|
10500
|
+
if (i === undefined) {
|
|
10501
|
+
_index.set(d, i = _domain.push(d) - 1);
|
|
10502
|
+
}
|
|
10503
|
+
return _range[i % _range.length];
|
|
10504
|
+
}
|
|
10505
|
+
|
|
10506
|
+
scale.domain = function(domain) {
|
|
10507
|
+
if (domain === undefined) return _domain.slice();
|
|
10508
|
+
_domain.length = 0;
|
|
10509
|
+
_index.clear();
|
|
10510
|
+
for (const d of domain) {
|
|
10511
|
+
if (!_index.has(d)) {
|
|
10512
|
+
_index.set(d, _domain.push(d) - 1);
|
|
10513
|
+
}
|
|
10514
|
+
}
|
|
10515
|
+
return scale;
|
|
10516
|
+
};
|
|
10517
|
+
|
|
10518
|
+
scale.range = function(range) {
|
|
10519
|
+
if (range === undefined) return _range.slice();
|
|
10520
|
+
_range.length = 0;
|
|
10521
|
+
_range.push(...range);
|
|
10522
|
+
return scale;
|
|
10523
|
+
};
|
|
10524
|
+
|
|
10525
|
+
return scale;
|
|
10526
|
+
};
|
|
10527
|
+
|
|
10528
|
+
// Export d3 to global scope
|
|
10529
|
+
global.d3 = d3;
|
|
10530
|
+
|
|
10531
|
+
})(typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : this);
|
|
10532
|
+
`;
|
|
10533
|
+
|
|
10534
|
+
// src/visualizer/violation-collector.ts
|
|
10535
|
+
import path16 from "path";
|
|
10536
|
+
import { createProject as createProject9 } from "@bfra.me/doc-sync";
|
|
10537
|
+
import { err as err7, isErr, ok as ok25 } from "@bfra.me/es/result";
|
|
10538
|
+
var DEFAULT_VIOLATION_COLLECTION_OPTIONS = {
|
|
10539
|
+
includeInfo: true,
|
|
10540
|
+
maxIssues: 1e4,
|
|
10541
|
+
excludePatterns: ["**/*.test.ts", "**/*.spec.ts", "**/node_modules/**"]
|
|
10542
|
+
};
|
|
10543
|
+
async function collectVisualizationViolations(context, options = {}) {
|
|
10544
|
+
const opts = {
|
|
10545
|
+
...DEFAULT_VIOLATION_COLLECTION_OPTIONS,
|
|
10546
|
+
...options
|
|
10547
|
+
};
|
|
10548
|
+
const { ruleEngine, packages, workspacePath, tsconfigPaths, reportProgress } = context;
|
|
10549
|
+
const allIssues = [];
|
|
10550
|
+
try {
|
|
10551
|
+
for (const pkg of packages) {
|
|
10552
|
+
reportProgress?.(`Collecting violations from ${pkg.name}...`);
|
|
10553
|
+
const tsconfigPath = path16.join(pkg.packagePath, "tsconfig.json");
|
|
10554
|
+
let project;
|
|
10555
|
+
try {
|
|
10556
|
+
project = createProject9({
|
|
10557
|
+
tsConfigPath: tsconfigPath
|
|
10558
|
+
});
|
|
10559
|
+
} catch {
|
|
10560
|
+
continue;
|
|
10561
|
+
}
|
|
10562
|
+
const sourceFiles = getSourceFiles(project);
|
|
10563
|
+
for (const sourceFile of sourceFiles) {
|
|
10564
|
+
const filePath = sourceFile.getFilePath();
|
|
10565
|
+
if (shouldSkipFile2(filePath, opts.excludePatterns)) {
|
|
10566
|
+
continue;
|
|
10567
|
+
}
|
|
10568
|
+
const ruleContext = {
|
|
10569
|
+
sourceFile,
|
|
10570
|
+
pkg,
|
|
10571
|
+
workspacePath,
|
|
10572
|
+
allPackages: packages,
|
|
10573
|
+
tsconfigPaths
|
|
10574
|
+
};
|
|
10575
|
+
const result = await ruleEngine.evaluateFile(ruleContext);
|
|
10576
|
+
if (isErr(result)) {
|
|
10577
|
+
continue;
|
|
10578
|
+
}
|
|
10579
|
+
const issues = result.data;
|
|
10580
|
+
const filteredIssues = opts.includeInfo ? issues : issues.filter((issue) => issue.severity !== "info");
|
|
10581
|
+
allIssues.push(...filteredIssues);
|
|
10582
|
+
if (allIssues.length >= opts.maxIssues) {
|
|
10583
|
+
reportProgress?.(`Reached maximum issue limit (${opts.maxIssues})`);
|
|
10584
|
+
break;
|
|
10585
|
+
}
|
|
10586
|
+
}
|
|
10587
|
+
if (allIssues.length >= opts.maxIssues) {
|
|
10588
|
+
break;
|
|
10589
|
+
}
|
|
10590
|
+
}
|
|
10591
|
+
return ok25(allIssues);
|
|
10592
|
+
} catch (error) {
|
|
10593
|
+
return err7({
|
|
10594
|
+
code: "TRANSFORM_FAILED",
|
|
10595
|
+
message: `Error collecting violations: ${error instanceof Error ? error.message : String(error)}`
|
|
10596
|
+
});
|
|
10597
|
+
}
|
|
10598
|
+
}
|
|
10599
|
+
function mapIssuesToNodes(nodes, issues) {
|
|
10600
|
+
const nodesByPath = /* @__PURE__ */ new Map();
|
|
10601
|
+
for (const node of nodes) {
|
|
10602
|
+
const normalizedPath = normalizePath2(node.filePath);
|
|
10603
|
+
nodesByPath.set(normalizedPath, node);
|
|
10604
|
+
}
|
|
10605
|
+
const issuesByPath = /* @__PURE__ */ new Map();
|
|
10606
|
+
for (const issue of issues) {
|
|
10607
|
+
const normalizedPath = normalizePath2(issue.location.filePath);
|
|
10608
|
+
const existing = issuesByPath.get(normalizedPath) ?? [];
|
|
10609
|
+
existing.push(issue);
|
|
10610
|
+
issuesByPath.set(normalizedPath, existing);
|
|
10611
|
+
}
|
|
10612
|
+
return nodes.map((node) => {
|
|
10613
|
+
const normalizedPath = normalizePath2(node.filePath);
|
|
10614
|
+
const nodeIssues = issuesByPath.get(normalizedPath) ?? [];
|
|
10615
|
+
if (nodeIssues.length === 0) {
|
|
10616
|
+
return node;
|
|
10617
|
+
}
|
|
10618
|
+
const violations = nodeIssues.map((issue, index) => ({
|
|
10619
|
+
id: `${issue.id}-${index}`,
|
|
10620
|
+
message: issue.description,
|
|
10621
|
+
severity: issue.severity,
|
|
10622
|
+
ruleId: issue.id
|
|
10623
|
+
}));
|
|
10624
|
+
const severities = violations.map((v) => v.severity);
|
|
10625
|
+
const highestSeverity = getHighestSeverity(severities);
|
|
10626
|
+
return {
|
|
10627
|
+
...node,
|
|
10628
|
+
violations,
|
|
10629
|
+
highestViolationSeverity: highestSeverity
|
|
10630
|
+
};
|
|
10631
|
+
});
|
|
10632
|
+
}
|
|
10633
|
+
function getSourceFiles(project) {
|
|
10634
|
+
return project.getSourceFiles().filter((sf) => {
|
|
10635
|
+
const filePath = sf.getFilePath();
|
|
10636
|
+
return (filePath.endsWith(".ts") || filePath.endsWith(".tsx") || filePath.endsWith(".js") || filePath.endsWith(".jsx")) && !filePath.includes("node_modules") && !filePath.includes(".test.") && !filePath.includes(".spec.");
|
|
10637
|
+
});
|
|
10638
|
+
}
|
|
10639
|
+
function shouldSkipFile2(filePath, excludePatterns) {
|
|
10640
|
+
if (excludePatterns.length === 0) {
|
|
10641
|
+
return false;
|
|
10642
|
+
}
|
|
10643
|
+
const normalized = normalizePath2(filePath);
|
|
10644
|
+
return excludePatterns.some((pattern) => {
|
|
10645
|
+
const normalizedPattern = pattern.replaceAll("\\", "/").toLowerCase();
|
|
10646
|
+
return normalized.includes(normalizedPattern.replaceAll("**", "").replaceAll("*", ""));
|
|
10647
|
+
});
|
|
10648
|
+
}
|
|
10649
|
+
function normalizePath2(filePath) {
|
|
10650
|
+
return filePath.replaceAll("\\", "/").toLowerCase();
|
|
10651
|
+
}
|
|
10652
|
+
|
|
6990
10653
|
export {
|
|
6991
10654
|
getSourceFile,
|
|
6992
10655
|
parseSourceFiles,
|
|
@@ -7117,6 +10780,30 @@ export {
|
|
|
7117
10780
|
getRelativePath,
|
|
7118
10781
|
createConsoleReporter,
|
|
7119
10782
|
createJsonReporter,
|
|
7120
|
-
createMarkdownReporter
|
|
10783
|
+
createMarkdownReporter,
|
|
10784
|
+
DEFAULT_VISUALIZER_OPTIONS,
|
|
10785
|
+
SEVERITY_ORDER2 as SEVERITY_ORDER,
|
|
10786
|
+
getHighestSeverity,
|
|
10787
|
+
DEFAULT_BUILD_OPTIONS,
|
|
10788
|
+
transformNodeToVisualization,
|
|
10789
|
+
transformEdgeToVisualization,
|
|
10790
|
+
transformCycleToVisualization,
|
|
10791
|
+
buildVisualizationData,
|
|
10792
|
+
DEFAULT_HTML_RENDER_OPTIONS,
|
|
10793
|
+
sanitizeHtml,
|
|
10794
|
+
sanitizeJsString,
|
|
10795
|
+
sanitizeFilePath,
|
|
10796
|
+
sanitizeVisualizationData,
|
|
10797
|
+
renderVisualizationHtml,
|
|
10798
|
+
exportVisualizationJson,
|
|
10799
|
+
estimateHtmlSize,
|
|
10800
|
+
isWithinSizeLimit,
|
|
10801
|
+
exportVisualizationMermaid,
|
|
10802
|
+
exportCycleMermaid,
|
|
10803
|
+
D3_CDN_URLS,
|
|
10804
|
+
D3_VERSION,
|
|
10805
|
+
DEFAULT_VIOLATION_COLLECTION_OPTIONS,
|
|
10806
|
+
collectVisualizationViolations,
|
|
10807
|
+
mapIssuesToNodes
|
|
7121
10808
|
};
|
|
7122
|
-
//# sourceMappingURL=chunk-
|
|
10809
|
+
//# sourceMappingURL=chunk-4V5KYQED.js.map
|