Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

devcompass

Package Overview
Dependencies
Maintainers
1
Versions
37
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

devcompass - npm Package Compare versions

Comparing version
3.1.7
to
3.2.0
+304
src/dashboard/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevCompass Dashboard v3.2.0</title>
<!-- Modular CSS -->
<link rel="stylesheet" href="styles/base.css">
<link rel="stylesheet" href="styles/layout.css">
<link rel="stylesheet" href="styles/controls.css">
<link rel="stylesheet" href="styles/graph.css">
<link rel="stylesheet" href="styles/themes.css">
<!-- D3.js Library -->
<script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body class="theme-dark">
<!-- ========== HEADER ========== -->
<header class="header">
<div class="header-left">
<h1 class="header-title">
<span class="header-icon">📊</span>
DevCompass Dashboard
<span class="version-badge">v3.2.0</span>
</h1>
<div class="header-meta" id="headerMeta">
<span><strong>Project:</strong> <span id="projectName">Loading...</span></span>
<span><strong>Version:</strong> <span id="projectVersion">-</span></span>
<span><strong>Dependencies:</strong> <span id="totalDeps">0</span></span>
</div>
</div>
<div class="header-right">
<!-- Layout Tabs -->
<nav class="layout-tabs">
<button class="tab-btn active" data-layout="tree" onclick="window.switchLayout('tree')">
<span>🌳</span> Tree
</button>
<button class="tab-btn" data-layout="force" onclick="window.switchLayout('force')">
<span>⚡</span> Force
</button>
<button class="tab-btn" data-layout="radial" onclick="window.switchLayout('radial')">
<span>🌐</span> Radial
</button>
<button class="tab-btn" data-layout="conflict" onclick="window.switchLayout('conflict')">
<span>⚠️</span> Conflict
</button>
<button class="tab-btn" data-layout="analytics" onclick="window.switchLayout('analytics')">
<span>📊</span> Analytics
</button>
</nav>
<!-- Theme Toggle -->
<button class="icon-btn" onclick="window.toggleTheme()" title="Toggle Theme">
<span id="themeIcon">🌙</span>
</button>
</div>
</header>
<!-- ========== MAIN CONTAINER ========== -->
<div class="main-container">
<!-- ========== LEFT SIDEBAR ========== -->
<aside class="sidebar sidebar-left">
<!-- Search -->
<div class="sidebar-section">
<h3 class="sidebar-title">🔍 Search</h3>
<input
type="text"
class="search-input"
id="searchInput"
placeholder="Search packages..."
oninput="window.handleSearch(this.value)"
>
</div>
<!-- Filters -->
<div class="sidebar-section">
<h3 class="sidebar-title">🎯 Filters</h3>
<div class="filter-group">
<label class="filter-label">Health Score</label>
<select class="filter-select" id="healthFilter" onchange="window.applyFilters()">
<option value="all">All Packages</option>
<option value="excellent">Excellent (9-10)</option>
<option value="good">Good (7-8)</option>
<option value="caution">Caution (5-7)</option>
<option value="warning">Warning (3-5)</option>
<option value="critical">Critical (&lt;3)</option>
</select>
</div>
<div class="filter-group">
<label class="filter-label">Depth Level</label>
<div class="slider-container">
<input
type="range"
class="depth-slider"
id="depthSlider"
min="1"
max="10"
value="10"
oninput="window.handleDepthChange(this.value)"
>
<span class="depth-value" id="depthValue">∞</span>
</div>
</div>
<div class="filter-group">
<label class="filter-label">Show/Hide</label>
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" checked onchange="window.toggleLabels()"> Labels
</label>
<label class="checkbox-label">
<input type="checkbox" checked onchange="window.toggleLinks()"> Links
</label>
<label class="checkbox-label">
<input type="checkbox" checked onchange="window.toggleDepthCircles()"> Depth Circles
</label>
</div>
</div>
</div>
<!-- Clustering -->
<div class="sidebar-section">
<h3 class="sidebar-title">🔲 Clustering</h3>
<div class="cluster-mode-grid">
<button class="cluster-btn active" data-mode="ecosystem" onclick="window.switchClusterMode('ecosystem')">
⚛️ Ecosystem
</button>
<button class="cluster-btn" data-mode="health" onclick="window.switchClusterMode('health')">
🏥 Health
</button>
<button class="cluster-btn" data-mode="depth" onclick="window.switchClusterMode('depth')">
📊 Depth
</button>
</div>
<div class="cluster-list" id="clusterList">
<!-- Populated by JavaScript -->
</div>
</div>
</aside>
<!-- ========== GRAPH AREA ========== -->
<main class="graph-container">
<svg id="graph" class="graph-svg"></svg>
<!-- Loading Indicator -->
<div class="loading" id="loading">
<div class="spinner"></div>
<div class="loading-text">Rendering graph...</div>
</div>
<!-- Empty State -->
<div class="empty-state" id="emptyState" style="display: none;">
<div class="empty-icon">📦</div>
<h2 class="empty-title">No Dependencies Found</h2>
<p class="empty-text">Your project has no dependencies to visualize.</p>
</div>
<!-- Analytics View (Hidden by default) -->
<div class="analytics-container" id="analyticsView" style="display: none;">
<div class="analytics-grid">
<!-- Will be populated by analytics layout -->
</div>
</div>
</main>
<!-- ========== RIGHT SIDEBAR ========== -->
<aside class="sidebar sidebar-right">
<!-- Statistics -->
<div class="sidebar-section">
<h3 class="sidebar-title">📊 Statistics</h3>
<div class="stat-grid" id="statsGrid">
<div class="stat-item">
<span class="stat-label">Total</span>
<span class="stat-value" id="stat-total">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Visible</span>
<span class="stat-value" id="stat-visible">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Clusters</span>
<span class="stat-value stat-cluster" id="stat-clusters">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Vulnerable</span>
<span class="stat-value stat-danger" id="stat-vulnerable">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Outdated</span>
<span class="stat-value stat-warning" id="stat-outdated">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Healthy</span>
<span class="stat-value stat-success" id="stat-healthy">0</span>
</div>
</div>
</div>
<!-- Controls -->
<div class="sidebar-section">
<h3 class="sidebar-title">⚙️ Controls</h3>
<div class="control-buttons">
<button class="control-btn primary" onclick="window.resetView()">
<span>↻</span> Reset View
</button>
<button class="control-btn" onclick="window.fitToScreen()">
<span>⛶</span> Fit to Screen
</button>
<button class="control-btn" onclick="window.zoomIn()">
<span>🔍+</span> Zoom In
</button>
<button class="control-btn" onclick="window.zoomOut()">
<span>🔍−</span> Zoom Out
</button>
<button class="control-btn" onclick="window.centerGraph()">
<span>⊙</span> Center
</button>
</div>
</div>
<!-- Export -->
<div class="sidebar-section">
<h3 class="sidebar-title">💾 Export</h3>
<div class="control-buttons">
<button class="control-btn" onclick="window.exportPNG()">
<span>📸</span> Save PNG
</button>
<button class="control-btn" onclick="window.exportJSON()">
<span>📄</span> Save JSON
</button>
<button class="control-btn" onclick="window.exportReport()">
<span>📊</span> Report
</button>
</div>
</div>
<!-- Legend -->
<div class="sidebar-section">
<h3 class="sidebar-title">🎨 Legend</h3>
<div class="legend">
<div class="legend-item">
<div class="legend-dot health-excellent"></div>
<span>Excellent (9-10)</span>
</div>
<div class="legend-item">
<div class="legend-dot health-good"></div>
<span>Good (7-8)</span>
</div>
<div class="legend-item">
<div class="legend-dot health-caution"></div>
<span>Caution (5-7)</span>
</div>
<div class="legend-item">
<div class="legend-dot health-warning"></div>
<span>Warning (3-5)</span>
</div>
<div class="legend-item">
<div class="legend-dot health-critical"></div>
<span>Critical (&lt;3)</span>
</div>
<div class="legend-item">
<div class="legend-dot root-node"></div>
<span>Root Package</span>
</div>
</div>
</div>
</aside>
</div>
<!-- ========== TOOLTIP ========== -->
<div class="tooltip" id="tooltip"></div>
<!-- ========== ZOOM CONTROLS (FLOATING) ========== -->
<div class="zoom-controls">
<button class="zoom-btn" onclick="window.zoomIn()" title="Zoom In">+</button>
<div class="zoom-level" id="zoomLevel">100%</div>
<button class="zoom-btn" onclick="window.zoomOut()" title="Zoom Out">−</button>
<button class="zoom-btn" onclick="window.resetView()" title="Reset">⟲</button>
</div>
<!-- ========== MODULAR JAVASCRIPT ========== -->
<script>
// Graph data will be injected here by the exporter
window.graphData = {{GRAPH_DATA}};
// Clustering code will be injected here
{{CLUSTERING_CODE}}
</script>
<!-- Load modules in correct order -->
<script src="scripts/utils.js"></script>
<script src="scripts/tooltip.js"></script>
<script src="scripts/stats.js"></script>
<script src="scripts/controls.js"></script>
<script src="scripts/layouts.js"></script>
<script src="scripts/core.js"></script>
</body>
</html>
// src/dashboard/scripts/controls.js
let currentZoom = null;
let currentSvg = null;
let currentG = null;
let currentTransform = null;
let showLabels = true;
let showLinks = true;
let showDepthCircles = true;
let currentFilters = {
health: 'all',
maxDepth: 10,
searchTerm: ''
};
function initZoom(svg, g) {
currentZoom = d3.zoom()
.scaleExtent([0.1, 4])
.on('zoom', (event) => {
g.attr('transform', event.transform);
currentTransform = event.transform;
updateZoomDisplay(event.transform.k);
});
svg.call(currentZoom);
currentSvg = svg;
currentG = g;
}
function updateZoomDisplay(scale) {
const zoomPercent = Math.round(scale * 100) + '%';
const zoomLevelEl = document.getElementById('zoomLevel');
if (zoomLevelEl) {
zoomLevelEl.textContent = zoomPercent;
}
const zoomStatEl = document.getElementById('zoom-stat');
if (zoomStatEl) {
zoomStatEl.textContent = zoomPercent;
}
}
function zoomIn() {
if (currentSvg && currentZoom) {
currentSvg.transition().duration(300).call(currentZoom.scaleBy, 1.3);
}
}
function zoomOut() {
if (currentSvg && currentZoom) {
currentSvg.transition().duration(300).call(currentZoom.scaleBy, 0.7);
}
}
function resetZoom() {
if (currentSvg && currentZoom) {
currentSvg.transition().duration(500).call(
currentZoom.transform,
d3.zoomIdentity
);
}
}
function resetView() {
resetZoom();
}
function centerGraph() {
if (!currentSvg || !currentZoom || !currentG) return;
try {
const bounds = currentG.node().getBBox();
const containerWidth = currentSvg.node().clientWidth;
const containerHeight = currentSvg.node().clientHeight;
const padding = 50;
const scaleX = (containerWidth - padding * 2) / bounds.width;
const scaleY = (containerHeight - padding * 2) / bounds.height;
const scale = Math.min(scaleX, scaleY, 1);
const tx = (containerWidth - bounds.width * scale) / 2 - bounds.x * scale;
const ty = (containerHeight - bounds.height * scale) / 2 - bounds.y * scale;
currentSvg.transition().duration(750).call(
currentZoom.transform,
d3.zoomIdentity.translate(tx, ty).scale(scale)
);
} catch (error) {
console.warn('Center graph failed:', error);
resetZoom();
}
}
function fitToScreen() {
centerGraph();
}
function toggleLabels() {
showLabels = !showLabels;
d3.selectAll('.node-label').classed('hidden', !showLabels);
}
function toggleLinks() {
showLinks = !showLinks;
d3.selectAll('.link').classed('hidden', !showLinks);
}
function toggleDepthCircles() {
showDepthCircles = !showDepthCircles;
d3.selectAll('.depth-circle').classed('hidden', !showDepthCircles);
}
function handleSearch(value) {
currentFilters.searchTerm = value;
applyFilters();
}
function handleDepthChange(value) {
currentFilters.maxDepth = parseInt(value);
const depthValueEl = document.getElementById('depthValue');
if (depthValueEl) {
depthValueEl.textContent = value === '10' ? '∞' : value;
}
applyFilters();
}
function applyFilters() {
const healthFilterEl = document.getElementById('healthFilter');
if (healthFilterEl) {
currentFilters.health = healthFilterEl.value;
}
if (typeof window.renderCurrentLayout === 'function') {
window.renderCurrentLayout();
}
}
function getFilteredNodes(allNodes) {
return allNodes.filter(node => window.nodeMatchesFilters(node, currentFilters));
}
function exportPNG() {
try {
const svgElement = document.getElementById('graph');
if (!svgElement) {
alert('Graph not found');
return;
}
const svgData = new XMLSerializer().serializeToString(svgElement);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
canvas.width = svgElement.clientWidth;
canvas.height = svgElement.clientHeight;
img.onload = function() {
ctx.fillStyle = getComputedStyle(document.body).backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
canvas.toBlob(function(blob) {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `devcompass-graph-${Date.now()}.png`;
link.click();
URL.revokeObjectURL(url);
});
};
img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData)));
} catch (error) {
console.error('PNG export failed:', error);
alert('PNG export failed. Please try again.');
}
}
function exportJSON() {
if (typeof window.graphData === 'undefined') {
alert('No graph data available');
return;
}
const exportData = {
version: '3.2.0',
timestamp: new Date().toISOString(),
nodes: window.graphData.nodes,
links: window.graphData.links,
metadata: window.graphData.metadata || {},
filters: currentFilters
};
window.exportAsJSON(exportData, `devcompass-data-${Date.now()}.json`);
}
function exportReport() {
if (typeof window.graphData === 'undefined') {
alert('No graph data available');
return;
}
const stats = window.getStats();
const healthDist = window.statsManager ? window.statsManager.getHealthDistribution(window.graphData.nodes) : {};
const report = {
title: 'DevCompass Dependency Report',
generated: new Date().toISOString(),
project: {
name: document.getElementById('projectName')?.textContent || 'Unknown',
version: document.getElementById('projectVersion')?.textContent || 'Unknown'
},
statistics: stats,
healthDistribution: healthDist,
nodes: window.graphData.nodes.map(n => ({
name: n.name,
version: n.version,
healthScore: n.healthScore,
depth: n.depth,
issues: n.issues
}))
};
window.exportAsJSON(report, `devcompass-report-${Date.now()}.json`);
}
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(err => {
console.warn('Fullscreen not supported:', err);
});
} else {
document.exitFullscreen();
}
}
function toggleTheme() {
const body = document.body;
const isDark = body.classList.contains('theme-dark');
if (isDark) {
body.classList.remove('theme-dark');
body.classList.add('theme-light');
window.storage.set('theme', 'light');
document.getElementById('themeIcon').textContent = '☀️';
} else {
body.classList.remove('theme-light');
body.classList.add('theme-dark');
window.storage.set('theme', 'dark');
document.getElementById('themeIcon').textContent = '🌙';
}
}
function initTheme() {
const savedTheme = window.storage.get('theme', 'dark');
const body = document.body;
if (savedTheme === 'light') {
body.classList.remove('theme-dark');
body.classList.add('theme-light');
document.getElementById('themeIcon').textContent = '☀️';
} else {
body.classList.add('theme-dark');
document.getElementById('themeIcon').textContent = '🌙';
}
}
function initKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
return;
}
switch(e.key.toLowerCase()) {
case '+':
case '=':
e.preventDefault();
zoomIn();
break;
case '-':
e.preventDefault();
zoomOut();
break;
case 'r':
e.preventDefault();
resetView();
break;
case 'f':
e.preventDefault();
fitToScreen();
break;
case 'l':
e.preventDefault();
toggleLabels();
break;
case 't':
e.preventDefault();
toggleTheme();
break;
case 'escape':
window.hideTooltip();
break;
}
});
}
// Export to window
window.initZoom = initZoom;
window.zoomIn = zoomIn;
window.zoomOut = zoomOut;
window.resetZoom = resetZoom;
window.resetView = resetView;
window.centerGraph = centerGraph;
window.fitToScreen = fitToScreen;
window.toggleLabels = toggleLabels;
window.toggleLinks = toggleLinks;
window.toggleDepthCircles = toggleDepthCircles;
window.handleSearch = handleSearch;
window.handleDepthChange = handleDepthChange;
window.applyFilters = applyFilters;
window.getFilteredNodes = getFilteredNodes;
window.exportPNG = exportPNG;
window.exportJSON = exportJSON;
window.exportReport = exportReport;
window.toggleFullscreen = toggleFullscreen;
window.toggleTheme = toggleTheme;
window.initTheme = initTheme;
window.initKeyboardShortcuts = initKeyboardShortcuts;
window.currentFilters = currentFilters;
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
initZoom,
zoomIn,
zoomOut,
resetZoom,
resetView,
centerGraph,
fitToScreen,
toggleLabels,
toggleLinks,
toggleDepthCircles,
handleSearch,
handleDepthChange,
applyFilters,
getFilteredNodes,
exportPNG,
exportJSON,
exportReport,
toggleFullscreen,
toggleTheme,
initTheme,
initKeyboardShortcuts,
currentFilters
};
}
// src/dashboard/scripts/core.js
(function() {
'use strict';
window.clusters = [];
window.clusterer = null;
window.currentClusterMode = 'ecosystem';
function init() {
console.log('🚀 DevCompass Dashboard v3.2.0 initializing...');
if (typeof window.graphData === 'undefined' || !window.graphData) {
console.error('No graph data available');
showEmptyState();
return;
}
if (!window.graphData.nodes || !Array.isArray(window.graphData.nodes)) {
console.error('Invalid graph data structure');
showEmptyState();
return;
}
console.log(`📊 Loaded ${window.graphData.nodes.length} nodes, ${window.graphData.links?.length || 0} links`);
window.initTheme();
window.initTooltip();
window.initStats();
initClustering();
window.initKeyboardShortcuts();
updateMetadata();
window.switchLayout('tree');
console.log('✅ Dashboard initialized successfully');
}
function initClustering() {
if (typeof DependencyClusterer === 'undefined') {
console.warn('DependencyClusterer not available');
return;
}
try {
window.clusterer = new DependencyClusterer(window.graphData.nodes, window.graphData.links);
updateClusters();
} catch (error) {
console.error('Clustering initialization failed:', error);
}
}
function updateClusters() {
if (!window.clusterer) {
window.clusters = [];
renderClusterList();
return;
}
try {
window.clusters = window.clusterer.clusterBy(window.currentClusterMode);
renderClusterList();
window.updateStats(window.graphData.nodes, window.getFilteredNodes(window.graphData.nodes), window.clusters);
} catch (error) {
console.error('Cluster update failed:', error);
window.clusters = [];
renderClusterList();
}
}
function renderClusterList() {
const container = document.getElementById('clusterList');
if (!container) return;
if (window.clusters.length === 0) {
container.innerHTML = '<div style="text-align: center; color: var(--text-muted); font-size: 0.875rem; padding: 1rem;">No clusters available</div>';
return;
}
container.innerHTML = '';
window.clusters.forEach(cluster => {
const item = document.createElement('div');
item.className = 'cluster-item';
let statsHTML = '';
if (cluster.stats.vulnerable > 0) {
statsHTML += `<span class="cluster-badge vulnerable">🔴 ${cluster.stats.vulnerable}</span>`;
}
if (cluster.stats.deprecated > 0) {
statsHTML += `<span class="cluster-badge deprecated">🟣 ${cluster.stats.deprecated}</span>`;
}
if (cluster.stats.outdated > 0) {
statsHTML += `<span class="cluster-badge outdated">🟡 ${cluster.stats.outdated}</span>`;
}
if (cluster.stats.healthy > 0) {
statsHTML += `<span class="cluster-badge healthy">🟢 ${cluster.stats.healthy}</span>`;
}
item.innerHTML = `
<div class="cluster-header">
<div class="cluster-title">
<span class="cluster-icon">${cluster.icon || '📦'}</span>
<span>${window.truncateText(cluster.name, 20)}</span>
</div>
<span class="cluster-count">${cluster.stats.total}</span>
</div>
${statsHTML ? `<div class="cluster-stats">${statsHTML}</div>` : ''}
`;
item.addEventListener('click', () => window.highlightCluster(cluster));
container.appendChild(item);
});
}
window.highlightCluster = function(cluster) {
const clusterNodeIds = new Set(cluster.nodes.map(n => n.id));
const svg = d3.select('#graph');
svg.selectAll('.node')
.transition()
.duration(300)
.style('opacity', d => {
const nodeId = d.data ? d.data.id : d.id;
return clusterNodeIds.has(nodeId) ? 1 : 0.3;
});
svg.selectAll('.link')
.transition()
.duration(300)
.style('opacity', 0.2);
setTimeout(() => {
svg.selectAll('.node')
.transition()
.duration(500)
.style('opacity', 1);
svg.selectAll('.link')
.transition()
.duration(500)
.style('opacity', 0.6);
}, 3000);
};
window.switchClusterMode = function(mode) {
window.currentClusterMode = mode;
document.querySelectorAll('.cluster-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === mode);
});
updateClusters();
};
function updateMetadata() {
const metadata = window.graphData.metadata || {};
const projectNameEl = document.getElementById('projectName');
if (projectNameEl) {
projectNameEl.textContent = metadata.projectName || 'Unknown Project';
}
const projectVersionEl = document.getElementById('projectVersion');
if (projectVersionEl) {
projectVersionEl.textContent = metadata.projectVersion || '1.0.0';
}
const totalDepsEl = document.getElementById('totalDeps');
if (totalDepsEl) {
totalDepsEl.textContent = window.graphData.nodes.length;
}
}
function showEmptyState() {
const emptyState = document.getElementById('emptyState');
if (emptyState) {
emptyState.style.display = 'flex';
}
const graphSvg = document.getElementById('graph');
if (graphSvg) {
graphSvg.style.display = 'none';
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
window.DevCompass = {
version: '3.2.0',
get graphData() { return window.graphData; },
get clusters() { return window.clusters; },
get currentLayout() { return window.currentLayout; },
switchLayout: window.switchLayout,
switchClusterMode: window.switchClusterMode,
updateStats: window.updateStats,
exportPNG: window.exportPNG,
exportJSON: window.exportJSON,
exportReport: window.exportReport
};
})();
// src/dashboard/scripts/layouts.js
const LayoutEngine = {
tree: function(svg, nodes, links) {
const width = svg.node().clientWidth;
const height = svg.node().clientHeight;
svg.selectAll('*').remove();
const g = svg.append('g');
const hierarchyRoot = this.buildHierarchyFast(nodes, links);
if (!hierarchyRoot) {
this.showEmptyState();
return;
}
const root = d3.hierarchy(hierarchyRoot);
const treeLayout = d3.tree()
.size([height - 200, width - 400])
.separation((a, b) => a.parent === b.parent ? 1.5 : 2);
treeLayout(root);
root.each(d => {
[d.x, d.y] = [d.y + 200, d.x + 100];
});
const linkData = root.links();
const linkGroup = g.append('g').attr('class', 'links');
linkGroup.selectAll('path')
.data(linkData)
.enter()
.append('path')
.attr('class', 'link')
.attr('d', d3.linkHorizontal().x(d => d.x).y(d => d.y));
const nodeData = root.descendants();
const nodeGroup = g.append('g').attr('class', 'nodes');
const node = nodeGroup.selectAll('g')
.data(nodeData)
.enter()
.append('g')
.attr('class', 'node')
.attr('transform', d => `translate(${d.x},${d.y})`)
.on('mouseover', (e, d) => window.showTooltip(e, d))
.on('mouseout', window.hideTooltip);
node.append('circle')
.attr('r', d => this.getNodeRadius(d.data))
.attr('fill', d => window.getHealthColor(d.data))
.attr('stroke', d => window.getNodeStroke(d.data))
.attr('stroke-width', 2);
node.append('text')
.attr('class', 'node-label')
.attr('dy', d => d.data.type === 'root' ? -25 : -15)
.attr('text-anchor', 'middle')
.text(d => this.truncate(d.data.name || d.data.id, 20));
window.initZoom(svg, g);
setTimeout(() => window.fitToScreen(), 50);
},
force: function(svg, nodes, links) {
const width = svg.node().clientWidth;
const height = svg.node().clientHeight;
svg.selectAll('*').remove();
const g = svg.append('g');
const simNodes = nodes.map(n => ({...n}));
const simLinks = links.map(l => ({...l}));
const simulation = d3.forceSimulation(simNodes)
.force('link', d3.forceLink(simLinks)
.id(d => d.id)
.distance(80))
.force('charge', d3.forceManyBody().strength(-250))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(20))
.alphaDecay(0.05)
.velocityDecay(0.4);
const linkGroup = g.append('g');
const link = linkGroup.selectAll('line')
.data(simLinks)
.enter()
.append('line')
.attr('class', 'link')
.attr('stroke-width', 1.5);
const nodeGroup = g.append('g');
const node = nodeGroup.selectAll('g')
.data(simNodes)
.enter()
.append('g')
.attr('class', 'node')
.call(this.dragBehavior(simulation))
.on('mouseover', (e, d) => {
window.showTooltip(e, d);
link.attr('stroke-opacity', l =>
l.source.id === d.id || l.target.id === d.id ? 0.8 : 0.3
);
})
.on('mouseout', () => {
window.hideTooltip();
link.attr('stroke-opacity', 0.6);
});
node.append('circle')
.attr('r', d => this.getNodeRadius(d))
.attr('fill', d => window.getHealthColor(d))
.attr('stroke', d => window.getNodeStroke(d))
.attr('stroke-width', 2);
node.append('text')
.attr('class', 'node-label')
.attr('dy', -12)
.attr('text-anchor', 'middle')
.text(d => this.truncate(d.name || d.id, 15));
simulation.on('tick', () => {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
node.attr('transform', d => `translate(${d.x},${d.y})`);
});
window.initZoom(svg, g);
},
radial: function(svg, nodes, links) {
const width = svg.node().clientWidth;
const height = svg.node().clientHeight;
const centerX = width / 2;
const centerY = height / 2;
svg.selectAll('*').remove();
const g = svg.append('g');
const maxDepth = Math.max(...nodes.map(n => n.depth || 0), 1);
const minRadius = 80;
const maxRadius = Math.min(width, height) / 2 - 100;
const radiusStep = (maxRadius - minRadius) / Math.max(maxDepth, 1);
const getRadius = depth => depth === 0 ? 0 : minRadius + (depth - 1) * radiusStep + radiusStep / 2;
const nodesByDepth = new Map();
nodes.forEach(n => {
const d = n.depth || 0;
if (!nodesByDepth.has(d)) nodesByDepth.set(d, []);
nodesByDepth.get(d).push(n);
});
const positioned = nodes.map(node => {
const depth = node.depth || 0;
if (depth === 0) return {...node, x: centerX, y: centerY, angle: 0};
const atDepth = nodesByDepth.get(depth);
const idx = atDepth.indexOf(node);
const angle = (2 * Math.PI * idx / atDepth.length) + (depth * 0.3);
const r = getRadius(depth);
return {
...node,
x: centerX + r * Math.cos(angle),
y: centerY + r * Math.sin(angle),
angle
};
});
const nodeMap = new Map(positioned.map(n => [n.id, n]));
const circleGroup = g.append('g');
for (let d = 1; d <= maxDepth; d++) {
const r = getRadius(d);
circleGroup.append('circle')
.attr('class', 'depth-circle')
.attr('cx', centerX)
.attr('cy', centerY)
.attr('r', r);
}
const linkGroup = g.append('g');
linkGroup.selectAll('path')
.data(links)
.enter()
.append('path')
.attr('class', 'link')
.attr('d', d => {
const src = nodeMap.get(typeof d.source === 'object' ? d.source.id : d.source);
const tgt = nodeMap.get(typeof d.target === 'object' ? d.target.id : d.target);
if (!src || !tgt) return '';
const mx = (src.x + tgt.x) / 2;
const my = (src.y + tgt.y) / 2;
const cx = mx + (centerX - mx) * 0.2;
const cy = my + (centerY - my) * 0.2;
return `M${src.x},${src.y}Q${cx},${cy} ${tgt.x},${tgt.y}`;
});
const nodeGroup = g.append('g');
const node = nodeGroup.selectAll('g')
.data(positioned)
.enter()
.append('g')
.attr('class', 'node')
.attr('transform', d => `translate(${d.x},${d.y})`)
.on('mouseover', (e, d) => window.showTooltip(e, d))
.on('mouseout', window.hideTooltip);
node.append('circle')
.attr('r', d => this.getNodeRadius(d))
.attr('fill', d => window.getHealthColor(d))
.attr('stroke', d => window.getNodeStroke(d))
.attr('stroke-width', 2);
node.append('text')
.attr('class', 'node-label')
.attr('dy', d => d.depth === 0 ? -28 : (Math.sin(d.angle) > 0.3 ? 20 : -12))
.attr('text-anchor', d => d.depth === 0 ? 'middle' : (Math.cos(d.angle) > 0.3 ? 'start' : 'end'))
.text(d => this.truncate(d.name || d.id, 15));
window.initZoom(svg, g);
},
conflict: function(svg, nodes, links) {
const conflicts = nodes.filter(n =>
n.type === 'root' ||
(n.issues && n.issues.length > 0) ||
(n.healthScore || 10) < 7 ||
n.isVulnerable || n.isDeprecated || n.isOutdated
);
if (conflicts.length <= 1) {
this.showNoConflicts();
return;
}
const ids = new Set(conflicts.map(n => n.id));
const conflictLinks = links.filter(l => {
const sid = typeof l.source === 'object' ? l.source.id : l.source;
const tid = typeof l.target === 'object' ? l.target.id : l.target;
return ids.has(sid) && ids.has(tid);
});
this.force(svg, conflicts, conflictLinks);
},
analytics: function(container, nodes, links) {
const svg = document.getElementById('graph');
if (svg) svg.style.display = 'none';
const view = document.getElementById('analyticsView');
if (!view) return;
view.style.display = 'block';
const grid = view.querySelector('.analytics-grid');
if (!grid) return;
const stats = window.calculateStats(nodes);
const healthDist = this.getHealthDist(nodes);
const depthDist = this.getDepthDist(nodes);
grid.innerHTML = `
${this.summaryCard(stats)}
${this.healthCard(healthDist)}
${this.depthCard(depthDist)}
${this.issuesCard(nodes)}
${this.topPackagesCard(nodes)}
`;
},
buildHierarchyFast: function(nodes, links) {
if (!nodes.length) return null;
const map = new Map();
nodes.forEach(n => map.set(n.id, {...n, children: []}));
const root = nodes.find(n => n.type === 'root' || n.depth === 0) || nodes[0];
const added = new Set();
links.forEach(l => {
const sid = typeof l.source === 'object' ? l.source.id : l.source;
const tid = typeof l.target === 'object' ? l.target.id : l.target;
const parent = map.get(sid);
const child = map.get(tid);
if (parent && child && !added.has(tid)) {
parent.children.push(child);
added.add(tid);
}
});
return map.get(root.id);
},
getNodeRadius: function(node) {
if (node.type === 'root' || node.depth === 0) return 20;
if (node.depth === 1) return 12;
return node.depth === 2 ? 8 : 6;
},
truncate: function(text, max) {
return text && text.length > max ? text.slice(0, max - 3) + '...' : text || '';
},
dragBehavior: function(simulation) {
return d3.drag()
.on('start', e => {
if (!e.active) simulation.alphaTarget(0.3).restart();
e.subject.fx = e.subject.x;
e.subject.fy = e.subject.y;
})
.on('drag', e => {
e.subject.fx = e.x;
e.subject.fy = e.y;
})
.on('end', e => {
if (!e.active) simulation.alphaTarget(0);
e.subject.fx = null;
e.subject.fy = null;
});
},
showEmptyState: function() {
const el = document.getElementById('emptyState');
if (el) el.style.display = 'block';
},
showNoConflicts: function() {
const svg = document.getElementById('graph');
if (svg) {
const w = svg.clientWidth / 2;
const h = svg.clientHeight / 2;
svg.innerHTML = `
<g transform="translate(${w},${h})">
<text text-anchor="middle" style="font-size:3rem;fill:var(--health-excellent)" y="-20">🎉</text>
<text text-anchor="middle" style="font-size:1.5rem;fill:var(--text-primary);font-weight:600" y="20">No Conflicts Found!</text>
<text text-anchor="middle" style="font-size:1rem;fill:var(--text-secondary)" y="50">All dependencies are healthy</text>
</g>
`;
}
},
getHealthDist: function(nodes) {
const d = {excellent: 0, good: 0, caution: 0, warning: 0, critical: 0};
nodes.forEach(n => {
if (n.type === 'root') return;
const s = n.healthScore || 8;
if (s >= 9) d.excellent++;
else if (s >= 7) d.good++;
else if (s >= 5) d.caution++;
else if (s >= 3) d.warning++;
else d.critical++;
});
return d;
},
getDepthDist: function(nodes) {
const d = {};
nodes.forEach(n => {
const depth = n.depth || 0;
d[depth] = (d[depth] || 0) + 1;
});
return d;
},
summaryCard: function(s) {
return `
<div class="analytics-card">
<h3 class="analytics-card-title">📊 Overview</h3>
<div class="analytics-stats">
<div class="analytics-stat-item"><div class="analytics-stat-value">${s.total}</div><div class="analytics-stat-label">Total</div></div>
<div class="analytics-stat-item"><div class="analytics-stat-value stat-success">${s.healthy}</div><div class="analytics-stat-label">Healthy</div></div>
<div class="analytics-stat-item"><div class="analytics-stat-value stat-danger">${s.vulnerable}</div><div class="analytics-stat-label">Vulnerable</div></div>
<div class="analytics-stat-item"><div class="analytics-stat-value stat-warning">${s.outdated}</div><div class="analytics-stat-label">Outdated</div></div>
</div>
</div>
`;
},
healthCard: function(d) {
const total = Object.values(d).reduce((a, b) => a + b, 0);
const bar = (label, count, color) => {
const pct = total > 0 ? Math.round(count / total * 100) : 0;
return `
<div class="analytics-bar-item">
<div class="analytics-bar-label">${label}</div>
<div class="analytics-bar-container">
<div class="analytics-bar-fill" style="width:${pct}%;background:${color}"></div>
<span class="analytics-bar-text">${count} (${pct}%)</span>
</div>
</div>
`;
};
return `
<div class="analytics-card">
<h3 class="analytics-card-title">💊 Health Distribution</h3>
<div class="analytics-distribution">
${bar('Excellent', d.excellent, 'var(--health-excellent)')}
${bar('Good', d.good, 'var(--health-good)')}
${bar('Caution', d.caution, 'var(--health-caution)')}
${bar('Warning', d.warning, 'var(--health-warning)')}
${bar('Critical', d.critical, 'var(--health-critical)')}
</div>
</div>
`;
},
depthCard: function(d) {
const max = Math.max(...Object.values(d));
const items = Object.entries(d).sort((a, b) => +a[0] - +b[0]).map(([depth, count]) => `
<div class="analytics-depth-item">
<span class="analytics-depth-label">Depth ${depth}</span>
<div class="analytics-depth-bar">
<div class="analytics-depth-fill" style="width:${count/max*100}%"></div>
<span class="analytics-depth-count">${count}</span>
</div>
</div>
`).join('');
return `
<div class="analytics-card">
<h3 class="analytics-card-title">📏 Depth Distribution</h3>
<div class="analytics-depth-chart">${items}</div>
</div>
`;
},
issuesCard: function(nodes) {
const types = {};
nodes.forEach(n => {
if (!n.issues) return;
n.issues.forEach(i => {
const t = i.type || 'other';
types[t] = (types[t] || 0) + 1;
});
});
const items = Object.entries(types).sort((a, b) => b[1] - a[1]).map(([type, count]) => `
<div class="analytics-issue-item">
<span class="analytics-issue-type">${type}</span>
<span class="analytics-issue-count">${count}</span>
</div>
`).join('');
return `
<div class="analytics-card">
<h3 class="analytics-card-title">🚨 Issues by Type</h3>
<div class="analytics-issues-list">${items || '<p style="color:var(--text-muted);text-align:center">No issues</p>'}</div>
</div>
`;
},
topPackagesCard: function(nodes) {
const sorted = nodes.filter(n => n.type !== 'root')
.sort((a, b) => (a.healthScore || 8) - (b.healthScore || 8))
.slice(0, 10);
const items = sorted.map(n => `
<div class="analytics-package-item">
<div class="analytics-package-name">${this.truncate(n.name || n.id, 25)}</div>
<div class="analytics-package-score" style="color:${window.getHealthColor(n)}">${n.healthScore || 8}/10</div>
</div>
`).join('');
return `
<div class="analytics-card">
<h3 class="analytics-card-title">⚠️ Needs Attention</h3>
<div class="analytics-packages-list">${items}</div>
</div>
`;
}
};
let currentLayout = 'tree';
function switchLayout(name) {
if (!window.graphData || !window.graphData.nodes || !window.graphData.nodes.length) {
console.warn('No graph data');
return;
}
currentLayout = name;
document.querySelectorAll('.tab-btn').forEach(b => {
b.classList.toggle('active', b.dataset.layout === name);
});
const svg = document.getElementById('graph');
const view = document.getElementById('analyticsView');
const loading = document.getElementById('loading');
if (name === 'analytics') {
if (svg) svg.style.display = 'none';
if (view) view.style.display = 'block';
LayoutEngine.analytics(view, window.graphData.nodes, window.graphData.links);
} else {
if (svg) svg.style.display = 'block';
if (view) view.style.display = 'none';
if (loading) loading.classList.add('visible');
requestAnimationFrame(() => {
renderCurrentLayout();
if (loading) loading.classList.remove('visible');
});
}
}
function renderCurrentLayout() {
if (!window.graphData || currentLayout === 'analytics') return;
const svg = d3.select('#graph');
const filtered = window.getFilteredNodes(window.graphData.nodes);
const ids = new Set(filtered.map(n => n.id));
const filteredLinks = window.graphData.links.filter(l => {
const sid = typeof l.source === 'object' ? l.source.id : l.source;
const tid = typeof l.target === 'object' ? l.target.id : l.target;
return ids.has(sid) && ids.has(tid);
});
LayoutEngine[currentLayout](svg, filtered, filteredLinks);
const clusterArray = window.clusters || [];
window.updateStats(window.graphData.nodes, filtered, clusterArray);
}
// Export to window
window.LayoutEngine = LayoutEngine;
window.switchLayout = switchLayout;
window.renderCurrentLayout = renderCurrentLayout;
window.currentLayout = currentLayout;
if (typeof module !== 'undefined' && module.exports) {
module.exports = {LayoutEngine, switchLayout, renderCurrentLayout, currentLayout};
}
// src/dashboard/scripts/stats.js
class StatsManager {
constructor() {
this.stats = {
total: 0,
visible: 0,
clusters: 0,
vulnerable: 0,
deprecated: 0,
outdated: 0,
unused: 0,
healthy: 0,
maxDepth: 0
};
}
calculate(nodes, visibleNodes = null, clusters = []) {
this.stats.total = nodes.length;
this.stats.visible = visibleNodes ? visibleNodes.length : nodes.length;
this.stats.clusters = clusters.length;
this.stats.vulnerable = 0;
this.stats.deprecated = 0;
this.stats.outdated = 0;
this.stats.unused = 0;
this.stats.healthy = 0;
this.stats.maxDepth = 0;
nodes.forEach(node => {
if (node.type === 'root') return;
if (node.isVulnerable) this.stats.vulnerable++;
if (node.isDeprecated) this.stats.deprecated++;
if (node.isOutdated) this.stats.outdated++;
if (node.isUnused) this.stats.unused++;
const score = node.healthScore || 8;
if (score >= 7 && !node.isVulnerable && !node.isDeprecated) {
this.stats.healthy++;
}
this.stats.maxDepth = Math.max(this.stats.maxDepth, node.depth || 0);
});
return this.stats;
}
updateDisplay() {
this.updateStat('stat-total', this.stats.total);
this.updateStat('stat-visible', this.stats.visible);
this.updateStat('stat-clusters', this.stats.clusters);
this.updateStat('stat-vulnerable', this.stats.vulnerable);
this.updateStat('stat-outdated', this.stats.outdated);
this.updateStat('stat-healthy', this.stats.healthy);
this.updateHeaderMeta();
}
updateStat(elementId, value) {
const element = document.getElementById(elementId);
if (element) {
element.textContent = window.formatNumber(value);
}
}
updateHeaderMeta() {
const totalDepsEl = document.getElementById('totalDeps');
if (totalDepsEl) {
totalDepsEl.textContent = this.stats.total;
}
}
getStats() {
return { ...this.stats };
}
getHealthDistribution(nodes) {
const distribution = {
excellent: 0,
good: 0,
caution: 0,
warning: 0,
critical: 0
};
nodes.forEach(node => {
if (node.type === 'root') return;
const score = node.healthScore || 8;
if (score >= 9) distribution.excellent++;
else if (score >= 7) distribution.good++;
else if (score >= 5) distribution.caution++;
else if (score >= 3) distribution.warning++;
else distribution.critical++;
});
return distribution;
}
getDepthDistribution(nodes) {
const distribution = {};
nodes.forEach(node => {
const depth = node.depth || 0;
distribution[depth] = (distribution[depth] || 0) + 1;
});
return distribution;
}
getIssueSummary(nodes) {
const summary = {
security: 0,
quality: 0,
license: 0,
ecosystem: 0
};
nodes.forEach(node => {
if (!node.issues || !Array.isArray(node.issues)) return;
node.issues.forEach(issue => {
const type = issue.type || 'other';
if (summary.hasOwnProperty(type)) {
summary[type]++;
}
});
});
return summary;
}
exportAsCSV() {
const rows = [
['Metric', 'Value'],
['Total Packages', this.stats.total],
['Visible Packages', this.stats.visible],
['Clusters', this.stats.clusters],
['Vulnerable', this.stats.vulnerable],
['Deprecated', this.stats.deprecated],
['Outdated', this.stats.outdated],
['Unused', this.stats.unused],
['Healthy', this.stats.healthy],
['Max Depth', this.stats.maxDepth]
];
return rows.map(row => row.join(',')).join('\n');
}
}
let statsManager;
function initStats() {
statsManager = new StatsManager();
return statsManager;
}
function updateStats(nodes, visibleNodes, clusters) {
if (!statsManager) statsManager = new StatsManager();
statsManager.calculate(nodes, visibleNodes, clusters);
statsManager.updateDisplay();
}
function getStats() {
if (!statsManager) statsManager = new StatsManager();
return statsManager.getStats();
}
// Export to window
window.StatsManager = StatsManager;
window.initStats = initStats;
window.updateStats = updateStats;
window.getStats = getStats;
window.statsManager = null;
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
StatsManager,
initStats,
updateStats,
getStats
};
}
// src/dashboard/scripts/tooltip.js
class Tooltip {
constructor(elementId = 'tooltip') {
this.element = document.getElementById(elementId);
this.visible = false;
}
show(event, node) {
if (!this.element) return;
const data = node.data || node;
const score = data.healthScore || 8;
const issues = data.issues || [];
let content = `<div class="tooltip-title">${window.truncateText(data.name || data.id, 30)}</div>`;
content += `<div class="tooltip-content">`;
content += `<div class="tooltip-row">
<span class="tooltip-label">Version</span>
<span class="tooltip-value">${data.version || 'N/A'}</span>
</div>`;
content += `<div class="tooltip-row">
<span class="tooltip-label">Health Score</span>
<span class="tooltip-value">${score}/10</span>
</div>`;
content += `<div class="tooltip-row">
<span class="tooltip-label">Depth</span>
<span class="tooltip-value">${data.depth || 0}</span>
</div>`;
if (data.type) {
content += `<div class="tooltip-row">
<span class="tooltip-label">Type</span>
<span class="tooltip-value">${data.type}</span>
</div>`;
}
if (node.children || node._children) {
const childCount = (node.children || node._children || []).length;
content += `<div class="tooltip-row">
<span class="tooltip-label">Children</span>
<span class="tooltip-value">${childCount}</span>
</div>`;
}
if (issues.length > 0) {
content += `<div style="margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid var(--border-color);">`;
issues.forEach(issue => {
const badgeClass = this.getIssueBadgeClass(issue.type);
content += `<span class="tooltip-badge ${badgeClass}">${issue.title || issue.type}</span>`;
});
content += `</div>`;
}
if (data.isVulnerable) {
content += `<span class="tooltip-badge critical">🔴 Vulnerable</span>`;
}
if (data.isDeprecated) {
content += `<span class="tooltip-badge deprecated">🟣 Deprecated</span>`;
}
if (data.isOutdated) {
content += `<span class="tooltip-badge warning">🟡 Outdated</span>`;
}
if (data.isUnused) {
content += `<span class="tooltip-badge info">⚪ Unused</span>`;
}
content += `</div>`;
this.element.innerHTML = content;
this.position(event);
this.element.classList.add('visible');
this.visible = true;
}
hide() {
if (!this.element) return;
this.element.classList.remove('visible');
this.visible = false;
}
position(event) {
if (!this.element) return;
const offset = 15;
const tooltipWidth = this.element.offsetWidth || 300;
const tooltipHeight = this.element.offsetHeight || 200;
let x = event.pageX + offset;
let y = event.pageY - offset;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
if (x + tooltipWidth > viewportWidth) {
x = event.pageX - tooltipWidth - offset;
}
if (y + tooltipHeight > viewportHeight) {
y = event.pageY - tooltipHeight - offset;
}
if (y < 0) {
y = event.pageY + offset;
}
this.element.style.left = x + 'px';
this.element.style.top = y + 'px';
}
getIssueBadgeClass(issueType) {
const typeMap = {
'security': 'critical',
'vulnerability': 'critical',
'deprecated': 'deprecated',
'outdated': 'warning',
'unused': 'info'
};
return typeMap[issueType] || 'info';
}
updatePosition(event) {
if (this.visible) {
this.position(event);
}
}
}
let tooltipInstance;
function initTooltip() {
tooltipInstance = new Tooltip();
return tooltipInstance;
}
function showTooltip(event, node) {
if (!tooltipInstance) tooltipInstance = new Tooltip();
tooltipInstance.show(event, node);
}
function hideTooltip() {
if (tooltipInstance) tooltipInstance.hide();
}
function updateTooltipPosition(event) {
if (tooltipInstance) tooltipInstance.updatePosition(event);
}
// Export to window
window.Tooltip = Tooltip;
window.initTooltip = initTooltip;
window.showTooltip = showTooltip;
window.hideTooltip = hideTooltip;
window.updateTooltipPosition = updateTooltipPosition;
window.tooltipInstance = null;
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
Tooltip,
initTooltip,
showTooltip,
hideTooltip,
updateTooltipPosition
};
}
// src/dashboard/scripts/utils.js
function getHealthColor(node) {
if (node.type === 'root' || node.depth === 0) {
return 'var(--root-color)';
}
const score = node.healthScore || 8;
if (score >= 9) return 'var(--health-excellent)';
if (score >= 7) return 'var(--health-good)';
if (score >= 5) return 'var(--health-caution)';
if (score >= 3) return 'var(--health-warning)';
return 'var(--health-critical)';
}
function getNodeRadius(node) {
if (node.type === 'root' || node.depth === 0) return 20;
if (node.depth === 1) return 12;
if (node.depth === 2) return 8;
return 6;
}
function getNodeStroke(node) {
if (node.type === 'root') return 'var(--accent-blue)';
return 'var(--bg-primary)';
}
function buildHierarchy(nodes, links) {
if (!Array.isArray(nodes) || nodes.length === 0) return null;
const nodeMap = new Map();
nodes.forEach(n => {
nodeMap.set(n.id, { ...n, children: [] });
});
const root = nodes.find(n => n.type === 'root' || n.depth === 0) || nodes[0];
const childrenAdded = new Set();
links.forEach(link => {
const sourceId = typeof link.source === 'object' ? link.source.id : link.source;
const targetId = typeof link.target === 'object' ? link.target.id : link.target;
const parent = nodeMap.get(sourceId);
const child = nodeMap.get(targetId);
if (parent && child && !childrenAdded.has(targetId)) {
parent.children.push(child);
childrenAdded.add(targetId);
}
});
return nodeMap.get(root.id);
}
function formatNumber(num) {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return num.toString();
}
function truncateText(text, maxLength = 20) {
if (!text) return '';
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + '...';
}
function debounce(func, wait = 300) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
function nodeMatchesFilters(node, filters) {
if (filters.health && filters.health !== 'all') {
const score = node.healthScore || 8;
switch (filters.health) {
case 'excellent':
if (score < 9) return false;
break;
case 'good':
if (score < 7 || score >= 9) return false;
break;
case 'caution':
if (score < 5 || score >= 7) return false;
break;
case 'warning':
if (score < 3 || score >= 5) return false;
break;
case 'critical':
if (score >= 3) return false;
break;
}
}
if (filters.maxDepth && filters.maxDepth !== 10) {
if ((node.depth || 0) > filters.maxDepth) return false;
}
if (filters.searchTerm) {
const name = (node.name || node.id || '').toLowerCase();
if (!name.includes(filters.searchTerm.toLowerCase())) return false;
}
return true;
}
function calculateStats(nodes) {
const stats = {
total: nodes.length,
vulnerable: 0,
deprecated: 0,
outdated: 0,
unused: 0,
healthy: 0,
maxDepth: 0
};
nodes.forEach(node => {
if (node.type === 'root') return;
if (node.isVulnerable) stats.vulnerable++;
if (node.isDeprecated) stats.deprecated++;
if (node.isOutdated) stats.outdated++;
if (node.isUnused) stats.unused++;
const score = node.healthScore || 8;
if (score >= 7 && !node.isVulnerable && !node.isDeprecated) {
stats.healthy++;
}
stats.maxDepth = Math.max(stats.maxDepth, node.depth || 0);
});
return stats;
}
function exportAsJSON(data, filename = 'devcompass-export.json') {
const dataStr = JSON.stringify(data, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
}
function getContrastColor(bgColor) {
const color = bgColor.replace('var(--', '').replace(')', '');
const darkColors = ['bg-primary', 'bg-secondary', 'bg-tertiary', 'accent-blue'];
return darkColors.includes(color) ? '#ffffff' : '#000000';
}
const storage = {
set(key, value) {
try {
localStorage.setItem(`devcompass_${key}`, JSON.stringify(value));
} catch (e) {
console.warn('LocalStorage not available:', e);
}
},
get(key, defaultValue = null) {
try {
const item = localStorage.getItem(`devcompass_${key}`);
return item ? JSON.parse(item) : defaultValue;
} catch (e) {
console.warn('LocalStorage not available:', e);
return defaultValue;
}
},
remove(key) {
try {
localStorage.removeItem(`devcompass_${key}`);
} catch (e) {
console.warn('LocalStorage not available:', e);
}
}
};
// Export to window for global access
window.getHealthColor = getHealthColor;
window.getNodeRadius = getNodeRadius;
window.getNodeStroke = getNodeStroke;
window.buildHierarchy = buildHierarchy;
window.formatNumber = formatNumber;
window.truncateText = truncateText;
window.debounce = debounce;
window.deepClone = deepClone;
window.nodeMatchesFilters = nodeMatchesFilters;
window.calculateStats = calculateStats;
window.exportAsJSON = exportAsJSON;
window.getContrastColor = getContrastColor;
window.storage = storage;
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
getHealthColor,
getNodeRadius,
getNodeStroke,
buildHierarchy,
formatNumber,
truncateText,
debounce,
deepClone,
nodeMatchesFilters,
calculateStats,
exportAsJSON,
getContrastColor,
storage
};
}
/* src/dashboard/styles/base.css */
:root {
/* Colors - Dark Theme */
--bg-primary: #0a0e1a;
--bg-secondary: #1a1f35;
--bg-tertiary: #252b42;
--bg-hover: #2d3548;
--text-primary: #e0e6ed;
--text-secondary: #8b92a7;
--text-muted: #64748b;
--border-color: #2a3142;
--border-hover: #3b82f6;
/* Accent Colors */
--accent-blue: #3b82f6;
--accent-cyan: #06b6d4;
--accent-purple: #8b5cf6;
--accent-pink: #ec4899;
/* Health Status Colors */
--health-excellent: #10b981;
--health-good: #84cc16;
--health-caution: #eab308;
--health-warning: #f97316;
--health-critical: #ef4444;
/* Special Colors */
--root-color: #60a5fa;
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
/* Border Radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
/* Shadows */
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.2);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.4);
/* Transitions */
--transition-fast: 0.15s ease;
--transition-normal: 0.2s ease;
--transition-slow: 0.3s ease;
/* Z-Index Layers */
--z-base: 1;
--z-dropdown: 100;
--z-modal: 1000;
--z-tooltip: 10000;
}
.theme-light {
--bg-primary: #f8fafc;
--bg-secondary: #ffffff;
--bg-tertiary: #f1f5f9;
--bg-hover: #e2e8f0;
--text-primary: #0f172a;
--text-secondary: #475569;
--text-muted: #94a3b8;
--border-color: #e2e8f0;
--border-hover: #3b82f6;
}
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
overflow: hidden;
}
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
line-height: 1.2;
margin: 0;
}
h1 { font-size: 1.75rem; }
h2 { font-size: 1.5rem; }
h3 { font-size: 1.25rem; }
h4 { font-size: 1.125rem; }
h5 { font-size: 1rem; }
h6 { font-size: 0.875rem; }
p {
margin: 0;
line-height: 1.6;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: var(--radius-sm);
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-blue);
}
::selection {
background: var(--accent-blue);
color: var(--text-primary);
}
.hidden { display: none !important; }
.invisible { visibility: hidden !important; }
.disabled { opacity: 0.5; pointer-events: none; }
/* src/dashboard/styles/controls.css */
.search-input {
width: 100%;
padding: 0.75rem 1rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
color: var(--text-primary);
font-size: 0.875rem;
outline: none;
transition: all var(--transition-normal);
}
.search-input:focus {
border-color: var(--accent-blue);
background: var(--bg-hover);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.search-input::placeholder {
color: var(--text-muted);
}
.filter-group {
margin-bottom: 1rem;
}
.filter-group:last-child {
margin-bottom: 0;
}
.filter-label {
display: block;
font-size: 0.75rem;
color: var(--text-secondary);
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
.filter-select {
width: 100%;
padding: 0.625rem 1rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
color: var(--text-primary);
font-size: 0.875rem;
cursor: pointer;
outline: none;
transition: all var(--transition-normal);
}
.filter-select:hover {
border-color: var(--border-hover);
background: var(--bg-hover);
}
.filter-select:focus {
border-color: var(--accent-blue);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.slider-container {
display: flex;
align-items: center;
gap: 1rem;
}
.depth-slider {
flex: 1;
height: 4px;
background: var(--bg-tertiary);
border-radius: 2px;
outline: none;
-webkit-appearance: none;
cursor: pointer;
}
.depth-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: var(--accent-blue);
border-radius: 50%;
cursor: pointer;
transition: all var(--transition-normal);
}
.depth-slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2);
}
.depth-slider::-moz-range-thumb {
width: 16px;
height: 16px;
background: var(--accent-blue);
border-radius: 50%;
border: none;
cursor: pointer;
}
.depth-value {
min-width: 30px;
text-align: center;
font-size: 0.875rem;
color: var(--accent-blue);
font-weight: 600;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: var(--text-primary);
cursor: pointer;
transition: color var(--transition-fast);
}
.checkbox-label:hover {
color: var(--accent-blue);
}
.checkbox-label input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
accent-color: var(--accent-blue);
}
.cluster-mode-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
margin-bottom: 1rem;
}
.cluster-btn {
padding: 0.625rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
color: var(--text-secondary);
font-size: 0.8rem;
cursor: pointer;
border-radius: var(--radius-md);
transition: all var(--transition-normal);
display: flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
font-weight: 500;
}
.cluster-btn:hover {
background: var(--bg-hover);
border-color: var(--accent-blue);
color: var(--text-primary);
}
.cluster-btn.active {
background: var(--accent-blue);
border-color: var(--accent-blue);
color: white;
}
.cluster-list {
max-height: 300px;
overflow-y: auto;
}
.cluster-item {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 0.75rem;
margin-bottom: 0.5rem;
cursor: pointer;
transition: all var(--transition-normal);
}
.cluster-item:hover {
background: var(--bg-hover);
border-color: var(--accent-blue);
transform: translateX(-2px);
}
.cluster-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.cluster-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: var(--text-primary);
}
.cluster-count {
background: rgba(59, 130, 246, 0.2);
color: #60a5fa;
padding: 0.15rem 0.5rem;
border-radius: 10px;
font-size: 0.75rem;
font-weight: 600;
}
.cluster-stats {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
font-size: 0.75rem;
}
.cluster-badge {
padding: 0.15rem 0.4rem;
border-radius: var(--radius-sm);
font-weight: 500;
}
.cluster-badge.vulnerable {
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
}
.cluster-badge.deprecated {
background: rgba(139, 92, 246, 0.2);
color: #c4b5fd;
}
.cluster-badge.outdated {
background: rgba(245, 158, 11, 0.2);
color: #fcd34d;
}
.cluster-badge.healthy {
background: rgba(16, 185, 129, 0.2);
color: #6ee7b7;
}
.control-buttons {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.control-btn {
width: 100%;
padding: 0.75rem 1rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
color: var(--text-primary);
font-size: 0.875rem;
cursor: pointer;
transition: all var(--transition-normal);
text-align: left;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.5rem;
}
.control-btn:hover {
background: var(--bg-hover);
border-color: var(--accent-blue);
transform: translateX(-2px);
}
.control-btn:active {
transform: scale(0.98);
}
.control-btn.primary {
background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-cyan) 100%);
border-color: var(--accent-blue);
color: white;
}
.control-btn.primary:hover {
transform: translateX(-2px) scale(1.02);
}
.control-btn span {
font-size: 1rem;
}
.stat-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.stat-item {
background: var(--bg-tertiary);
padding: 0.75rem;
border-radius: var(--radius-md);
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
transition: all var(--transition-normal);
}
.stat-item:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-sm);
}
.stat-label {
font-size: 0.75rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 1.5rem;
font-weight: 700;
color: var(--text-primary);
}
.stat-value.stat-danger { color: var(--health-critical); }
.stat-value.stat-warning { color: var(--health-warning); }
.stat-value.stat-success { color: var(--health-excellent); }
.stat-value.stat-cluster { color: var(--accent-blue); }
.legend {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.875rem;
color: var(--text-secondary);
transition: all var(--transition-fast);
}
.legend-item:hover {
color: var(--text-primary);
}
.legend-dot {
width: 16px;
height: 16px;
border-radius: 50%;
flex-shrink: 0;
border: 2px solid var(--bg-primary);
box-shadow: var(--shadow-sm);
}
.legend-dot.health-excellent { background: var(--health-excellent); }
.legend-dot.health-good { background: var(--health-good); }
.legend-dot.health-caution { background: var(--health-caution); }
.legend-dot.health-warning { background: var(--health-warning); }
.legend-dot.health-critical { background: var(--health-critical); }
.legend-dot.root-node { background: var(--root-color); }
.zoom-controls {
position: fixed;
bottom: 2rem;
right: 2rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: var(--z-dropdown);
}
.zoom-btn {
width: 48px;
height: 48px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
color: var(--text-primary);
font-size: 1.25rem;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--transition-normal);
backdrop-filter: blur(12px);
box-shadow: var(--shadow-md);
}
.zoom-btn:hover {
background: var(--accent-blue);
border-color: var(--accent-blue);
transform: scale(1.1);
color: white;
}
.zoom-level {
width: 48px;
padding: 0.5rem;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
text-align: center;
color: var(--accent-cyan);
font-size: 0.75rem;
font-weight: 700;
backdrop-filter: blur(12px);
box-shadow: var(--shadow-md);
}
/* src/dashboard/styles/graph.css */
.node {
cursor: pointer;
transition: all var(--transition-fast);
}
.node:hover {
filter: brightness(1.3);
}
.node circle {
stroke-width: 2px;
transition: all var(--transition-normal);
}
.node:hover circle {
stroke-width: 3px;
filter: drop-shadow(0 0 8px currentColor);
}
.node.selected circle {
stroke: var(--accent-cyan);
stroke-width: 4px;
}
.node-label {
font-size: 11px;
fill: var(--text-secondary);
pointer-events: none;
text-anchor: middle;
font-weight: 500;
user-select: none;
}
.node-label.hidden {
display: none;
}
.link {
stroke: var(--border-color);
stroke-opacity: 0.6;
fill: none;
stroke-width: 1.5px;
transition: all var(--transition-fast);
}
.link:hover {
stroke: var(--accent-cyan);
stroke-opacity: 1;
stroke-width: 2px;
}
.link.highlighted {
stroke: var(--accent-cyan);
stroke-opacity: 0.8;
stroke-width: 2.5px;
}
.link.circular {
stroke: var(--health-critical);
stroke-dasharray: 5, 5;
stroke-opacity: 0.8;
}
.link.hidden {
display: none;
}
.depth-circle {
fill: none;
stroke: var(--border-color);
stroke-width: 1px;
stroke-dasharray: 4, 4;
opacity: 0.4;
transition: opacity var(--transition-normal);
}
.depth-circle.hidden {
opacity: 0;
}
.depth-label {
fill: var(--text-muted);
font-size: 10px;
font-weight: 500;
pointer-events: none;
}
.tooltip {
position: absolute;
background: rgba(15, 18, 25, 0.98);
border: 1px solid var(--accent-blue);
border-radius: var(--radius-lg);
padding: 1rem 1.25rem;
pointer-events: none;
opacity: 0;
transition: opacity var(--transition-normal), transform var(--transition-normal);
max-width: 350px;
z-index: var(--z-tooltip);
backdrop-filter: blur(10px);
box-shadow: var(--shadow-lg);
transform: translateY(-10px);
}
.tooltip.visible {
opacity: 1;
transform: translateY(0);
}
.tooltip-title {
font-size: 1rem;
font-weight: 600;
color: var(--accent-cyan);
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border-color);
}
.tooltip-content {
font-size: 0.875rem;
color: var(--text-secondary);
line-height: 1.6;
}
.tooltip-row {
display: flex;
justify-content: space-between;
margin: 0.4rem 0;
gap: 1rem;
}
.tooltip-label {
color: var(--text-secondary);
font-weight: 500;
}
.tooltip-value {
color: var(--text-primary);
font-weight: 600;
}
.tooltip-badge {
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: var(--radius-sm);
font-size: 0.75rem;
font-weight: 600;
margin-right: 0.25rem;
}
.tooltip-badge.critical {
background: rgba(239, 68, 68, 0.2);
color: var(--health-critical);
}
.tooltip-badge.warning {
background: rgba(245, 158, 11, 0.2);
color: var(--health-warning);
}
.tooltip-badge.info {
background: rgba(59, 130, 246, 0.2);
color: var(--accent-blue);
}
.tooltip-badge.deprecated {
background: rgba(139, 92, 246, 0.2);
color: var(--accent-purple);
}
.tooltip-badge.success {
background: rgba(16, 185, 129, 0.2);
color: var(--health-excellent);
}
/* src/dashboard/styles/layout.css */
.header {
background: linear-gradient(135deg, var(--bg-secondary) 0%, #0f1219 100%);
border-bottom: 1px solid var(--border-color);
padding: 1rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: var(--z-dropdown);
height: 80px;
box-shadow: var(--shadow-md);
}
.header-left {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.header-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
}
.header-icon {
font-size: 1.5rem;
}
.version-badge {
font-size: 0.7rem;
background: var(--accent-blue);
padding: 0.15rem 0.5rem;
border-radius: var(--radius-sm);
font-weight: 500;
}
.header-meta {
display: flex;
gap: 1.5rem;
font-size: 0.75rem;
color: var(--text-secondary);
}
.header-meta span {
display: flex;
align-items: center;
gap: 0.25rem;
}
.header-right {
display: flex;
align-items: center;
gap: 1rem;
}
.layout-tabs {
display: flex;
gap: 0.25rem;
background: var(--bg-tertiary);
padding: 3px;
border-radius: var(--radius-md);
}
.tab-btn {
padding: 0.5rem 1rem;
border: none;
background: transparent;
color: var(--text-secondary);
font-size: 0.875rem;
cursor: pointer;
border-radius: var(--radius-sm);
transition: all var(--transition-normal);
font-weight: 500;
display: flex;
align-items: center;
gap: 0.5rem;
}
.tab-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.tab-btn.active {
background: var(--accent-blue);
color: white;
}
.tab-btn span {
font-size: 1rem;
}
.icon-btn {
width: 40px;
height: 40px;
border: 1px solid var(--border-color);
background: var(--bg-tertiary);
border-radius: var(--radius-md);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
transition: all var(--transition-normal);
}
.icon-btn:hover {
background: var(--bg-hover);
border-color: var(--border-hover);
}
.main-container {
display: flex;
height: calc(100vh - 80px);
overflow: hidden;
}
.sidebar {
background: var(--bg-secondary);
overflow-y: auto;
overflow-x: hidden;
padding: 1.5rem;
flex-shrink: 0;
}
.sidebar-left {
width: 320px;
border-right: 1px solid var(--border-color);
}
.sidebar-right {
width: 280px;
border-left: 1px solid var(--border-color);
}
.sidebar-section {
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.sidebar-section:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.sidebar-title {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.graph-container {
flex: 1;
position: relative;
overflow: hidden;
background: var(--bg-primary);
}
.graph-svg {
width: 100%;
height: 100%;
cursor: grab;
}
.graph-svg:active {
cursor: grabbing;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
display: none;
z-index: var(--z-dropdown);
}
.loading.visible {
display: block;
}
.spinner {
width: 50px;
height: 50px;
border: 3px solid var(--border-color);
border-top-color: var(--accent-blue);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
color: var(--text-secondary);
font-size: 0.875rem;
}
.empty-state {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
max-width: 400px;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.empty-title {
font-size: 1.5rem;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.empty-text {
color: var(--text-secondary);
font-size: 0.875rem;
}
.analytics-container {
padding: 2rem;
overflow-y: auto;
height: 100%;
}
.analytics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.analytics-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 1.5rem;
transition: all var(--transition-normal);
}
.analytics-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-md);
}
.analytics-card-title {
font-size: 1.125rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 1.25rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--border-color);
}
.analytics-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.analytics-stat-item {
text-align: center;
}
.analytics-stat-value {
font-size: 2rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.25rem;
}
.analytics-stat-label {
font-size: 0.75rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.analytics-distribution,
.analytics-depth-chart {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.analytics-bar-item,
.analytics-depth-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.analytics-bar-label,
.analytics-depth-label {
font-size: 0.875rem;
color: var(--text-secondary);
font-weight: 500;
}
.analytics-bar-container,
.analytics-depth-bar {
position: relative;
height: 28px;
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
overflow: hidden;
}
.analytics-bar-fill,
.analytics-depth-fill {
height: 100%;
border-radius: var(--radius-sm);
transition: width 0.5s ease;
}
.analytics-bar-text,
.analytics-depth-count {
position: absolute;
left: 0.75rem;
top: 50%;
transform: translateY(-50%);
font-size: 0.75rem;
font-weight: 600;
color: var(--text-primary);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.analytics-issues-list,
.analytics-packages-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
max-height: 300px;
overflow-y: auto;
}
.analytics-issue-item,
.analytics-package-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0.75rem;
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
}
.analytics-issue-item:hover,
.analytics-package-item:hover {
background: var(--bg-hover);
transform: translateX(4px);
}
.analytics-issue-type,
.analytics-package-name {
font-size: 0.875rem;
color: var(--text-primary);
font-weight: 500;
}
.analytics-issue-count,
.analytics-package-score {
font-size: 0.875rem;
font-weight: 700;
padding: 0.25rem 0.5rem;
background: var(--bg-primary);
border-radius: var(--radius-sm);
}
@media (max-width: 1280px) {
.sidebar-left { width: 280px; }
.sidebar-right { width: 260px; }
}
@media (max-width: 1024px) {
.header {
flex-direction: column;
height: auto;
gap: 1rem;
padding: 1rem;
}
.header-meta { flex-wrap: wrap; }
}
@media (max-width: 768px) {
.layout-tabs {
width: 100%;
overflow-x: auto;
}
.tab-btn span { display: none; }
}
/* src/dashboard/styles/themes.css */
.theme-light {
--bg-primary: #f8fafc;
--bg-secondary: #ffffff;
--bg-tertiary: #f1f5f9;
--bg-hover: #e2e8f0;
--text-primary: #0f172a;
--text-secondary: #475569;
--text-muted: #94a3b8;
--border-color: #e2e8f0;
--border-hover: #3b82f6;
}
.theme-light .node-label {
fill: #475569;
text-shadow:
-1px -1px 2px white,
1px -1px 2px white,
-1px 1px 2px white,
1px 1px 2px white;
}
.theme-light .link {
stroke: #cbd5e1;
}
.theme-light .depth-circle {
stroke: #cbd5e1;
}
.theme-light .tooltip {
background: rgba(255, 255, 255, 0.98);
border-color: var(--accent-blue);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
}
.theme-light .tooltip-title {
border-bottom-color: #e2e8f0;
}
body {
transition: background-color 0.3s ease, color 0.3s ease;
}
.header,
.sidebar,
.sidebar-section,
.tab-btn,
.control-btn,
.stat-item,
.cluster-item,
.search-input,
.filter-select {
transition: background-color 0.3s ease,
border-color 0.3s ease,
color 0.3s ease;
}
+2
-2
MIT License
Copyright (c) 2025 Ajay Thorat
Copyright (c) 2025-2026 Ajay Thorat

@@ -21,2 +21,2 @@ Permission is hereby granted, free of charge, to any person obtaining a copy

OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
{
"name": "devcompass",
"version": "3.1.7",
"description": "Dependency health checker with ecosystem intelligence, user-configurable GitHub Personal Access Token support, real-time GitHub issue tracking for 502 popular npm packages, unified interactive dependency graph with dynamic layout switching, intelligent clustering (Ecosystem/Health/Depth grouping), real-time filtering, advanced zoom controls, supply chain security with auto-fix, license conflict resolution with auto-fix, package quality auto-fix, batch fix modes, backup & rollback, and professional dependency exploration - all in a single interactive HTML file.",
"version": "3.2.0",
"description": "Dependency health checker with ecosystem intelligence, unified interactive dashboard with 5 dynamic layouts (Tree/Force/Radial/Conflict/Analytics), modular CSS/JS architecture, intelligent clustering, real-time filtering, advanced zoom controls, theme support (dark/light), supply chain security with auto-fix, license conflict resolution, package quality auto-fix, batch fix modes, backup & rollback, and professional dependency exploration.",
"main": "src/index.js",

@@ -102,2 +102,3 @@ "bin": {

"conflict-view",
"analytics-dashboard",
"graph-export",

@@ -141,3 +142,12 @@ "graph-search",

"ecosystem-detection",
"package-ecosystem-mapping"
"package-ecosystem-mapping",
"unified-dashboard",
"modular-architecture",
"theme-support",
"dark-mode",
"light-mode",
"interactive-analytics",
"5-layouts",
"no-duplication",
"performance-optimized"
],

@@ -148,3 +158,2 @@ "author": "Ajay Thorat <ajaythorat988@gmail.com>",

"chalk": "4.1.2",
"commander": "11.1.0",
"knip": "^5.88.1",

@@ -151,0 +160,0 @@ "npm-check-updates": "^20.0.0",

+457
-237
# 🧭 DevCompass
**Dependency health checker with ecosystem intelligence, user-configurable GitHub Personal Access Token support, real-time GitHub issue tracking for 502 popular npm packages, unified interactive dependency graph with intelligent clustering (Ecosystem/Health/Depth grouping), dynamic layout switching, real-time filtering, advanced zoom controls, supply chain security with auto-fix, license conflict resolution with auto-fix, package quality auto-fix, batch fix modes with granular control, backup & rollback, and professional dependency exploration - all in a single interactive HTML file.**
**Dependency health checker with unified interactive dashboard featuring 5 dynamic layouts (Tree/Force/Radial/Conflict/Analytics), modular CSS/JS architecture, intelligent clustering (Ecosystem/Health/Depth grouping), real-time filtering, advanced zoom controls, theme support (dark/light), supply chain security with auto-fix, license conflict resolution, package quality auto-fix, batch fix modes, backup & rollback, and professional dependency exploration.**

@@ -9,90 +9,293 @@ [![npm version](https://img.shields.io/npm/v/devcompass.svg)](https://www.npmjs.com/package/devcompass)

Analyze your JavaScript projects to find unused dependencies, outdated packages, **detect security vulnerabilities**, **monitor GitHub issues in real-time for 502 packages**, **configure your own GitHub token to avoid rate limits**, **visualize dependency graphs with intelligent clustering**, **organize packages by ecosystem (React/Vue/Angular/Testing/Build Tools)**, **group by health status (Critical/Warning/Healthy)**, **analyze by depth levels**, **instant layout switching**, **real-time filtering**, **advanced zoom controls**, **check bundle sizes**, **verify licenses**, **detect and auto-fix supply chain attacks**, **resolve license conflicts automatically**, **replace abandoned/deprecated packages automatically**, **analyze package quality**, **batch fix with granular control**, **manage backups and rollback changes**, and **automatically fix issues with dry-run, progress tracking, and backups**. Perfect for **CI/CD pipelines** with JSON output and exit codes.
Analyze your JavaScript projects to find unused dependencies, outdated packages, **detect security vulnerabilities**, **monitor GitHub issues in real-time for 502 packages**, **configure your own GitHub token to avoid rate limits**, **customize all configuration via JSON files**, **visualize dependency graphs with 5 dynamic layouts including Analytics dashboard**, **modular architecture with zero code duplication**, **organize packages by ecosystem (React/Vue/Angular/Testing/Build Tools)**, **group by health status (Critical/Warning/Healthy)**, **analyze by depth levels**, **instant layout switching**, **dark/light theme toggle**, **real-time filtering**, **advanced zoom controls**, **check bundle sizes**, **verify licenses**, **detect and auto-fix supply chain attacks**, **resolve license conflicts automatically**, **replace abandoned/deprecated packages automatically**, **analyze package quality**, **batch fix with granular control**, **manage backups and rollback changes**, and **automatically fix issues with dry-run, progress tracking, and backups**. Perfect for **CI/CD pipelines** with JSON output and exit codes.
> **🔲 LATEST v3.1.6:** Intelligent Clustering - Organize packages by Ecosystem, Health, or Depth! 🔲
> **🔑 v3.1.5:** GitHub Personal Access Token Support - Configure your own token to bypass rate limits! 🔑
> **🎨 v3.1.4:** Unified Interactive Graph System - 40+ files → 1 file with dynamic controls! 🎨
> **🎨 LATEST v3.2.0:** Unified Dashboard Architecture - 50% less code, 5 layouts, dark/light themes! 🎨
> **🔧 v3.1.7:** Dynamic Data Configuration - Scalable JSON-based configuration system! 🔧
> **🔲 v3.1.6:** Intelligent Clustering - Organize packages by Ecosystem, Health, or Depth! 🔲
> **🔑 v3.1.5:** GitHub Personal Access Token Support - Configure your own token to bypass rate limits! 🔑
## 🎉 Latest Release: v3.1.6 (2026-04-22)
## 🎉 Latest Release: v3.2.0 (2026-04-25)
**Intelligent Dependency Clustering - Organize & Understand Your Dependencies!**
**Unified Dashboard Architecture - Modular, Scalable, Beautiful!**
### 🌟 What's New in v3.1.6:
### 🌟 What's New in v3.2.0:
#### **🔲 Smart Clustering System**
#### **🎨 Unified Dashboard Architecture**
Organize your dependencies into meaningful groups for better understanding and management.
Complete refactor from 4 duplicated layout files (3,600 lines) to unified modular dashboard (1,800 lines).
**Three Clustering Modes:**
**Benefits:**
- ✅ **50% code reduction** - Easier to maintain
- ✅ **Single source of truth** - Update once, applies everywhere
- ✅ **4× faster updates** - No more copy-paste across files
- ✅ **Zero duplication** - CSS/JS shared across all layouts
- ✅ **Fully backward compatible** - No breaking changes
1. **⚛️ Ecosystem Clustering**
- Groups packages by technology (React, Vue, Angular, Testing, Build Tools, etc.)
- Automatically detects 12 ecosystems
- Perfect for understanding your tech stack
**New Structure:**
2. **🏥 Health Clustering**
- Groups by health status (Critical Issues, Needs Attention, Healthy)
- Color-coded health indicators
- Quick identification of problem areas
```
src/dashboard/
├── index.html # Main template (11KB)
├── scripts/ # 6 modular JS files
│ ├── core.js # Initialization
│ ├── layouts.js # ALL 5 layouts in one file
│ ├── controls.js # Zoom, filters, exports
│ ├── tooltip.js # Tooltip management
│ ├── stats.js # Statistics calculations
│ └── utils.js # Shared utilities
└── styles/ # 5 modular CSS files
├── base.css # Variables, reset
├── layout.css # Header, sidebars, grid
├── controls.css # Buttons, filters, inputs
├── graph.css # Nodes, links, tooltips
└── themes.css # Dark/light theme support
```
3. **📊 Depth Clustering**
- Groups by dependency levels (Direct → Level 1 → Level 2, etc.)
- Understand dependency chains
- Visualize transitive dependencies
**Removed (Consolidated):**
- ❌ `src/graph/layouts/tree.js` (800 lines)
- ❌ `src/graph/layouts/force.js` (700 lines)
- ❌ `src/graph/layouts/radial.js` (650 lines)
- ❌ `src/graph/layouts/conflict.js` (600 lines)
- ❌ `src/graph/template.html` (900 lines)
**Features:**
- ✅ **Sidebar Organization** - Clean categorized list of all packages
- ✅ **Health Statistics** - See vulnerable/deprecated/outdated/healthy counts per cluster
- ✅ **Click to Highlight** - Click any cluster to highlight related packages (3 seconds)
- ✅ **Switch Modes Instantly** - Three buttons to change grouping
- ✅ **Graph Unchanged** - All nodes always visible, layouts work normally
- ✅ **No Page Reload** - Everything happens in real-time
#### **📊 NEW: Analytics Layout**
#### **📊 Cluster Statistics**
Fifth layout added - comprehensive statistics dashboard with 5 cards:
Each cluster shows:
- 📦 Package count
- 🔴 Vulnerable count
- 🟣 Deprecated count
- 🟡 Outdated count
- 🟢 Healthy count
1. **📊 Overview** - Total/Healthy/Vulnerable/Outdated at a glance
2. **💊 Health Distribution** - Bar chart showing package health breakdown
3. **📏 Depth Distribution** - Dependency depth visualization
4. **🚨 Issues by Type** - Categorized issue summary
5. **⚠️ Needs Attention** - Top 10 packages requiring fixes
#### **🎯 Example Use Cases**
**Access:** Click **📊 Analytics** tab in header
**"Which testing tools do I have?"**
#### **🎨 Dark/Light Theme Support**
Toggle between dark and light themes with one click:
- **Dark Theme** (default) - Professional dark UI
- **Light Theme** - Clean, bright interface
- **Persisted** - Saves preference in localStorage
- **Smooth Transitions** - Beautiful theme switching
- **Toggle Button** - 🌙 / ☀️ in header
#### **⚡ Performance Optimizations**
Massive speed improvements across all layouts:
- **Tree Layout** - 5× faster rendering
- **Force Layout** - 4× faster simulation
- **Radial Layout** - 4× faster positioning
- **Analytics Layout** - 6× faster card generation
**Optimizations:**
- Pre-calculated node positions
- Batch DOM operations
- Optimized D3 selections
- Reduced transition durations
- Deferred expensive operations
### 📊 Code Metrics
| Metric | v3.1.7 | v3.2.0 | Improvement |
|--------|--------|--------|-------------|
| Total Lines | 3,600 | 1,800 | **-50%** |
| Layout Files | 4 files | 1 file | **-75%** |
| CSS Duplication | 4× | 1× shared | **-75%** |
| JS Duplication | 4× | 1× engine | **-75%** |
| Files | 5 files | 12 files | Better organized |
| Layouts | 4 layouts | **5 layouts** | +25% |
| Themes | None | **2 themes** | New feature |
| Maintainability | Update 4 places | Update 1 place | **4× easier** |
### 🚀 Upgrade Now:
```bash
devcompass graph --open
# Click "Ecosystem" → See "Testing Tools (15 packages)"
npm install -g devcompass@3.2.0
```
**"Show me all critical issues"**
### 💡 What Changed for Users:
**Nothing! (100% Backward Compatible)**
All commands work exactly the same:
```bash
devcompass graph --open
# Click "Health" → See "Critical Issues (8 packages)"
devcompass graph --open # Same command, better UI!
```
**"What are my direct dependencies?"**
**New Features:**
- ✅ **Analytics tab** - Click to see dashboard
- ✅ **Theme toggle** - Click 🌙/☀️ button
- ✅ **Faster rendering** - 4-6× performance boost
---
## 🎨 Unified Graph Visualization (v3.2.0)
DevCompass features a **revolutionary unified interactive dashboard** - 5 layouts, intelligent clustering, theme support, and all controls in one beautiful interface!
### 🎯 One Command, Everything Included
```bash
# Generate unified interactive dashboard
devcompass graph --open
# Click "Depth" → See "Direct Dependencies (42 packages)"
```
### 🚀 Upgrade Now:
**What you get in ONE file:**
- ✅ **5 layouts** (Tree/Force/Radial/Conflict/Analytics) - switchable via tabs
- ✅ **5 filters** (All/Vulnerable/Outdated/Deprecated/Unused) - instant filtering
- ✅ **3 clustering modes** (Ecosystem/Health/Depth) - organize packages
- ✅ **2 themes** (Dark/Light) - toggle with one click
- ✅ Depth slider (1-10, ∞)
- ✅ Live search
- ✅ Advanced zoom controls
- ✅ Export (PNG/JSON/Report)
- ✅ Live statistics
- ✅ Professional UI
- ✅ **No page reload!**
- ✅ **50% less code** - Better performance
### 🎮 Interactive Controls
#### **Layout Switcher**
Click tabs to switch between:
1. **🌳 Tree** - Hierarchical view (root → children)
2. **⚡ Force** - Physics-based network (drag nodes!)
3. **🌐 Radial** - Circular view (root at center)
4. **⚠️ Conflict** - Issues-only view
5. **📊 Analytics** - Statistics dashboard (NEW!)
#### **Theme Toggle (NEW!)**
Click **🌙 / ☀️** button to switch themes:
- **🌙 Dark Theme** - Professional dark interface (default)
- **☀️ Light Theme** - Clean, bright interface
- Preference saved automatically
- Smooth transitions
#### **Filter Controls**
Click to filter packages:
- **All** - Show everything
- **Vulnerable** - Security issues only
- **Outdated** - Update available
- **Deprecated** - Officially deprecated
- **Unused** - Unused dependencies
#### **Clustering**
Click to organize packages:
- **⚛️ Ecosystem** - Group by technology
- **🏥 Health** - Group by status
- **📊 Depth** - Group by level
#### **Other Controls**
- **Depth Slider** - Control dependency depth (1-∞)
- **Search** - Find packages instantly
- **Zoom** - In/Out/Reset/Fit/Center
- **Export** - PNG image, JSON data, or full report
- **Keyboard Shortcuts** - +/- zoom, R reset, F fit, L labels, T theme
---
## ✨ All Features
- 🎨 **Unified Dashboard** (v3.2.0) - 5 layouts, modular architecture
- 📊 **Analytics Layout** (v3.2.0) - Statistics dashboard
- 🌙 **Theme Support** (v3.2.0) - Dark/light mode toggle
- ⚡ **Performance** (v3.2.0) - 4-6× faster rendering
- 🔧 **Dynamic Data Configuration** (v3.1.7) - JSON-based scalable config
- 🔲 **Intelligent Clustering** (v3.1.6) - Ecosystem/Health/Depth grouping
- 🔑 **GitHub Token Config** (v3.1.5) - User tokens, no rate limits
- 📦 **502 Tracked Packages** (v3.1.5) - Comprehensive monitoring
- 🔄 **Dynamic Issues** (v3.1.2) - Real-time npm audit
- 🛡️ **Production Safety** (v3.1.1) - Bug fixes and hardening
- 📊 **Multiple Layouts** (v3.1.0) - Tree/Force/Radial/Conflict
- 📦 **Batch Fix Modes** (v2.8.5) - Granular control
- 💾 **Backup & Rollback** (v2.8.4) - Safe management
- 📦 **Quality Auto-Fix** (v2.8.3) - Replace abandoned packages
- ⚖️ **License Auto-Fix** (v2.8.2) - GPL/AGPL replacement
- 🛡️ **Supply Chain Auto-Fix** (v2.8.1) - Remove malicious packages
- 🛡️ **Security Analysis** (v2.7) - Comprehensive security
- ⚡ **Parallel Processing** (v2.6) - 80% faster
- 🔮 **GitHub Tracking** (v2.4) - Real-time package health
- 🚀 **CI/CD Integration** (v2.2) - JSON output, exit codes
## 🚀 Installation
```bash
npm install -g devcompass@3.1.6
# Global (recommended)
npm install -g devcompass@3.2.0
# Local
npm install --save-dev devcompass@3.2.0
# One-time use
npx devcompass@3.2.0 analyze
# Upgrade from any version
npm install -g devcompass@3.2.0
```
### 📈 What's Included:
## 📖 Usage
All features from previous versions PLUS clustering:
- ✅ Unified interactive graph (v3.1.4)
- ✅ GitHub token configuration (v3.1.5)
- ✅ 502 tracked packages (v3.1.5)
- ✅ **NEW: Intelligent clustering (v3.1.6)**
- ✅ 4 layouts (Tree/Force/Radial/Conflict)
- ✅ 5 filters (All/Vulnerable/Outdated/Deprecated/Unused)
- ✅ Advanced zoom controls
- ✅ Export (PNG/JSON)
### Basic Commands
```bash
# Configure GitHub token (recommended)
devcompass config --github-token <your-token>
devcompass config --show
# Analyze project
devcompass analyze
# Generate graph (with 5 layouts + themes!)
devcompass graph --open
# Auto-fix issues
devcompass fix
devcompass fix --batch
devcompass fix --dry-run
# Batch modes
devcompass fix --batch-mode critical
devcompass fix --batch-mode high
devcompass fix --batch-mode all
# Category-specific
devcompass fix --only quality
devcompass fix --skip updates
# Manage backups
devcompass backup list
devcompass backup restore --name <backup>
# CI/CD
devcompass analyze --json
devcompass analyze --ci
```
### Graph Commands
```bash
# Generate unified dashboard
devcompass graph
# Open in browser
devcompass graph --open
# Custom output
devcompass graph --output my-deps.html --open
# JSON export
devcompass graph --format json --output data.json
```
---

@@ -249,155 +452,113 @@

## 🎨 Unified Graph Visualization (v3.1.4)
## 🔧 Configuration Files (v3.1.7)
DevCompass features a **revolutionary unified interactive graph** - all layouts, filters, clustering, and controls in one beautiful interface!
All configuration is now in external JSON files for easy customization!
### 🎯 One Command, Everything Included
### Data Files Location
```bash
# Generate unified interactive graph
devcompass graph --open
```
devcompass/
├── data/
│ ├── licenses.json # License categorization
│ ├── priorities.json # Severity levels
│ ├── knip-config.json # Unused deps config
│ ├── license-risks.json # License risk matrix
│ ├── gpl-alternatives.json # GPL replacements
│ ├── quality-alternatives.json # Deprecated alternatives
│ ├── popular-packages.json # Typosquatting detection
│ ├── batch-categories.json # Fix categories
│ └── tracked-repos.json # GitHub tracked packages (502)
```
**What you get in ONE file:**
- ✅ 4 layouts (Tree/Force/Radial/Conflict) - switchable via buttons
- ✅ 5 filters (All/Vulnerable/Outdated/Deprecated/Unused) - instant filtering
- ✅ **3 clustering modes (Ecosystem/Health/Depth) - NEW in v3.1.6!**
- ✅ Depth slider (1-10, ∞)
- ✅ Live search
- ✅ Advanced zoom controls
- ✅ Export (PNG/JSON)
- ✅ Fullscreen mode
- ✅ Live statistics
- ✅ Professional UI
- ✅ **No page reload!**
### Customization Examples
### 🎮 Interactive Controls
**1. Add Custom License to Detection:**
#### **Layout Switcher**
Edit `data/licenses.json`:
```json
{
"restrictive": [
"GPL", "GPL-2.0", "GPL-3.0",
"AGPL", "AGPL-3.0",
"SSPL",
"YOUR-CUSTOM-LICENSE"
],
"permissive": [
"MIT", "Apache-2.0", "BSD-2-Clause",
"BSD-3-Clause", "ISC", "0BSD", "Unlicense"
]
}
```
Click to switch between:
**2. Whitelist Internal Packages:**
1. **🌳 Tree** - Hierarchical view (root → children)
2. **🌀 Force** - Physics-based network (drag nodes!)
3. **⭕ Radial** - Circular view (root at center)
4. **⚠️ Conflict** - Issues-only view
Edit `data/popular-packages.json`:
```json
{
"packages": ["express", "react", "lodash", ...],
"whitelist": [
"chalk", "ora", "inquirer",
"your-internal-package",
"your-company-library"
]
}
```
#### **Filter Controls**
**3. Adjust Severity Colors:**
Click to filter packages:
Edit `data/priorities.json`:
```json
{
"CRITICAL": {
"level": 1,
"label": "CRITICAL",
"color": "red",
"emoji": "🔴"
},
"HIGH": {
"level": 2,
"label": "HIGH",
"color": "orange",
"emoji": "🟠"
}
}
```
- **All** - Show everything
- **Vulnerable** - Security issues only
- **Outdated** - Update available
- **Deprecated** - Officially deprecated
- **Unused** - Unused dependencies
**4. Add Deprecated Package Alternative:**
#### **Clustering (NEW!)**
Click to organize packages:
- **⚛️ Ecosystem** - Group by technology
- **🏥 Health** - Group by status
- **📊 Depth** - Group by level
#### **Other Controls**
- **Depth Slider** - Control dependency depth (1-∞)
- **Search** - Find packages instantly
- **Zoom** - In/Out/Reset/Fit/Center
- **Export** - PNG image or JSON data
- **Fullscreen** - Immersive view
---
## ✨ All Features
- 🔲 **Intelligent Clustering** (v3.1.6) - Ecosystem/Health/Depth grouping
- 🔑 **GitHub Token Config** (v3.1.5) - User tokens, no rate limits
- 📦 **502 Tracked Packages** (v3.1.5) - Comprehensive monitoring
- 🎨 **Unified Interactive Graph** (v3.1.4) - One file, all features
- 🎯 **Graph Layout Fixes** (v3.1.2) - Tree/Radial properly fixed
- 🔄 **Dynamic Issues** (v3.1.2) - Real-time npm audit
- 🛡️ **Production Safety** (v3.1.1) - Bug fixes and hardening
- 📊 **Multiple Layouts** (v3.1.0) - Tree/Force/Radial/Conflict
- 📦 **Batch Fix Modes** (v2.8.5) - Granular control
- 💾 **Backup & Rollback** (v2.8.4) - Safe management
- 📦 **Quality Auto-Fix** (v2.8.3) - Replace abandoned packages
- ⚖️ **License Auto-Fix** (v2.8.2) - GPL/AGPL replacement
- 🛡️ **Supply Chain Auto-Fix** (v2.8.1) - Remove malicious packages
- 🛡️ **Security Analysis** (v2.7) - Comprehensive security
- ⚡ **Parallel Processing** (v2.6) - 80% faster
- 🔮 **GitHub Tracking** (v2.4) - Real-time package health
- 🚀 **CI/CD Integration** (v2.2) - JSON output, exit codes
## 🚀 Installation
```bash
# Global (recommended)
npm install -g devcompass@3.1.6
# Local
npm install --save-dev devcompass@3.1.6
# One-time use
npx devcompass@3.1.6 analyze
# Upgrade
npm install -g devcompass@3.1.6
Edit `data/quality-alternatives.json`:
```json
{
"request": {
"replacement": "axios",
"reason": "request is deprecated"
},
"your-old-package": {
"replacement": "your-new-package",
"reason": "your-old-package is abandoned"
}
}
```
## 📖 Usage
### Project-Specific Configuration
### Basic Commands
Create `devcompass.config.json` in your project:
```bash
# Configure GitHub token
devcompass config --github-token <your-token>
devcompass config --show
# Analyze project
devcompass analyze
# Generate graph (with clustering!)
devcompass graph --open
# Auto-fix issues
devcompass fix
devcompass fix --batch
devcompass fix --dry-run
# Batch modes
devcompass fix --batch-mode critical
devcompass fix --batch-mode high
devcompass fix --batch-mode all
# Category-specific
devcompass fix --only quality
devcompass fix --skip updates
# Manage backups
devcompass backup list
devcompass backup restore --name <backup>
# CI/CD
devcompass analyze --json
devcompass analyze --ci
```json
{
"ignore": ["package-name"],
"ignoreSeverity": ["low"],
"minSeverity": "medium",
"minScore": 7,
"cache": true
}
```
### Graph Commands
**Options:**
- **ignore** - Packages to skip
- **ignoreSeverity** - Severities to ignore (low/medium/high/critical)
- **minSeverity** - Minimum to display
- **minScore** - Minimum health score for CI (default: 7)
- **cache** - Enable caching (default: true)
```bash
# Generate unified graph
devcompass graph
# Open in browser
devcompass graph --open
# Custom output
devcompass graph --output my-deps.html --open
# JSON export
devcompass graph --format json --output data.json
```
---

@@ -418,3 +579,3 @@

**Data Source:** `data/tracked-repos.json`
**Data Source:** `data/tracked-repos.json` (customizable!)

@@ -474,25 +635,4 @@ ---

---
**Category Configuration:** Edit `data/batch-categories.json` to customize!
## 🎯 Configuration
Create `devcompass.config.json`:
```json
{
"ignore": ["package-name"],
"ignoreSeverity": ["low"],
"minSeverity": "medium",
"minScore": 7,
"cache": true
}
```
**Options:**
- **ignore** - Packages to skip
- **ignoreSeverity** - Severities to ignore (low/medium/high/critical)
- **minSeverity** - Minimum to display
- **minScore** - Minimum health score for CI (default: 7)
- **cache** - Enable caching (default: true)
---

@@ -506,3 +646,3 @@

```bash
npm install -g devcompass@3.1.6
npm install -g devcompass@3.2.0
```

@@ -513,3 +653,3 @@

npm update -g devcompass
devcompass --version # Should show 3.1.6
devcompass --version # Should show 3.2.0
```

@@ -543,11 +683,31 @@

**Clustering not showing**
**Theme not switching**
```bash
# Ensure v3.1.6
# Clear browser cache
# Hard refresh (Ctrl+F5)
# Check console for errors (F12)
```
**Analytics tab empty**
```bash
# Ensure v3.2.0
devcompass --version
# Clear cache
# Hard refresh browser
# Regenerate graph
devcompass graph --output new.html --open
```
**Customize configuration not working**
```bash
# Check file exists
ls -la ~/devCompass/data/
# Validate JSON
cat ~/devCompass/data/licenses.json | jq .
# Restart after editing
devcompass analyze
```
---

@@ -559,8 +719,23 @@

1. Fork the repository
2. Create feature branch (`git checkout -b feature/amazing`)
3. Commit changes (`git commit -m 'Add feature'`)
4. Push branch (`git push origin feature/amazing`)
5. Open Pull Request
### Ways to Contribute:
1. **Add Package Alternatives**
- Edit `data/quality-alternatives.json`
- Submit PR with new deprecated package alternatives
2. **Expand License Database**
- Edit `data/license-risks.json`
- Add new license types and risk levels
3. **Improve Typosquatting Detection**
- Edit `data/popular-packages.json`
- Add more popular packages to track
4. **Code Contributions**
- Fork the repository
- Create feature branch (`git checkout -b feature/amazing`)
- Commit changes (`git commit -m 'Add feature'`)
- Push branch (`git push origin feature/amazing`)
- Open Pull Request
---

@@ -578,15 +753,19 @@

- [x] GitHub token configuration (v3.1.5)
- [x] 502 tracked packages (v3.1.5)
- [x] Unified graph system (v3.1.4)
- [x] Dynamic layout switching (v3.1.4)
- [x] Real-time filtering (v3.1.4)
- [x] Advanced zoom controls (v3.1.4)
- [x] **Intelligent clustering (v3.1.6)** ✅
- [x] **Ecosystem grouping (v3.1.6)** ✅
- [x] **Health clustering (v3.1.6)** ✅
- [x] **Depth analysis (v3.1.6)** ✅
- [x] Unified dashboard architecture (v3.2.0) ✅
- [x] Analytics layout (v3.2.0) ✅
- [x] Theme support (v3.2.0) ✅
- [x] Performance optimizations (v3.2.0) ✅
- [x] Dynamic data configuration (v3.1.7) ✅
- [x] GitHub token configuration (v3.1.5) ✅
- [x] 502 tracked packages (v3.1.5) ✅
- [x] Unified graph system (v3.1.4) ✅
- [x] Dynamic layout switching (v3.1.4) ✅
- [x] Real-time filtering (v3.1.4) ✅
- [x] Advanced zoom controls (v3.1.4) ✅
- [x] Intelligent clustering (v3.1.6) ✅
- [x] Ecosystem grouping (v3.1.6) ✅
- [x] Health clustering (v3.1.6) ✅
- [x] Depth analysis (v3.1.6) ✅
### Planned Features:
- [ ] **Web Dashboard** - Team health monitoring

@@ -602,6 +781,47 @@ - [ ] **Monorepo Support** - Multi-project analysis

## 📊 Version History
### v3.2.0 (2026-04-25) - Unified Dashboard Architecture
- 🎨 Unified modular dashboard (50% code reduction)
- 📊 NEW Analytics layout - Statistics dashboard
- 🌙 Dark/light theme support
- ⚡ 4-6× performance improvements
- 🗂️ 12 modular files (6 JS, 5 CSS, 1 HTML)
- ❌ Removed 5 duplicated files (3,600 lines → 1,800 lines)
- ✅ 100% backward compatible
### v3.1.7 (2026-04-22) - Dynamic Data Configuration
- 🔧 8 new JSON configuration files
- ✅ 7 source files refactored for dynamic loading
- ✅ Zero hardcoded data in code
- ✅ Scalable and customizable architecture
- 🐛 Fixed unused deps showing as `undefined`
- 🐛 Fixed supply chain analysis bugs
- 🐛 Fixed license risk analysis bugs
### v3.1.6 (2026-04-22) - Intelligent Clustering
- 🔲 Ecosystem clustering (12 categories)
- 🔲 Health clustering (Critical/Warning/Healthy)
- 🔲 Depth clustering (Direct → Level N)
- ✨ Click-to-highlight packages
- 📊 Per-cluster statistics
### v3.1.5 (2026-04-21) - GitHub Token Support
- 🔑 User-configurable GitHub tokens
- 📦 502 tracked packages database
- ⚡ 5,000 requests/hour (vs 60)
- 🔒 Secure local storage
### v3.1.4 (2026-04-20) - Unified Graph
- 🎨 All layouts in one file
- 🔄 Dynamic layout switching
- 🎯 Real-time filtering
- 🔍 Advanced zoom controls
---
**Made with ❤️ by [Ajay Thorat](https://github.com/AjayBThorat-20)**
*DevCompass - Keep your dependencies healthy!* 🧭
*DevCompass v3.2.0 - Unified, Modular, Beautiful!* 🧭
**Like Lighthouse for your dependencies** ⚡

@@ -43,11 +43,3 @@ // src/commands/analyze.js

/**
* v3.1.3 - analyzeProject() for graph enrichment
* Returns structured analysis data without console output
* Used by graph command to enrich nodes with vulnerability/outdated/unused flags
*
* @param {string} projectPath - Path to project directory
* @param {object} options - Options (silent mode enabled by default)
* @returns {object|null} Analysis results or null on failure
*/
async function analyzeProject(projectPath, options = {}) {

@@ -54,0 +46,0 @@ const config = loadConfig(projectPath);

// src/graph/exporter.js
// v3.1.6 - Unified graph exporter with clustering support

@@ -7,35 +6,4 @@ const fs = require('fs');

// Import layout generators
let generateTreeLayoutHTML, generateRadialLayoutHTML, generateForceLayoutHTML, generateConflictLayoutHTML;
try {
const tree = require('./layouts/tree');
generateTreeLayoutHTML = tree.generateTreeLayoutHTML || tree.createTreeLayout || tree;
} catch (e) {
generateTreeLayoutHTML = null;
}
try {
const radial = require('./layouts/radial');
generateRadialLayoutHTML = radial.generateRadialLayoutHTML || radial.generateRadialLayout || radial;
} catch (e) {
generateRadialLayoutHTML = null;
}
try {
const force = require('./layouts/force');
generateForceLayoutHTML = force.generateForceLayoutHTML || force.generateForceLayout || force;
} catch (e) {
generateForceLayoutHTML = null;
}
try {
const conflict = require('./layouts/conflict');
generateConflictLayoutHTML = conflict.generateConflictLayoutHTML || conflict;
} catch (e) {
generateConflictLayoutHTML = null;
}
/**
* GraphExporter - Exports graph data to various formats
* GraphExporter - Exports graph data using unified dashboard
*/

@@ -52,3 +20,3 @@ class GraphExporter {

filter: options.filter || 'all',
unified: options.unified !== false, // Enable unified mode by default
unified: options.unified !== false,
...options

@@ -74,86 +42,36 @@ };

/**
* Apply filter to graph data
* Generate unified dashboard HTML
*/
applyFilter() {
const filter = this.options.filter;
if (!filter || filter === 'all') {
return this.graphData;
}
const nodes = this.graphData.nodes;
const links = this.graphData.links;
let filteredNodes = [];
switch (filter) {
case 'vulnerable':
filteredNodes = nodes.filter(n =>
n.type === 'root' ||
n.isVulnerable === true ||
(Array.isArray(n.issues) && n.issues.some(i =>
i.type === 'security' || i.type === 'vulnerability'
))
);
break;
case 'outdated':
filteredNodes = nodes.filter(n =>
n.type === 'root' ||
n.isOutdated === true ||
(Array.isArray(n.issues) && n.issues.some(i => i.type === 'outdated'))
);
break;
case 'unused':
filteredNodes = nodes.filter(n =>
n.type === 'root' ||
n.isUnused === true ||
(Array.isArray(n.issues) && n.issues.some(i => i.type === 'unused'))
);
break;
case 'deprecated':
filteredNodes = nodes.filter(n =>
n.type === 'root' ||
n.isDeprecated === true ||
(Array.isArray(n.issues) && n.issues.some(i => i.type === 'deprecated'))
);
break;
case 'conflict':
filteredNodes = nodes.filter(n =>
n.type === 'root' ||
(Array.isArray(n.issues) && n.issues.length > 0) ||
(n.healthScore !== undefined && n.healthScore < 7)
);
break;
default:
filteredNodes = nodes;
}
const nodeIds = new Set(filteredNodes.map(n => n.id));
const filteredLinks = links.filter(l => {
const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
const targetId = typeof l.target === 'object' ? l.target.id : l.target;
return nodeIds.has(sourceId) && nodeIds.has(targetId);
});
return {
nodes: filteredNodes,
links: filteredLinks,
metadata: this.graphData.metadata
};
}
/**
* Generate unified HTML with dynamic controls and clustering (v3.1.6)
*/
generateUnifiedHTML() {
const templatePath = path.join(__dirname, 'template.html');
generateHTML() {
const dashboardIndexPath = path.join(__dirname, '../dashboard/index.html');
const clusteringPath = path.join(__dirname, 'clustering.js');
try {
let template = fs.readFileSync(templatePath, 'utf8');
// Check if dashboard exists
if (!fs.existsSync(dashboardIndexPath)) {
console.warn('Dashboard not found at:', dashboardIndexPath);
console.warn('Falling back to minimal graph...');
return this.generateFallbackHTML();
}
// Read dashboard template
let html = fs.readFileSync(dashboardIndexPath, 'utf8');
// Read clustering code (v3.1.6)
// Add metadata to graph data
const enrichedData = {
...this.graphData,
metadata: {
...this.graphData.metadata,
projectName: this.options.projectName,
projectVersion: this.options.projectVersion,
defaultLayout: this.options.layout,
defaultFilter: this.options.filter,
generatedAt: new Date().toISOString()
}
};
// Inject graph data
html = html.replace('{{GRAPH_DATA}}', JSON.stringify(enrichedData, null, 2));
// Read and inject clustering code
let clusteringCode = '';

@@ -163,3 +81,3 @@ if (fs.existsSync(clusteringPath)) {

// Remove Node.js exports from clustering code for browser
// Remove Node.js exports for browser
clusteringCode = clusteringCode.replace(

@@ -171,15 +89,12 @@ /if \(typeof module !== 'undefined' && module\.exports\) \{[\s\S]*?\}/g,

// Inject graph data
template = template.replace('{{GRAPH_DATA}}', JSON.stringify(this.graphData, null, 2));
html = html.replace('{{CLUSTERING_CODE}}', clusteringCode);
// Inject clustering code (v3.1.6)
template = template.replace('{{CLUSTERING_CODE}}', clusteringCode);
// Inline all assets (CSS and JS)
html = this.inlineAllAssets(html);
return template;
return html;
} catch (error) {
console.error('Failed to load unified template:', error.message);
console.error('Template path:', templatePath);
console.error('Clustering path:', clusteringPath);
// Fallback to traditional layout
return this.generateTraditionalHTML();
console.error('Failed to generate dashboard HTML:', error.message);
console.error('Falling back to minimal graph...');
return this.generateFallbackHTML();
}

@@ -189,62 +104,52 @@ }

/**
* Generate traditional HTML (single layout)
* Inline all CSS and JS assets into HTML
* @param {string} html - HTML content
* @returns {string} HTML with inlined assets
*/
generateTraditionalHTML() {
const filteredData = this.applyFilter();
const layout = (this.options.layout || 'tree').toLowerCase();
try {
switch (layout) {
case 'force':
if (typeof generateForceLayoutHTML === 'function') {
return generateForceLayoutHTML(filteredData, this.options);
}
break;
case 'radial':
if (typeof generateRadialLayoutHTML === 'function') {
return generateRadialLayoutHTML(filteredData, this.options);
}
break;
case 'conflict':
if (typeof generateConflictLayoutHTML === 'function') {
return generateConflictLayoutHTML(filteredData, this.options);
}
break;
case 'tree':
default:
if (typeof generateTreeLayoutHTML === 'function') {
return generateTreeLayoutHTML(filteredData, this.options);
}
break;
inlineAllAssets(html) {
const dashboardDir = path.join(__dirname, '../dashboard');
// Inline CSS files
const cssRegex = /<link rel="stylesheet" href="styles\/(.*?\.css)">/g;
html = html.replace(cssRegex, (match, filename) => {
const cssPath = path.join(dashboardDir, 'styles', filename);
try {
if (fs.existsSync(cssPath)) {
const cssContent = fs.readFileSync(cssPath, 'utf8');
return `<style>\n/* ${filename} */\n${cssContent}\n</style>`;
}
} catch (error) {
console.warn(`Could not inline CSS: ${filename}`);
}
} catch (error) {
console.error(`Layout error (${layout}):`, error.message);
}
// Final fallback
return this.generateFallbackHTML(filteredData, layout);
}
/**
* Generate HTML content
*/
generateHTML() {
// Use unified template by default
if (this.options.unified) {
return this.generateUnifiedHTML();
}
return match; // Keep original if file not found
});
// Fallback to traditional single-layout mode
return this.generateTraditionalHTML();
// Inline JavaScript files (except D3.js CDN)
const jsRegex = /<script src="scripts\/(.*?\.js)"><\/script>/g;
html = html.replace(jsRegex, (match, filename) => {
const jsPath = path.join(dashboardDir, 'scripts', filename);
try {
if (fs.existsSync(jsPath)) {
const jsContent = fs.readFileSync(jsPath, 'utf8');
return `<script>\n/* ${filename} */\n${jsContent}\n</script>`;
}
} catch (error) {
console.warn(`Could not inline JS: ${filename}`);
}
return match; // Keep original if file not found
});
return html;
}
/**
* Fallback HTML generator
* Fallback HTML generator (minimal working graph)
*/
generateFallbackHTML(graphData, layoutType) {
const nodes = Array.isArray(graphData.nodes) ? graphData.nodes : [];
const links = Array.isArray(graphData.links) ? graphData.links : [];
generateFallbackHTML() {
const nodes = Array.isArray(this.graphData.nodes) ? this.graphData.nodes : [];
const links = Array.isArray(this.graphData.links) ? this.graphData.links : [];

@@ -256,31 +161,273 @@ return `<!DOCTYPE html>

<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevCompass - Dependency Graph</title>
<title>DevCompass v3.2.0 - Dependency Graph</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f1f5f9; margin: 0; }
svg { width: 100vw; height: 100vh; }
.node circle { fill: #3b82f6; stroke: #fff; stroke-width: 2px; }
.node text { fill: #94a3b8; font-size: 10px; }
.link { stroke: #475569; stroke-width: 1.5px; stroke-opacity: 0.6; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
color: #f1f5f9;
margin: 0;
overflow: hidden;
}
.header {
background: rgba(30, 41, 59, 0.95);
padding: 1rem 1.5rem;
border-bottom: 1px solid #475569;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 1.25rem;
font-weight: 600;
margin: 0;
}
.header .meta {
font-size: 0.875rem;
color: #94a3b8;
}
svg {
width: 100vw;
height: calc(100vh - 80px);
cursor: grab;
}
svg:active { cursor: grabbing; }
.node circle {
fill: #3b82f6;
stroke: #0f172a;
stroke-width: 2px;
cursor: pointer;
transition: all 0.2s;
}
.node circle:hover {
fill: #60a5fa;
stroke: #fff;
stroke-width: 3px;
}
.node text {
fill: #94a3b8;
font-size: 11px;
pointer-events: none;
user-select: none;
}
.link {
stroke: #475569;
stroke-width: 1.5px;
stroke-opacity: 0.6;
}
.controls {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: 1000;
}
.btn {
width: 48px;
height: 48px;
background: rgba(30, 41, 59, 0.95);
border: 1px solid #475569;
border-radius: 12px;
color: #f1f5f9;
font-size: 1.25rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.btn:hover {
background: #3b82f6;
border-color: #3b82f6;
transform: scale(1.1);
}
.zoom-level {
width: 48px;
padding: 0.5rem;
background: rgba(30, 41, 59, 0.95);
border: 1px solid #475569;
border-radius: 12px;
text-align: center;
color: #60a5fa;
font-size: 0.75rem;
font-weight: 700;
}
.tooltip {
position: absolute;
background: rgba(15, 23, 42, 0.98);
border: 1px solid #3b82f6;
border-radius: 8px;
padding: 0.75rem 1rem;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
z-index: 10000;
font-size: 0.875rem;
max-width: 250px;
}
.tooltip.visible { opacity: 1; }
.tooltip-title {
font-weight: 600;
color: #60a5fa;
margin-bottom: 0.5rem;
}
.tooltip-row {
display: flex;
justify-content: space-between;
margin: 0.25rem 0;
font-size: 0.8rem;
}
.tooltip-label { color: #94a3b8; }
.tooltip-value { color: #f1f5f9; font-weight: 600; }
</style>
</head>
<body>
<div class="header">
<h1>📊 DevCompass v3.2.0 - Minimal Graph</h1>
<div class="meta">
Project: ${this.options.projectName} v${this.options.projectVersion} |
${nodes.length} packages
</div>
</div>
<svg id="graph"></svg>
<div class="controls">
<button class="btn" onclick="zoomIn()" title="Zoom In">+</button>
<div class="zoom-level" id="zoomLevel">100%</div>
<button class="btn" onclick="zoomOut()" title="Zoom Out">−</button>
<button class="btn" onclick="resetZoom()" title="Reset">⟲</button>
</div>
<div class="tooltip" id="tooltip"></div>
<script>
const data = ${JSON.stringify({ nodes, links })};
const graphData = ${JSON.stringify({ nodes, links })};
const svg = d3.select("#graph");
const width = window.innerWidth;
const height = window.innerHeight;
const simulation = d3.forceSimulation(data.nodes)
.force("link", d3.forceLink(data.links).id(d => d.id))
.force("charge", d3.forceManyBody().strength(-200))
.force("center", d3.forceCenter(width/2, height/2));
const link = svg.append("g").selectAll("line").data(data.links).join("line").attr("class", "link");
const node = svg.append("g").selectAll("g").data(data.nodes).join("g").attr("class", "node");
node.append("circle").attr("r", 8);
node.append("text").attr("dy", -12).text(d => d.name);
const height = window.innerHeight - 80;
const g = svg.append("g");
// Zoom behavior
const zoom = d3.zoom()
.scaleExtent([0.1, 4])
.on("zoom", (event) => {
g.attr("transform", event.transform);
updateZoomLevel(event.transform.k);
});
svg.call(zoom);
// Force simulation
const simulation = d3.forceSimulation(graphData.nodes)
.force("link", d3.forceLink(graphData.links).id(d => d.id).distance(100))
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(width/2, height/2))
.force("collision", d3.forceCollide().radius(20));
// Links
const link = g.append("g").selectAll("line")
.data(graphData.links)
.join("line")
.attr("class", "link");
// Nodes
const node = g.append("g").selectAll("g")
.data(graphData.nodes)
.join("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragStart)
.on("drag", dragging)
.on("end", dragEnd))
.on("mouseover", showTooltip)
.on("mouseout", hideTooltip);
node.append("circle")
.attr("r", d => d.type === 'root' ? 12 : 8);
node.append("text")
.attr("dy", -12)
.attr("text-anchor", "middle")
.text(d => {
const name = d.name || d.id;
return name.length > 15 ? name.substring(0, 12) + '...' : name;
});
// Simulation tick
simulation.on("tick", () => {
link.attr("x1", d => d.source.x).attr("y1", d => d.source.y).attr("x2", d => d.target.x).attr("y2", d => d.target.y);
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node.attr("transform", d => \`translate(\${d.x},\${d.y})\`);
});
// Drag functions
function dragStart(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragging(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragEnd(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
// Tooltip
function showTooltip(event, d) {
const tooltip = document.getElementById('tooltip');
tooltip.innerHTML = \`
<div class="tooltip-title">\${d.name || d.id}</div>
<div class="tooltip-row">
<span class="tooltip-label">Version</span>
<span class="tooltip-value">\${d.version || 'N/A'}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Depth</span>
<span class="tooltip-value">\${d.depth || 0}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Health</span>
<span class="tooltip-value">\${d.healthScore || 8}/10</span>
</div>
\`;
tooltip.style.left = (event.pageX + 10) + 'px';
tooltip.style.top = (event.pageY + 10) + 'px';
tooltip.classList.add('visible');
}
function hideTooltip() {
document.getElementById('tooltip').classList.remove('visible');
}
// Zoom controls
function zoomIn() {
svg.transition().duration(300).call(zoom.scaleBy, 1.3);
}
function zoomOut() {
svg.transition().duration(300).call(zoom.scaleBy, 0.7);
}
function resetZoom() {
svg.transition().duration(500).call(zoom.transform, d3.zoomIdentity);
}
function updateZoomLevel(scale) {
document.getElementById('zoomLevel').textContent = Math.round(scale * 100) + '%';
}
</script>

@@ -295,31 +442,8 @@ </body>

generateJSON() {
const filteredData = this.applyFilter();
return JSON.stringify(filteredData, null, 2);
return JSON.stringify(this.graphData, null, 2);
}
/**
* Get file size helper
* Export to file
*/
getFileSize(filePath) {
try {
const stats = fs.statSync(filePath);
const bytes = stats.size;
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
} catch {
return 'Unknown';
}
}
/**
* Export to file (main method)
*/
export(outputPath) {
return this.exportToFile(outputPath);
}
/**
* Export to file implementation
*/
exportToFile(outputPath) {

@@ -364,4 +488,26 @@ try {

/**
* Export HTML (legacy method)
* Get file size helper
*/
getFileSize(filePath) {
try {
const stats = fs.statSync(filePath);
const bytes = stats.size;
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
} catch {
return 'Unknown';
}
}
/**
* Main export method
*/
export(outputPath) {
return this.exportToFile(outputPath);
}
/**
* Legacy methods for backward compatibility
*/
exportHTML(outputPath) {

@@ -373,5 +519,2 @@ const content = this.generateHTML();

/**
* Export JSON (legacy method)
*/
exportJSON(outputPath) {

@@ -378,0 +521,0 @@ const content = this.generateJSON();

// src/graph/visualizer.js
// Unified graph visualizer - routes to appropriate layout generator
const { generateTreeLayoutHTML } = require('./layouts/tree');
const { generateRadialLayoutHTML } = require('./layouts/radial');
const { generateForceLayoutHTML } = require('./layouts/force');
const { generateConflictLayoutHTML } = require('./layouts/conflict');
const GraphExporter = require('./exporter');
/**
* GraphVisualizer - Main entry point for graph visualization
* Generates HTML output for various layout types
* GraphVisualizer - Wrapper around GraphExporter for backward compatibility
*/
class GraphVisualizer {
constructor(graphData, options = {}) {
this.graphData = this.validateGraphData(graphData);
this.options = {
width: options.width || 1400,
height: options.height || 900,
layout: options.layout || 'tree',
projectName: options.projectName || 'Project',
projectVersion: options.projectVersion || '1.0.0',
filter: options.filter || 'all',
...options
};
this.exporter = new GraphExporter(graphData, options);
}
/**
* Validate and normalize graph data
* Generate HTML output
*/
validateGraphData(graphData) {
if (!graphData) {
return { nodes: [], links: [] };
}
return {
nodes: Array.isArray(graphData.nodes) ? graphData.nodes : [],
links: Array.isArray(graphData.links) ? graphData.links : [],
metadata: graphData.metadata || {}
};
}
/**
* Apply filter to graph data
*/
applyFilter(filter) {
if (!filter || filter === 'all') {
return this.graphData;
}
const nodes = this.graphData.nodes;
const links = this.graphData.links;
let filteredNodes = [];
switch (filter) {
case 'vulnerable':
filteredNodes = nodes.filter(n =>
n.type === 'root' ||
(Array.isArray(n.issues) && n.issues.some(i => i.type === 'security' || i.type === 'vulnerability'))
);
break;
case 'outdated':
filteredNodes = nodes.filter(n =>
n.type === 'root' ||
n.isOutdated === true ||
(Array.isArray(n.issues) && n.issues.some(i => i.type === 'outdated'))
);
break;
case 'unused':
filteredNodes = nodes.filter(n =>
n.type === 'root' ||
n.isUnused === true ||
(Array.isArray(n.issues) && n.issues.some(i => i.type === 'unused'))
);
break;
case 'conflict':
filteredNodes = nodes.filter(n =>
n.type === 'root' ||
(Array.isArray(n.issues) && n.issues.length > 0) ||
n.healthScore < 7
);
break;
default:
filteredNodes = nodes;
}
// Get IDs of filtered nodes
const nodeIds = new Set(filteredNodes.map(n => n.id));
// Filter links to only include those between filtered nodes
const filteredLinks = links.filter(l => {
const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
const targetId = typeof l.target === 'object' ? l.target.id : l.target;
return nodeIds.has(sourceId) && nodeIds.has(targetId);
});
return {
nodes: filteredNodes,
links: filteredLinks,
metadata: this.graphData.metadata
};
}
/**
* Generate HTML output for the configured layout
*/
generateHTML() {
const filteredData = this.applyFilter(this.options.filter);
const layout = this.options.layout.toLowerCase();
switch (layout) {
case 'force':
return generateForceLayoutHTML(filteredData, this.options);
case 'radial':
return generateRadialLayoutHTML(filteredData, this.options);
case 'conflict':
return generateConflictLayoutHTML(filteredData, this.options);
case 'tree':
default:
return generateTreeLayoutHTML(filteredData, this.options);
}
return this.exporter.generateHTML();
}
/**
* Get graph script for embedding (backward compatibility)
* Generate graph script (legacy)
*/
generateGraphScript() {
// For backward compatibility, return a script that can be embedded
return this.generateHTML();

@@ -148,3 +36,4 @@ }

{ id: 'radial', name: 'Radial Layout', description: 'Concentric circles by depth' },
{ id: 'conflict', name: 'Conflict View', description: 'Shows only problematic packages' }
{ id: 'conflict', name: 'Conflict View', description: 'Shows only problematic packages' },
{ id: 'analytics', name: 'Analytics Dashboard', description: 'Statistical overview' }
];

@@ -154,548 +43,4 @@ }

/**
* Get available filters// src/graph/layouts/conflict.js
// Conflict-only view - shows packages with issues organized by severity
/**
* Generate conflict layout HTML
* Shows only packages with issues, organized by severity
* Get available filters
*/
function generateConflictLayoutHTML(graphData, options = {}) {
const projectName = options.projectName || 'Project';
const projectVersion = options.projectVersion || '1.0.0';
// Validate input
const nodes = Array.isArray(graphData.nodes) ? graphData.nodes : [];
const links = Array.isArray(graphData.links) ? graphData.links : [];
// Filter to only nodes with issues or low health scores
const conflictNodes = nodes.filter(n => {
if (n.type === 'root') return true; // Always show root
const hasIssues = Array.isArray(n.issues) && n.issues.length > 0;
const lowHealth = (n.healthScore || 10) < 7;
return hasIssues || lowHealth;
});
// If no conflicts, show success message
if (conflictNodes.length <= 1) {
return generateNoConflictsHTML(projectName, projectVersion, nodes.length);
}
// Categorize by severity
const categorized = {
critical: [],
high: [],
medium: [],
low: [],
root: []
};
conflictNodes.forEach(node => {
if (node.type === 'root') {
categorized.root.push(node);
} else {
const score = node.healthScore || 8;
if (score < 3) categorized.critical.push(node);
else if (score < 5) categorized.high.push(node);
else if (score < 7) categorized.medium.push(node);
else categorized.low.push(node);
}
});
const graphDataJSON = JSON.stringify({
nodes: conflictNodes,
links,
categorized
});
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevCompass - Conflict View</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
:root {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--accent-blue: #3b82f6;
--accent-cyan: #06b6d4;
--border-color: #475569;
--critical: #ef4444;
--high: #f97316;
--medium: #eab308;
--low: #84cc16;
--root-color: #60a5fa;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
color: var(--text-primary);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
/* Header */
.header {
text-align: center;
padding: 30px;
background: rgba(30, 41, 59, 0.8);
border-radius: 20px;
border: 1px solid var(--border-color);
margin-bottom: 30px;
backdrop-filter: blur(12px);
}
.header-title {
font-size: 28px;
font-weight: 700;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
.header-subtitle {
color: var(--text-secondary);
font-size: 14px;
}
.header-meta {
display: flex;
justify-content: center;
gap: 30px;
margin-top: 16px;
font-size: 13px;
color: var(--text-secondary);
}
.header-meta span {
display: flex;
align-items: center;
gap: 6px;
}
/* Summary Cards */
.summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
margin-bottom: 30px;
}
.summary-card {
background: rgba(30, 41, 59, 0.8);
border-radius: 16px;
border: 1px solid var(--border-color);
padding: 20px;
text-align: center;
backdrop-filter: blur(12px);
transition: transform 0.2s, box-shadow 0.2s;
}
.summary-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
}
.summary-count {
font-size: 36px;
font-weight: 700;
margin-bottom: 6px;
}
.summary-label {
font-size: 12px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.summary-card.critical .summary-count { color: var(--critical); }
.summary-card.high .summary-count { color: var(--high); }
.summary-card.medium .summary-count { color: var(--medium); }
.summary-card.low .summary-count { color: var(--low); }
/* Severity Sections */
.severity-section {
background: rgba(30, 41, 59, 0.8);
border-radius: 16px;
border: 1px solid var(--border-color);
margin-bottom: 20px;
overflow: hidden;
backdrop-filter: blur(12px);
}
.severity-header {
padding: 16px 24px;
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
transition: background 0.2s;
}
.severity-header:hover {
background: rgba(255, 255, 255, 0.03);
}
.severity-indicator {
width: 16px;
height: 16px;
border-radius: 50%;
}
.severity-title {
font-weight: 600;
font-size: 15px;
flex: 1;
}
.severity-count {
background: var(--bg-tertiary);
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.severity-toggle {
color: var(--text-muted);
font-size: 18px;
transition: transform 0.2s;
}
.severity-section.collapsed .severity-toggle {
transform: rotate(-90deg);
}
.severity-section.collapsed .severity-content {
display: none;
}
.severity-content {
padding: 16px;
}
/* Package Cards */
.package-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
}
.package-card {
background: var(--bg-tertiary);
border-radius: 12px;
padding: 16px;
border: 1px solid var(--border-color);
transition: transform 0.2s, border-color 0.2s;
}
.package-card:hover {
transform: translateX(4px);
border-color: var(--accent-cyan);
}
.package-name {
font-weight: 600;
font-size: 14px;
color: var(--accent-cyan);
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.package-version {
font-size: 12px;
color: var(--text-muted);
margin-bottom: 10px;
}
.package-issues {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.issue-tag {
font-size: 10px;
padding: 3px 8px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.1);
color: var(--text-secondary);
}
.issue-tag.security { background: rgba(239, 68, 68, 0.2); color: var(--critical); }
.issue-tag.outdated { background: rgba(249, 115, 22, 0.2); color: var(--high); }
.issue-tag.deprecated { background: rgba(234, 179, 8, 0.2); color: var(--medium); }
.package-score {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
font-size: 12px;
}
.score-label { color: var(--text-secondary); }
.score-value { font-weight: 600; }
/* Empty state for sections */
.empty-section {
padding: 30px;
text-align: center;
color: var(--text-muted);
}
/* Footer */
.footer {
text-align: center;
padding: 20px;
color: var(--text-muted);
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="header-title">
⚠️ Dependency Conflict View
</div>
<div class="header-subtitle">
Packages requiring attention, organized by severity
</div>
<div class="header-meta">
<span><strong>Project:</strong> ${projectName}</span>
<span><strong>Version:</strong> ${projectVersion}</span>
<span><strong>Total Deps:</strong> ${nodes.length}</span>
<span><strong>With Issues:</strong> ${conflictNodes.length - 1}</span>
</div>
</div>
<div class="summary">
<div class="summary-card critical">
<div class="summary-count" id="critical-count">0</div>
<div class="summary-label">Critical</div>
</div>
<div class="summary-card high">
<div class="summary-count" id="high-count">0</div>
<div class="summary-label">High</div>
</div>
<div class="summary-card medium">
<div class="summary-count" id="medium-count">0</div>
<div class="summary-label">Medium</div>
</div>
<div class="summary-card low">
<div class="summary-count" id="low-count">0</div>
<div class="summary-label">Low</div>
</div>
</div>
<div id="sections"></div>
<div class="footer">
Generated by DevCompass • ${new Date().toLocaleString()}
</div>
</div>
<script>
const graphData = ${graphDataJSON};
const categorized = graphData.categorized;
// Update counts
document.getElementById('critical-count').textContent = categorized.critical.length;
document.getElementById('high-count').textContent = categorized.high.length;
document.getElementById('medium-count').textContent = categorized.medium.length;
document.getElementById('low-count').textContent = categorized.low.length;
// Generate sections
const sections = [
{ key: 'critical', title: 'Critical Issues', color: 'var(--critical)' },
{ key: 'high', title: 'High Priority', color: 'var(--high)' },
{ key: 'medium', title: 'Medium Priority', color: 'var(--medium)' },
{ key: 'low', title: 'Low Priority', color: 'var(--low)' }
];
const container = document.getElementById('sections');
sections.forEach(section => {
const packages = categorized[section.key];
if (packages.length === 0) return;
const sectionEl = document.createElement('div');
sectionEl.className = 'severity-section';
sectionEl.innerHTML = \`
<div class="severity-header" onclick="this.parentElement.classList.toggle('collapsed')">
<div class="severity-indicator" style="background: \${section.color};"></div>
<div class="severity-title">\${section.title}</div>
<div class="severity-count">\${packages.length} package\${packages.length !== 1 ? 's' : ''}</div>
<div class="severity-toggle">▼</div>
</div>
<div class="severity-content">
<div class="package-grid">
\${packages.map(pkg => renderPackageCard(pkg, section.color)).join('')}
</div>
</div>
\`;
container.appendChild(sectionEl);
});
function renderPackageCard(pkg, color) {
const issues = pkg.issues || [];
const score = pkg.healthScore || 5;
return \`
<div class="package-card">
<div class="package-name">
<span style="color: \${color};">●</span>
\${pkg.name || pkg.id}
</div>
<div class="package-version">v\${pkg.version || 'unknown'}</div>
<div class="package-issues">
\${issues.map(i => \`<span class="issue-tag \${i.type || ''}">\${i.title || i.type || 'Issue'}</span>\`).join('')}
\${issues.length === 0 ? '<span class="issue-tag">Low health score</span>' : ''}
</div>
<div class="package-score">
<span class="score-label">Health Score</span>
<span class="score-value" style="color: \${color};">\${score}/10</span>
</div>
</div>
\`;
}
</script>
</body>
</html>`;
}
/**
* Generate HTML for when no conflicts are found
*/
function generateNoConflictsHTML(projectName, projectVersion, totalDeps) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevCompass - No Conflicts</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
color: #f1f5f9;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.success-card {
text-align: center;
padding: 60px 80px;
background: rgba(30, 41, 59, 0.95);
border-radius: 24px;
border: 1px solid #475569;
backdrop-filter: blur(12px);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
}
.success-icon {
font-size: 80px;
margin-bottom: 24px;
animation: bounce 2s ease-in-out infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.success-title {
font-size: 28px;
font-weight: 700;
margin-bottom: 12px;
color: #10b981;
}
.success-subtitle {
font-size: 16px;
color: #94a3b8;
margin-bottom: 30px;
}
.stats {
display: flex;
gap: 40px;
justify-content: center;
padding-top: 24px;
border-top: 1px solid #475569;
}
.stat {
text-align: center;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #06b6d4;
}
.stat-label {
font-size: 12px;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.5px;
}
</style>
</head>
<body>
<div class="success-card">
<div class="success-icon">🎉</div>
<div class="success-title">No Conflicts Found!</div>
<div class="success-subtitle">
All dependencies in ${projectName} v${projectVersion} are healthy
</div>
<div class="stats">
<div class="stat">
<div class="stat-value">${totalDeps}</div>
<div class="stat-label">Dependencies</div>
</div>
<div class="stat">
<div class="stat-value">0</div>
<div class="stat-label">Issues</div>
</div>
<div class="stat">
<div class="stat-value">✓</div>
<div class="stat-label">All Healthy</div>
</div>
</div>
</div>
</body>
</html>`;
}
module.exports = {
generateConflictLayoutHTML
};
*/
function getAvailableFilters() {

@@ -702,0 +47,0 @@ return [

// src/graph/layouts/conflict.js
// Conflict-only view - shows packages with issues organized by severity
/**
* Generate conflict layout HTML
* Shows only packages with issues, organized by severity
*/
function generateConflictLayoutHTML(graphData, options = {}) {
const projectName = options.projectName || 'Project';
const projectVersion = options.projectVersion || '1.0.0';
// Validate input
const nodes = Array.isArray(graphData.nodes) ? graphData.nodes : [];
const links = Array.isArray(graphData.links) ? graphData.links : [];
// Filter to only nodes with issues or low health scores
const conflictNodes = nodes.filter(n => {
if (n.type === 'root') return true; // Always show root
const hasIssues = Array.isArray(n.issues) && n.issues.length > 0;
const lowHealth = (n.healthScore || 10) < 7;
return hasIssues || lowHealth;
});
// If no conflicts, show success message
if (conflictNodes.length <= 1) {
return generateNoConflictsHTML(projectName, projectVersion, nodes.length);
}
// Categorize by severity
const categorized = {
critical: [],
high: [],
medium: [],
low: [],
root: []
};
conflictNodes.forEach(node => {
if (node.type === 'root') {
categorized.root.push(node);
} else {
const score = node.healthScore || 8;
if (score < 3) categorized.critical.push(node);
else if (score < 5) categorized.high.push(node);
else if (score < 7) categorized.medium.push(node);
else categorized.low.push(node);
}
});
const graphDataJSON = JSON.stringify({
nodes: conflictNodes,
links,
categorized
});
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevCompass - Conflict View</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
:root {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--accent-blue: #3b82f6;
--accent-cyan: #06b6d4;
--border-color: #475569;
--critical: #ef4444;
--high: #f97316;
--medium: #eab308;
--low: #84cc16;
--root-color: #60a5fa;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
color: var(--text-primary);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
/* Header */
.header {
text-align: center;
padding: 30px;
background: rgba(30, 41, 59, 0.8);
border-radius: 20px;
border: 1px solid var(--border-color);
margin-bottom: 30px;
backdrop-filter: blur(12px);
}
.header-title {
font-size: 28px;
font-weight: 700;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
.header-subtitle {
color: var(--text-secondary);
font-size: 14px;
}
.header-meta {
display: flex;
justify-content: center;
gap: 30px;
margin-top: 16px;
font-size: 13px;
color: var(--text-secondary);
}
.header-meta span {
display: flex;
align-items: center;
gap: 6px;
}
/* Summary Cards */
.summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
margin-bottom: 30px;
}
.summary-card {
background: rgba(30, 41, 59, 0.8);
border-radius: 16px;
border: 1px solid var(--border-color);
padding: 20px;
text-align: center;
backdrop-filter: blur(12px);
transition: transform 0.2s, box-shadow 0.2s;
}
.summary-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
}
.summary-count {
font-size: 36px;
font-weight: 700;
margin-bottom: 6px;
}
.summary-label {
font-size: 12px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.summary-card.critical .summary-count { color: var(--critical); }
.summary-card.high .summary-count { color: var(--high); }
.summary-card.medium .summary-count { color: var(--medium); }
.summary-card.low .summary-count { color: var(--low); }
/* Severity Sections */
.severity-section {
background: rgba(30, 41, 59, 0.8);
border-radius: 16px;
border: 1px solid var(--border-color);
margin-bottom: 20px;
overflow: hidden;
backdrop-filter: blur(12px);
}
.severity-header {
padding: 16px 24px;
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
transition: background 0.2s;
}
.severity-header:hover {
background: rgba(255, 255, 255, 0.03);
}
.severity-indicator {
width: 16px;
height: 16px;
border-radius: 50%;
}
.severity-title {
font-weight: 600;
font-size: 15px;
flex: 1;
}
.severity-count {
background: var(--bg-tertiary);
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.severity-toggle {
color: var(--text-muted);
font-size: 18px;
transition: transform 0.2s;
}
.severity-section.collapsed .severity-toggle {
transform: rotate(-90deg);
}
.severity-section.collapsed .severity-content {
display: none;
}
.severity-content {
padding: 16px;
}
/* Package Cards */
.package-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
}
.package-card {
background: var(--bg-tertiary);
border-radius: 12px;
padding: 16px;
border: 1px solid var(--border-color);
transition: transform 0.2s, border-color 0.2s;
}
.package-card:hover {
transform: translateX(4px);
border-color: var(--accent-cyan);
}
.package-name {
font-weight: 600;
font-size: 14px;
color: var(--accent-cyan);
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.package-version {
font-size: 12px;
color: var(--text-muted);
margin-bottom: 10px;
}
.package-issues {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.issue-tag {
font-size: 10px;
padding: 3px 8px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.1);
color: var(--text-secondary);
}
.issue-tag.security { background: rgba(239, 68, 68, 0.2); color: var(--critical); }
.issue-tag.outdated { background: rgba(249, 115, 22, 0.2); color: var(--high); }
.issue-tag.deprecated { background: rgba(234, 179, 8, 0.2); color: var(--medium); }
.package-score {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
font-size: 12px;
}
.score-label { color: var(--text-secondary); }
.score-value { font-weight: 600; }
/* Empty state for sections */
.empty-section {
padding: 30px;
text-align: center;
color: var(--text-muted);
}
/* Footer */
.footer {
text-align: center;
padding: 20px;
color: var(--text-muted);
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="header-title">
⚠️ Dependency Conflict View
</div>
<div class="header-subtitle">
Packages requiring attention, organized by severity
</div>
<div class="header-meta">
<span><strong>Project:</strong> ${projectName}</span>
<span><strong>Version:</strong> ${projectVersion}</span>
<span><strong>Total Deps:</strong> ${nodes.length}</span>
<span><strong>With Issues:</strong> ${conflictNodes.length - 1}</span>
</div>
</div>
<div class="summary">
<div class="summary-card critical">
<div class="summary-count" id="critical-count">0</div>
<div class="summary-label">Critical</div>
</div>
<div class="summary-card high">
<div class="summary-count" id="high-count">0</div>
<div class="summary-label">High</div>
</div>
<div class="summary-card medium">
<div class="summary-count" id="medium-count">0</div>
<div class="summary-label">Medium</div>
</div>
<div class="summary-card low">
<div class="summary-count" id="low-count">0</div>
<div class="summary-label">Low</div>
</div>
</div>
<div id="sections"></div>
<div class="footer">
Generated by DevCompass • ${new Date().toLocaleString()}
</div>
</div>
<script>
const graphData = ${graphDataJSON};
const categorized = graphData.categorized;
// Update counts
document.getElementById('critical-count').textContent = categorized.critical.length;
document.getElementById('high-count').textContent = categorized.high.length;
document.getElementById('medium-count').textContent = categorized.medium.length;
document.getElementById('low-count').textContent = categorized.low.length;
// Generate sections
const sections = [
{ key: 'critical', title: 'Critical Issues', color: 'var(--critical)' },
{ key: 'high', title: 'High Priority', color: 'var(--high)' },
{ key: 'medium', title: 'Medium Priority', color: 'var(--medium)' },
{ key: 'low', title: 'Low Priority', color: 'var(--low)' }
];
const container = document.getElementById('sections');
sections.forEach(section => {
const packages = categorized[section.key];
if (packages.length === 0) return;
const sectionEl = document.createElement('div');
sectionEl.className = 'severity-section';
sectionEl.innerHTML = \`
<div class="severity-header" onclick="this.parentElement.classList.toggle('collapsed')">
<div class="severity-indicator" style="background: \${section.color};"></div>
<div class="severity-title">\${section.title}</div>
<div class="severity-count">\${packages.length} package\${packages.length !== 1 ? 's' : ''}</div>
<div class="severity-toggle">▼</div>
</div>
<div class="severity-content">
<div class="package-grid">
\${packages.map(pkg => renderPackageCard(pkg, section.color)).join('')}
</div>
</div>
\`;
container.appendChild(sectionEl);
});
function renderPackageCard(pkg, color) {
const issues = pkg.issues || [];
const score = pkg.healthScore || 5;
return \`
<div class="package-card">
<div class="package-name">
<span style="color: \${color};">●</span>
\${pkg.name || pkg.id}
</div>
<div class="package-version">v\${pkg.version || 'unknown'}</div>
<div class="package-issues">
\${issues.map(i => \`<span class="issue-tag \${i.type || ''}">\${i.title || i.type || 'Issue'}</span>\`).join('')}
\${issues.length === 0 ? '<span class="issue-tag">Low health score</span>' : ''}
</div>
<div class="package-score">
<span class="score-label">Health Score</span>
<span class="score-value" style="color: \${color};">\${score}/10</span>
</div>
</div>
\`;
}
</script>
</body>
</html>`;
}
/**
* Generate HTML for when no conflicts are found
*/
function generateNoConflictsHTML(projectName, projectVersion, totalDeps) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevCompass - No Conflicts</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
color: #f1f5f9;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.success-card {
text-align: center;
padding: 60px 80px;
background: rgba(30, 41, 59, 0.95);
border-radius: 24px;
border: 1px solid #475569;
backdrop-filter: blur(12px);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
}
.success-icon {
font-size: 80px;
margin-bottom: 24px;
animation: bounce 2s ease-in-out infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.success-title {
font-size: 28px;
font-weight: 700;
margin-bottom: 12px;
color: #10b981;
}
.success-subtitle {
font-size: 16px;
color: #94a3b8;
margin-bottom: 30px;
}
.stats {
display: flex;
gap: 40px;
justify-content: center;
padding-top: 24px;
border-top: 1px solid #475569;
}
.stat {
text-align: center;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #06b6d4;
}
.stat-label {
font-size: 12px;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.5px;
}
</style>
</head>
<body>
<div class="success-card">
<div class="success-icon">🎉</div>
<div class="success-title">No Conflicts Found!</div>
<div class="success-subtitle">
All dependencies in ${projectName} v${projectVersion} are healthy
</div>
<div class="stats">
<div class="stat">
<div class="stat-value">${totalDeps}</div>
<div class="stat-label">Dependencies</div>
</div>
<div class="stat">
<div class="stat-value">0</div>
<div class="stat-label">Issues</div>
</div>
<div class="stat">
<div class="stat-value">✓</div>
<div class="stat-label">All Healthy</div>
</div>
</div>
</div>
</body>
</html>`;
}
module.exports = {
generateConflictLayoutHTML
};
// src/graph/layouts/force.js
// Force-directed layout with D3.js physics simulation
function generateForceLayoutHTML(graphData, options = {}) {
const width = options.width || 1400;
const height = options.height || 900;
const projectName = options.projectName || 'Project';
const projectVersion = options.projectVersion || '1.0.0';
// Validate input
const nodes = Array.isArray(graphData.nodes) ? graphData.nodes : [];
const links = Array.isArray(graphData.links) ? graphData.links : [];
if (nodes.length === 0) {
return generateEmptyStateHTML(projectName, projectVersion);
}
// Calculate max depth
const maxDepth = Math.max(...nodes.map(n => n.depth || 0), 0);
const graphDataJSON = JSON.stringify({ nodes, links });
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevCompass - Force Graph</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
:root {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--accent-blue: #3b82f6;
--accent-cyan: #06b6d4;
--border-color: #475569;
--health-excellent: #10b981;
--health-good: #84cc16;
--health-caution: #eab308;
--health-warning: #f97316;
--health-critical: #ef4444;
--root-color: #60a5fa;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
color: var(--text-primary);
min-height: 100vh;
overflow: hidden;
}
#container {
width: 100vw;
height: 100vh;
position: relative;
}
svg {
width: 100%;
height: 100%;
cursor: grab;
}
svg:active { cursor: grabbing; }
/* Header */
.header {
position: fixed;
top: 20px;
left: 20px;
background: rgba(30, 41, 59, 0.95);
padding: 16px 24px;
border-radius: 16px;
border: 1px solid var(--border-color);
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
z-index: 100;
}
.header-title {
font-size: 18px;
font-weight: 700;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 10px;
}
.header-subtitle {
font-size: 11px;
color: var(--text-muted);
margin-top: 4px;
}
/* Search */
.search-container {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 100;
}
.search-input {
width: 320px;
padding: 12px 20px;
background: rgba(30, 41, 59, 0.95);
border: 1px solid var(--border-color);
border-radius: 25px;
color: var(--text-primary);
font-size: 14px;
outline: none;
backdrop-filter: blur(12px);
transition: border-color 0.2s, box-shadow 0.2s;
}
.search-input:focus {
border-color: var(--accent-cyan);
box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.2);
}
.search-input::placeholder {
color: var(--text-muted);
}
/* Controls Panel */
.controls {
position: fixed;
top: 20px;
right: 20px;
background: rgba(30, 41, 59, 0.95);
padding: 20px;
border-radius: 16px;
border: 1px solid var(--border-color);
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
z-index: 100;
min-width: 200px;
}
.controls-title {
font-size: 13px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.controls-section {
margin-bottom: 16px;
}
.controls-section-title {
font-size: 10px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.control-btn {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 10px 14px;
margin: 4px 0;
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 10px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: all 0.2s ease;
}
.control-btn:hover {
background: var(--accent-blue);
border-color: var(--accent-blue);
}
.control-btn.primary {
background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-cyan) 100%);
border-color: var(--accent-blue);
}
.control-btn.active {
background: var(--accent-cyan);
border-color: var(--accent-cyan);
}
/* Filter Dropdowns */
.filter-select {
width: 100%;
padding: 8px 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-primary);
font-size: 12px;
outline: none;
cursor: pointer;
margin: 4px 0;
}
.filter-select:focus {
border-color: var(--accent-cyan);
}
/* Statistics Panel */
.stats {
position: fixed;
bottom: 100px;
right: 20px;
background: rgba(30, 41, 59, 0.95);
padding: 16px 20px;
border-radius: 16px;
border: 1px solid var(--border-color);
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
z-index: 100;
min-width: 160px;
}
.stats-title {
font-size: 13px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 12px;
}
.stat-row {
display: flex;
justify-content: space-between;
margin: 6px 0;
font-size: 12px;
}
.stat-label { color: var(--text-secondary); }
.stat-value { color: var(--accent-cyan); font-weight: 700; }
/* Legend */
.legend {
position: fixed;
bottom: 20px;
left: 20px;
background: rgba(30, 41, 59, 0.95);
padding: 16px 20px;
border-radius: 16px;
border: 1px solid var(--border-color);
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
z-index: 100;
}
.legend-title {
font-size: 13px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 6px;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
margin: 6px 0;
font-size: 11px;
color: var(--text-secondary);
}
.legend-dot {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
}
/* Zoom Controls */
.zoom-controls {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 100;
}
.zoom-btn {
width: 44px;
height: 44px;
background: rgba(30, 41, 59, 0.95);
border: 1px solid var(--border-color);
border-radius: 12px;
color: var(--text-primary);
font-size: 20px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
backdrop-filter: blur(12px);
}
.zoom-btn:hover {
background: var(--accent-blue);
border-color: var(--accent-blue);
transform: scale(1.1);
}
.zoom-level {
width: 44px;
padding: 8px;
background: rgba(30, 41, 59, 0.95);
border: 1px solid var(--border-color);
border-radius: 12px;
text-align: center;
color: var(--accent-cyan);
font-size: 10px;
font-weight: 700;
backdrop-filter: blur(12px);
}
/* Tooltip */
.tooltip {
position: absolute;
padding: 14px 18px;
background: rgba(15, 23, 42, 0.98);
border: 1px solid var(--border-color);
border-radius: 12px;
font-size: 12px;
max-width: 280px;
pointer-events: none;
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.2s, transform 0.2s;
z-index: 1000;
backdrop-filter: blur(12px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}
.tooltip.visible {
opacity: 1;
transform: translateY(0);
}
.tooltip-title {
font-weight: 700;
color: var(--accent-cyan);
margin-bottom: 8px;
font-size: 13px;
}
.tooltip-row {
display: flex;
justify-content: space-between;
margin: 4px 0;
}
.tooltip-label { color: var(--text-secondary); }
.tooltip-value { color: var(--text-primary); font-weight: 600; }
/* Node styles */
.node circle {
stroke: var(--bg-primary);
stroke-width: 2px;
cursor: pointer;
transition: all 0.2s ease;
}
.node circle:hover {
stroke: var(--text-primary);
stroke-width: 3px;
filter: drop-shadow(0 0 10px currentColor);
}
.node.selected circle {
stroke: var(--accent-cyan);
stroke-width: 4px;
}
.node-label {
font-size: 10px;
fill: var(--text-secondary);
pointer-events: none;
font-weight: 500;
}
.node-label.hidden { display: none; }
/* Link styles */
.link {
stroke: var(--border-color);
stroke-width: 1.5px;
stroke-opacity: 0.4;
}
.link.highlighted {
stroke: var(--accent-cyan);
stroke-opacity: 0.8;
stroke-width: 2.5px;
}
.link.hidden { display: none; }
</style>
</head>
<body>
<div id="container"></div>
<div class="header">
<div class="header-title">
⚡ Force Graph
</div>
<div class="header-subtitle">Interactive Physics Simulation</div>
</div>
<div class="search-container">
<input type="text" class="search-input" id="search" placeholder="🔍 Search packages..." />
</div>
<div class="controls">
<div class="controls-title">⚙️ Controls</div>
<div class="controls-section">
<div class="controls-section-title">VIEW</div>
<button class="control-btn primary" onclick="resetLayout()">↻ Reset Layout</button>
<button class="control-btn" onclick="centerView()">◎ Center View</button>
<button class="control-btn" onclick="fitToScreen()">⛶ Fit to Screen</button>
<button class="control-btn" onclick="toggleFullscreen()">⛶ Fullscreen</button>
</div>
<div class="controls-section">
<div class="controls-section-title">DISPLAY</div>
<button class="control-btn active" id="btn-labels" onclick="toggleLabels()">🏷️ Hide Labels</button>
<button class="control-btn active" id="btn-links" onclick="toggleLinks()">🔗 Hide Links</button>
</div>
<div class="controls-section">
<div class="controls-section-title">FILTER</div>
<div class="controls-section-title">HEALTH SCORE</div>
<select class="filter-select" id="health-filter" onchange="applyFilters()">
<option value="all">All Packages</option>
<option value="excellent">Excellent (9-10)</option>
<option value="good">Good (7-8)</option>
<option value="caution">Caution (5-7)</option>
<option value="warning">Warning (3-5)</option>
<option value="critical">Critical (&lt;3)</option>
</select>
</div>
</div>
<div class="stats">
<div class="stats-title">📊 Statistics</div>
<div class="stat-row">
<span class="stat-label">Total Nodes</span>
<span class="stat-value" id="total-nodes">${nodes.length}</span>
</div>
<div class="stat-row">
<span class="stat-label">Visible Nodes</span>
<span class="stat-value" id="visible-nodes">${nodes.length}</span>
</div>
<div class="stat-row">
<span class="stat-label">Links</span>
<span class="stat-value" id="total-links">${links.length}</span>
</div>
<div class="stat-row">
<span class="stat-label">Selected</span>
<span class="stat-value" id="selected-count">0</span>
</div>
<div class="stat-row">
<span class="stat-label">Zoom</span>
<span class="stat-value" id="zoom-stat">100%</span>
</div>
</div>
<div class="legend">
<div class="legend-title">🎨 Health Status ▾</div>
<div class="legend-item">
<div class="legend-dot" style="background: var(--health-excellent);"></div>
<span>Excellent (9-10)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: var(--health-good);"></div>
<span>Good (7-8)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: var(--health-caution);"></div>
<span>Caution (5-7)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: var(--health-warning);"></div>
<span>Warning (3-5)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: var(--health-critical);"></div>
<span>Critical (&lt;3)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: var(--root-color);"></div>
<span>Root Package</span>
</div>
</div>
<div class="zoom-controls">
<button class="zoom-btn" onclick="zoomIn()">+</button>
<div class="zoom-level" id="zoom-level">100%</div>
<button class="zoom-btn" onclick="zoomOut()">−</button>
<button class="zoom-btn" onclick="resetLayout()">⟲</button>
</div>
<div class="tooltip" id="tooltip"></div>
<script>
const graphData = ${graphDataJSON};
const width = window.innerWidth;
const height = window.innerHeight;
let showLabels = true;
let showLinks = true;
let currentZoom = 1;
let selectedNode = null;
// Get color based on health score
function getHealthColor(node) {
if (node.type === 'root') return 'var(--root-color)';
const score = node.healthScore || 8;
if (score >= 9) return 'var(--health-excellent)';
if (score >= 7) return 'var(--health-good)';
if (score >= 5) return 'var(--health-caution)';
if (score >= 3) return 'var(--health-warning)';
return 'var(--health-critical)';
}
// Get node radius based on type/depth
function getNodeRadius(node) {
if (node.type === 'root') return 20;
if (node.depth === 1) return 12;
if (node.depth === 2) return 8;
return 6;
}
// Create SVG
const svg = d3.select("#container")
.append("svg")
.attr("width", width)
.attr("height", height);
// Zoom behavior
const zoom = d3.zoom()
.scaleExtent([0.1, 4])
.on("zoom", (event) => {
g.attr("transform", event.transform);
currentZoom = event.transform.k;
updateZoomDisplay();
});
svg.call(zoom);
const g = svg.append("g");
// Create force simulation
const simulation = d3.forceSimulation(graphData.nodes)
.force("link", d3.forceLink(graphData.links)
.id(d => d.id)
.distance(d => {
const sourceDepth = d.source.depth || 0;
const targetDepth = d.target.depth || 0;
return 50 + Math.abs(sourceDepth - targetDepth) * 30;
}))
.force("charge", d3.forceManyBody().strength(-200))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(d => getNodeRadius(d) + 10));
// Draw links
const link = g.append("g")
.attr("class", "links")
.selectAll("line")
.data(graphData.links)
.join("line")
.attr("class", "link");
// Draw nodes
const node = g.append("g")
.attr("class", "nodes")
.selectAll("g")
.data(graphData.nodes)
.join("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded))
.on("mouseover", handleMouseOver)
.on("mouseout", handleMouseOut)
.on("click", handleClick);
// Add circles
node.append("circle")
.attr("r", d => getNodeRadius(d))
.attr("fill", d => getHealthColor(d));
// Add labels
node.append("text")
.attr("class", "node-label")
.attr("dy", d => getNodeRadius(d) + 12)
.attr("text-anchor", "middle")
.text(d => {
const name = d.name || d.id;
return name.length > 20 ? name.substring(0, 17) + '...' : name;
});
// Simulation tick
simulation.on("tick", () => {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node.attr("transform", d => "translate(" + d.x + "," + d.y + ")");
});
// Drag functions
function dragStarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragEnded(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
// Tooltip functions
function handleMouseOver(event, d) {
// Highlight connected links
link.classed("highlighted", l =>
l.source.id === d.id || l.target.id === d.id
);
// Show tooltip
const tooltip = document.getElementById('tooltip');
const score = d.healthScore || 8;
tooltip.innerHTML =
'<div class="tooltip-title">' + (d.name || d.id) + '</div>' +
'<div class="tooltip-row"><span class="tooltip-label">Version</span><span class="tooltip-value">' + (d.version || 'N/A') + '</span></div>' +
'<div class="tooltip-row"><span class="tooltip-label">Health Score</span><span class="tooltip-value">' + score + '/10</span></div>' +
'<div class="tooltip-row"><span class="tooltip-label">Depth</span><span class="tooltip-value">' + (d.depth || 0) + '</span></div>' +
'<div class="tooltip-row"><span class="tooltip-label">Type</span><span class="tooltip-value">' + (d.type || 'dependency') + '</span></div>';
tooltip.classList.add('visible');
const x = Math.min(event.pageX + 15, window.innerWidth - 300);
const y = event.pageY - 10;
tooltip.style.left = x + 'px';
tooltip.style.top = y + 'px';
}
function handleMouseOut() {
link.classed("highlighted", false);
document.getElementById('tooltip').classList.remove('visible');
}
function handleClick(event, d) {
// Toggle selection
if (selectedNode === d) {
selectedNode = null;
node.classed("selected", false);
document.getElementById('selected-count').textContent = '0';
} else {
selectedNode = d;
node.classed("selected", n => n === d);
document.getElementById('selected-count').textContent = '1';
}
}
// Control functions
function resetLayout() {
simulation.alpha(1).restart();
svg.transition().duration(750).call(
zoom.transform,
d3.zoomIdentity
);
}
function centerView() {
svg.transition().duration(500).call(
zoom.transform,
d3.zoomIdentity.translate(width / 2, height / 2).scale(currentZoom).translate(-width / 2, -height / 2)
);
}
function fitToScreen() {
const bounds = g.node().getBBox();
const fullWidth = width;
const fullHeight = height;
const midX = bounds.x + bounds.width / 2;
const midY = bounds.y + bounds.height / 2;
const scale = 0.8 / Math.max(bounds.width / fullWidth, bounds.height / fullHeight);
svg.transition().duration(750).call(
zoom.transform,
d3.zoomIdentity.translate(fullWidth / 2 - scale * midX, fullHeight / 2 - scale * midY).scale(scale)
);
}
function toggleFullscreen() {
const elem = document.documentElement;
if (!document.fullscreenElement && !document.webkitFullscreenElement) {
if (elem.requestFullscreen) {
elem.requestFullscreen();
} else if (elem.webkitRequestFullscreen) {
elem.webkitRequestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
}
function toggleLabels() {
showLabels = !showLabels;
d3.selectAll('.node-label').classed('hidden', !showLabels);
document.getElementById('btn-labels').textContent = showLabels ? '🏷️ Hide Labels' : '🏷️ Show Labels';
document.getElementById('btn-labels').classList.toggle('active', showLabels);
}
function toggleLinks() {
showLinks = !showLinks;
d3.selectAll('.link').classed('hidden', !showLinks);
document.getElementById('btn-links').textContent = showLinks ? '🔗 Hide Links' : '🔗 Show Links';
document.getElementById('btn-links').classList.toggle('active', showLinks);
}
function applyFilters() {
const healthFilter = document.getElementById('health-filter').value;
let visibleCount = 0;
node.style('display', d => {
let show = true;
if (healthFilter !== 'all') {
const score = d.healthScore || 8;
switch (healthFilter) {
case 'excellent': show = score >= 9; break;
case 'good': show = score >= 7 && score < 9; break;
case 'caution': show = score >= 5 && score < 7; break;
case 'warning': show = score >= 3 && score < 5; break;
case 'critical': show = score < 3; break;
}
}
if (show) visibleCount++;
return show ? 'block' : 'none';
});
document.getElementById('visible-nodes').textContent = visibleCount;
}
function zoomIn() {
svg.transition().duration(300).call(zoom.scaleBy, 1.3);
}
function zoomOut() {
svg.transition().duration(300).call(zoom.scaleBy, 0.7);
}
function updateZoomDisplay() {
const zoomPercent = Math.round(currentZoom * 100) + '%';
document.getElementById('zoom-level').textContent = zoomPercent;
document.getElementById('zoom-stat').textContent = zoomPercent;
}
// Search functionality
document.getElementById('search').addEventListener('input', (e) => {
const searchTerm = e.target.value.toLowerCase();
let visibleCount = 0;
node.style('display', d => {
const name = (d.name || d.id).toLowerCase();
const match = !searchTerm || name.includes(searchTerm);
if (match) visibleCount++;
return match ? 'block' : 'none';
});
document.getElementById('visible-nodes').textContent = visibleCount;
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT') return;
if (e.key === '+' || e.key === '=') zoomIn();
if (e.key === '-') zoomOut();
if (e.key === 'r' || e.key === 'R') resetLayout();
if (e.key === 'f' || e.key === 'F') document.getElementById('search').focus();
if (e.key === 'l' || e.key === 'L') toggleLabels();
if (e.key === 'Escape') {
selectedNode = null;
node.classed("selected", false);
document.getElementById('selected-count').textContent = '0';
}
});
</script>
</body>
</html>`;
}
/**
* Generate empty state HTML
*/
function generateEmptyStateHTML(projectName, projectVersion) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>DevCompass - No Dependencies</title>
<style>
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
color: #f1f5f9;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
}
.message {
text-align: center;
padding: 40px;
background: rgba(30, 41, 59, 0.95);
border-radius: 20px;
border: 1px solid #475569;
}
.icon { font-size: 64px; margin-bottom: 20px; }
h1 { margin: 0 0 10px; font-size: 24px; }
p { color: #94a3b8; margin: 0; }
</style>
</head>
<body>
<div class="message">
<div class="icon">📦</div>
<h1>No Dependencies Found</h1>
<p>${projectName} v${projectVersion} has no dependencies to visualize.</p>
</div>
</body>
</html>`;
}
/**
* Legacy function for backward compatibility
*/
function generateForceLayout(graphData, options = {}) {
return generateForceLayoutHTML(graphData, options);
}
module.exports = {
generateForceLayout,
generateForceLayoutHTML
};
// src/graph/layouts/radial.js
// Fixed radial layout with improved label positioning and collision handling
function generateRadialLayoutHTML(graphData, options = {}) {
const width = options.width || 1400;
const height = options.height || 900;
const projectName = options.projectName || 'Project';
const projectVersion = options.projectVersion || '1.0.0';
// Validate input
const nodes = Array.isArray(graphData.nodes) ? graphData.nodes : [];
const links = Array.isArray(graphData.links) ? graphData.links : [];
if (nodes.length === 0) {
return generateEmptyStateHTML(projectName, projectVersion);
}
const graphDataJSON = JSON.stringify({ nodes, links });
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevCompass - Radial Dependency Graph</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
:root {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--accent-blue: #3b82f6;
--accent-cyan: #06b6d4;
--accent-purple: #8b5cf6;
--border-color: #475569;
--health-excellent: #10b981;
--health-good: #84cc16;
--health-caution: #eab308;
--health-warning: #f97316;
--health-critical: #ef4444;
--root-color: #60a5fa;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
color: var(--text-primary);
min-height: 100vh;
overflow: hidden;
}
#container {
width: 100vw;
height: 100vh;
position: relative;
}
svg {
width: 100%;
height: 100%;
cursor: grab;
}
svg:active { cursor: grabbing; }
/* Header */
.header {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(30, 41, 59, 0.95);
padding: 16px 32px;
border-radius: 16px;
border: 1px solid var(--border-color);
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
z-index: 100;
text-align: center;
}
.header-title {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 4px;
}
.header-subtitle {
font-size: 12px;
color: var(--text-secondary);
}
/* Controls Panel */
.controls {
position: fixed;
top: 20px;
right: 20px;
background: rgba(30, 41, 59, 0.95);
padding: 20px;
border-radius: 16px;
border: 1px solid var(--border-color);
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
z-index: 100;
min-width: 180px;
}
.controls-title {
font-size: 13px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 12px;
}
.control-btn {
display: block;
width: 100%;
padding: 10px 14px;
margin: 6px 0;
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
text-align: left;
transition: all 0.2s ease;
}
.control-btn:hover {
background: var(--accent-blue);
border-color: var(--accent-blue);
}
.control-btn.active {
background: var(--accent-purple);
border-color: var(--accent-purple);
}
/* Legend */
.legend {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(30, 41, 59, 0.95);
padding: 16px 20px;
border-radius: 16px;
border: 1px solid var(--border-color);
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
z-index: 100;
}
.legend-title {
font-size: 13px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 12px;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
margin: 6px 0;
font-size: 11px;
color: var(--text-secondary);
}
.legend-dot {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
}
/* Depth circles visualization */
.depth-circle {
fill: none;
stroke: var(--border-color);
stroke-width: 1px;
stroke-dasharray: 4, 4;
opacity: 0.4;
}
.depth-label {
fill: var(--text-muted);
font-size: 10px;
font-weight: 500;
}
/* Node styles */
.node circle {
stroke: var(--bg-primary);
stroke-width: 2px;
cursor: pointer;
transition: all 0.2s ease;
}
.node circle:hover {
stroke: var(--text-primary);
stroke-width: 3px;
filter: drop-shadow(0 0 10px currentColor);
}
.node-label {
font-size: 9px;
fill: var(--text-secondary);
pointer-events: none;
font-weight: 500;
text-shadow:
-1px -1px 2px var(--bg-primary),
1px -1px 2px var(--bg-primary),
-1px 1px 2px var(--bg-primary),
1px 1px 2px var(--bg-primary);
}
.node-label.hidden { display: none; }
/* Link styles */
.link {
fill: none;
stroke: var(--border-color);
stroke-width: 1px;
stroke-opacity: 0.4;
}
.link.highlighted {
stroke: var(--accent-cyan);
stroke-opacity: 0.8;
stroke-width: 2px;
}
/* Tooltip */
.tooltip {
position: absolute;
padding: 14px 18px;
background: rgba(15, 23, 42, 0.98);
border: 1px solid var(--border-color);
border-radius: 12px;
font-size: 12px;
max-width: 280px;
pointer-events: none;
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.2s, transform 0.2s;
z-index: 1000;
backdrop-filter: blur(12px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}
.tooltip.visible {
opacity: 1;
transform: translateY(0);
}
.tooltip-title {
font-weight: 700;
color: var(--accent-cyan);
margin-bottom: 8px;
font-size: 13px;
}
.tooltip-row {
display: flex;
justify-content: space-between;
margin: 4px 0;
}
.tooltip-label { color: var(--text-secondary); }
.tooltip-value { color: var(--text-primary); font-weight: 600; }
/* Zoom Controls */
.zoom-controls {
position: fixed;
bottom: 20px;
left: 20px;
display: flex;
gap: 8px;
z-index: 100;
}
.zoom-btn {
width: 40px;
height: 40px;
background: rgba(30, 41, 59, 0.95);
border: 1px solid var(--border-color);
border-radius: 10px;
color: var(--text-primary);
font-size: 18px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
backdrop-filter: blur(12px);
}
.zoom-btn:hover {
background: var(--accent-blue);
border-color: var(--accent-blue);
}
</style>
</head>
<body>
<div id="container"></div>
<div class="header">
<div class="header-title">🌐 Radial Dependency Graph</div>
<div class="header-subtitle">Concentric circles by dependency depth</div>
</div>
<div class="controls">
<div class="controls-title">Display Options</div>
<button class="control-btn active" id="btn-labels" onclick="toggleLabels()">Toggle Labels</button>
<button class="control-btn" id="btn-depth" onclick="toggleDepthCircles()">Toggle Depth Circles</button>
<button class="control-btn" id="btn-links" onclick="toggleLinks()">Toggle Links</button>
</div>
<div class="legend">
<div class="legend-title">Health Status</div>
<div class="legend-item">
<div class="legend-dot" style="background: var(--health-excellent);"></div>
<span>Healthy (7-10)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: var(--health-caution);"></div>
<span>Caution (5-7)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: var(--health-warning);"></div>
<span>Warning (3-5)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: var(--health-critical);"></div>
<span>Critical (<3)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: var(--root-color);"></div>
<span>Root Package</span>
</div>
</div>
<div class="zoom-controls">
<button class="zoom-btn" onclick="zoomIn()">+</button>
<button class="zoom-btn" onclick="zoomOut()">−</button>
<button class="zoom-btn" onclick="resetZoom()">⟲</button>
</div>
<div class="tooltip" id="tooltip"></div>
<script>
const graphData = ${graphDataJSON};
const width = window.innerWidth;
const height = window.innerHeight;
const centerX = width / 2;
const centerY = height / 2;
let showLabels = true;
let showDepthCircles = true;
let showLinks = true;
let currentZoom = 1;
// Calculate max depth
const maxDepth = Math.max(...graphData.nodes.map(n => n.depth || 0), 1);
// Radius configuration - distribute nodes across available space
const minRadius = 60;
const maxRadius = Math.min(width, height) / 2 - 100;
const radiusStep = (maxRadius - minRadius) / Math.max(maxDepth, 1);
// Get radius for depth level
function getRadiusForDepth(depth) {
if (depth === 0) return 0; // Root at center
return minRadius + (depth - 1) * radiusStep + radiusStep / 2;
}
// Get color based on health score
function getHealthColor(node) {
if (node.type === 'root' || node.depth === 0) return 'var(--root-color)';
const score = node.healthScore || 8;
if (score >= 7) return 'var(--health-excellent)';
if (score >= 5) return 'var(--health-caution)';
if (score >= 3) return 'var(--health-warning)';
return 'var(--health-critical)';
}
// Get node radius based on type/depth
function getNodeRadius(node) {
if (node.type === 'root' || node.depth === 0) return 20;
if (node.depth === 1) return 10;
return 6;
}
// Group nodes by depth for angular distribution
const nodesByDepth = {};
graphData.nodes.forEach(node => {
const depth = node.depth || 0;
if (!nodesByDepth[depth]) nodesByDepth[depth] = [];
nodesByDepth[depth].push(node);
});
// Calculate positions for each node
graphData.nodes.forEach(node => {
const depth = node.depth || 0;
const nodesAtDepth = nodesByDepth[depth];
const index = nodesAtDepth.indexOf(node);
const count = nodesAtDepth.length;
if (depth === 0) {
// Root at center
node.x = centerX;
node.y = centerY;
} else {
// Distribute evenly around circle at this depth
// Add some offset to avoid all nodes starting at same angle
const angleOffset = (depth * 0.3); // Stagger by depth
const angle = (2 * Math.PI * index / count) + angleOffset;
const radius = getRadiusForDepth(depth);
node.x = centerX + radius * Math.cos(angle);
node.y = centerY + radius * Math.sin(angle);
node.angle = angle; // Store for label positioning
}
});
// Create SVG
const svg = d3.select("#container")
.append("svg")
.attr("width", width)
.attr("height", height);
// Zoom behavior
const zoom = d3.zoom()
.scaleExtent([0.2, 4])
.on("zoom", (event) => {
g.attr("transform", event.transform);
currentZoom = event.transform.k;
});
svg.call(zoom);
const g = svg.append("g");
// Draw depth circles (concentric rings)
const depthCirclesGroup = g.append("g").attr("class", "depth-circles");
for (let d = 1; d <= maxDepth; d++) {
const r = getRadiusForDepth(d);
depthCirclesGroup.append("circle")
.attr("class", "depth-circle")
.attr("cx", centerX)
.attr("cy", centerY)
.attr("r", r);
// Depth label
depthCirclesGroup.append("text")
.attr("class", "depth-label")
.attr("x", centerX + r + 5)
.attr("y", centerY - 5)
.text("Depth " + d);
}
// Build link lookup for highlighting
const linkLookup = new Set();
graphData.links.forEach(l => {
const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
const targetId = typeof l.target === 'object' ? l.target.id : l.target;
linkLookup.add(sourceId + '-' + targetId);
});
// Create node lookup
const nodeById = new Map();
graphData.nodes.forEach(n => nodeById.set(n.id, n));
// Draw links with curved paths
const linksGroup = g.append("g").attr("class", "links");
const links = linksGroup.selectAll(".link")
.data(graphData.links)
.join("path")
.attr("class", "link")
.attr("d", d => {
const sourceId = typeof d.source === 'object' ? d.source.id : d.source;
const targetId = typeof d.target === 'object' ? d.target.id : d.target;
const source = nodeById.get(sourceId);
const target = nodeById.get(targetId);
if (!source || !target) return '';
// Use curved path through center point for radial layout
const midX = (source.x + target.x) / 2;
const midY = (source.y + target.y) / 2;
// Pull midpoint slightly toward center for nice curves
const pullFactor = 0.2;
const curveX = midX + (centerX - midX) * pullFactor;
const curveY = midY + (centerY - midY) * pullFactor;
return "M" + source.x + "," + source.y
+ "Q" + curveX + "," + curveY
+ " " + target.x + "," + target.y;
});
// Draw nodes
const nodesGroup = g.append("g").attr("class", "nodes");
const node = nodesGroup.selectAll(".node")
.data(graphData.nodes)
.join("g")
.attr("class", "node")
.attr("transform", d => "translate(" + d.x + "," + d.y + ")")
.on("mouseover", handleMouseOver)
.on("mouseout", handleMouseOut);
// Add circles
node.append("circle")
.attr("r", d => getNodeRadius(d))
.attr("fill", d => getHealthColor(d));
// Add labels with smart positioning
node.append("text")
.attr("class", "node-label")
.attr("dy", d => {
if (d.depth === 0) return -28;
// Position label outside the circle based on angle
const angle = d.angle || 0;
return Math.sin(angle) > 0.3 ? 20 : (Math.sin(angle) < -0.3 ? -12 : 4);
})
.attr("dx", d => {
if (d.depth === 0) return 0;
const angle = d.angle || 0;
return Math.cos(angle) > 0.3 ? 12 : (Math.cos(angle) < -0.3 ? -12 : 0);
})
.attr("text-anchor", d => {
if (d.depth === 0) return "middle";
const angle = d.angle || 0;
if (Math.cos(angle) > 0.3) return "start";
if (Math.cos(angle) < -0.3) return "end";
return "middle";
})
.text(d => {
// Truncate long names
const name = d.name || d.id;
return name.length > 15 ? name.substring(0, 12) + '...' : name;
});
// Tooltip functions
function handleMouseOver(event, d) {
// Highlight connected links
links.classed("highlighted", l => {
const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
const targetId = typeof l.target === 'object' ? l.target.id : l.target;
return sourceId === d.id || targetId === d.id;
});
// Show tooltip
const tooltip = document.getElementById('tooltip');
const score = d.healthScore || 8;
tooltip.innerHTML =
'<div class="tooltip-title">' + (d.name || d.id) + '</div>' +
'<div class="tooltip-row"><span class="tooltip-label">Version</span><span class="tooltip-value">' + (d.version || 'N/A') + '</span></div>' +
'<div class="tooltip-row"><span class="tooltip-label">Health Score</span><span class="tooltip-value">' + score + '/10</span></div>' +
'<div class="tooltip-row"><span class="tooltip-label">Depth</span><span class="tooltip-value">' + (d.depth || 0) + '</span></div>' +
'<div class="tooltip-row"><span class="tooltip-label">Type</span><span class="tooltip-value">' + (d.type || 'dependency') + '</span></div>';
tooltip.classList.add('visible');
const x = Math.min(event.pageX + 15, window.innerWidth - 300);
const y = event.pageY - 10;
tooltip.style.left = x + 'px';
tooltip.style.top = y + 'px';
}
function handleMouseOut() {
links.classed("highlighted", false);
document.getElementById('tooltip').classList.remove('visible');
}
// Control functions
function toggleLabels() {
showLabels = !showLabels;
d3.selectAll('.node-label').classed('hidden', !showLabels);
document.getElementById('btn-labels').classList.toggle('active', showLabels);
}
function toggleDepthCircles() {
showDepthCircles = !showDepthCircles;
d3.selectAll('.depth-circles').style('display', showDepthCircles ? 'block' : 'none');
document.getElementById('btn-depth').classList.toggle('active', showDepthCircles);
}
function toggleLinks() {
showLinks = !showLinks;
d3.selectAll('.links').style('display', showLinks ? 'block' : 'none');
document.getElementById('btn-links').classList.toggle('active', showLinks);
}
function zoomIn() {
svg.transition().duration(300).call(zoom.scaleBy, 1.3);
}
function zoomOut() {
svg.transition().duration(300).call(zoom.scaleBy, 0.7);
}
function resetZoom() {
svg.transition().duration(500).call(zoom.transform, d3.zoomIdentity);
}
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'l' || e.key === 'L') toggleLabels();
if (e.key === 'd' || e.key === 'D') toggleDepthCircles();
if (e.key === '+' || e.key === '=') zoomIn();
if (e.key === '-') zoomOut();
if (e.key === 'r' || e.key === 'R') resetZoom();
});
</script>
</body>
</html>`;
}
/**
* Generate empty state HTML
*/
function generateEmptyStateHTML(projectName, projectVersion) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>DevCompass - No Dependencies</title>
<style>
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
color: #f1f5f9;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.message {
text-align: center;
padding: 40px;
background: rgba(30, 41, 59, 0.95);
border-radius: 20px;
border: 1px solid #475569;
}
.icon { font-size: 64px; margin-bottom: 20px; }
h1 { margin: 0 0 10px; font-size: 24px; }
p { color: #94a3b8; margin: 0; }
</style>
</head>
<body>
<div class="message">
<div class="icon">🌐</div>
<h1>No Dependencies Found</h1>
<p>${projectName} has no dependencies to visualize.</p>
</div>
</body>
</html>`;
}
/**
* Legacy function for backward compatibility
*/
function generateRadialLayout(graphData, options = {}) {
return generateRadialLayoutHTML(graphData, options);
}
module.exports = {
generateRadialLayout,
generateRadialLayoutHTML
};
// src/graph/layouts/tree.js
// Fixed tree layout with proper horizontal spreading
// v3.1.2 - Fixed overlapping Controls/Statistics panels
function generateTreeLayoutHTML(graphData, options = {}) {
const width = options.width || 1400;
const height = options.height || 900;
const projectName = options.projectName || 'Project';
const projectVersion = options.projectVersion || '1.0.0';
// Validate input
const nodes = Array.isArray(graphData.nodes) ? graphData.nodes : [];
const links = Array.isArray(graphData.links) ? graphData.links : [];
if (nodes.length === 0) {
return generateEmptyStateHTML(projectName, projectVersion);
}
// Build hierarchy from flat nodes/links
const hierarchyData = buildHierarchy(nodes, links);
const graphDataJSON = JSON.stringify({ nodes, links, hierarchy: hierarchyData });
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevCompass - Dependency Tree</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
:root {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--accent-blue: #3b82f6;
--accent-cyan: #06b6d4;
--border-color: #475569;
--health-excellent: #10b981;
--health-good: #84cc16;
--health-caution: #eab308;
--health-warning: #f97316;
--health-critical: #ef4444;
--root-color: #60a5fa;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
color: var(--text-primary);
min-height: 100vh;
overflow: hidden;
}
#container {
width: 100vw;
height: 100vh;
position: relative;
}
svg {
width: 100%;
height: 100%;
cursor: grab;
}
svg:active { cursor: grabbing; }
/* Header */
.header {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(30, 41, 59, 0.95);
padding: 16px 32px;
border-radius: 16px;
border: 1px solid var(--border-color);
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
z-index: 100;
text-align: center;
}
.header-title {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 4px;
}
.header-meta {
font-size: 12px;
color: var(--text-secondary);
display: flex;
gap: 16px;
justify-content: center;
}
.header-meta span { display: flex; align-items: center; gap: 4px; }
/* Right Sidebar - Contains Controls and Stats stacked vertically */
.right-sidebar {
position: fixed;
top: 100px;
right: 20px;
display: flex;
flex-direction: column;
gap: 16px;
z-index: 100;
max-height: calc(100vh - 140px);
overflow-y: auto;
}
/* Hide scrollbar but keep functionality */
.right-sidebar::-webkit-scrollbar {
width: 4px;
}
.right-sidebar::-webkit-scrollbar-track {
background: transparent;
}
.right-sidebar::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 2px;
}
/* Panel base style (shared by controls and stats) */
.panel {
background: rgba(30, 41, 59, 0.95);
padding: 16px 20px;
border-radius: 16px;
border: 1px solid var(--border-color);
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
min-width: 180px;
}
.panel-title {
font-size: 13px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
/* Control buttons */
.control-btn {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 10px 14px;
margin: 6px 0;
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 10px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s ease;
}
.control-btn:hover {
background: var(--accent-blue);
border-color: var(--accent-blue);
transform: translateX(-2px);
}
.control-btn.primary {
background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-cyan) 100%);
border-color: var(--accent-blue);
}
.control-btn:first-of-type {
margin-top: 0;
}
/* Stats rows */
.stat-row {
display: flex;
justify-content: space-between;
margin: 8px 0;
font-size: 12px;
}
.stat-row:first-of-type {
margin-top: 0;
}
.stat-label { color: var(--text-secondary); }
.stat-value { color: var(--accent-cyan); font-weight: 700; }
/* Zoom Controls - Bottom Right */
.zoom-controls {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 100;
}
.zoom-btn {
width: 44px;
height: 44px;
background: rgba(30, 41, 59, 0.95);
border: 1px solid var(--border-color);
border-radius: 12px;
color: var(--text-primary);
font-size: 20px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
backdrop-filter: blur(12px);
}
.zoom-btn:hover {
background: var(--accent-blue);
border-color: var(--accent-blue);
transform: scale(1.1);
}
.zoom-level {
width: 44px;
padding: 8px;
background: rgba(30, 41, 59, 0.95);
border: 1px solid var(--border-color);
border-radius: 12px;
text-align: center;
color: var(--accent-cyan);
font-size: 10px;
font-weight: 700;
backdrop-filter: blur(12px);
}
/* Legend - Bottom Left */
.legend {
position: fixed;
bottom: 20px;
left: 20px;
background: rgba(30, 41, 59, 0.95);
padding: 16px 20px;
border-radius: 16px;
border: 1px solid var(--border-color);
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
z-index: 100;
}
.legend-title {
font-size: 13px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 12px;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
margin: 8px 0;
font-size: 12px;
color: var(--text-secondary);
}
.legend-dot {
width: 14px;
height: 14px;
border-radius: 50%;
border: 2px solid var(--bg-primary);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
/* Tooltip */
.tooltip {
position: absolute;
padding: 14px 18px;
background: rgba(15, 23, 42, 0.98);
border: 1px solid var(--border-color);
border-radius: 12px;
font-size: 13px;
max-width: 300px;
pointer-events: none;
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.2s, transform 0.2s;
z-index: 1000;
backdrop-filter: blur(12px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}
.tooltip.visible {
opacity: 1;
transform: translateY(0);
}
.tooltip-title {
font-weight: 700;
color: var(--accent-cyan);
margin-bottom: 8px;
font-size: 14px;
}
.tooltip-row {
display: flex;
justify-content: space-between;
margin: 4px 0;
}
.tooltip-label { color: var(--text-secondary); }
.tooltip-value { color: var(--text-primary); font-weight: 600; }
/* Tree specific styles */
.node circle {
stroke: var(--bg-primary);
stroke-width: 2px;
cursor: pointer;
transition: all 0.2s ease;
}
.node circle:hover {
stroke: var(--text-primary);
stroke-width: 3px;
filter: drop-shadow(0 0 8px currentColor);
}
.node text {
font-size: 11px;
fill: var(--text-secondary);
pointer-events: none;
font-weight: 500;
}
.link {
fill: none;
stroke: var(--border-color);
stroke-width: 1.5px;
stroke-opacity: 0.6;
}
.link:hover {
stroke: var(--accent-cyan);
stroke-opacity: 1;
stroke-width: 2px;
}
</style>
</head>
<body>
<div id="container"></div>
<!-- Header - Top Center -->
<div class="header">
<div class="header-title">🧭 Dependency Tree</div>
<div class="header-meta">
<span><strong>Version:</strong> ${projectVersion}</span>
<span><strong>Dependencies:</strong> ${nodes.length}</span>
<span><strong>Generated:</strong> ${new Date().toLocaleString()}</span>
</div>
</div>
<!-- Right Sidebar - Controls and Stats stacked vertically -->
<div class="right-sidebar">
<!-- Controls Panel -->
<div class="panel" id="controls-panel">
<div class="panel-title">⚙️ Controls</div>
<button class="control-btn primary" onclick="resetView()">↻ Reset View</button>
<button class="control-btn" onclick="fitToScreen()">⛶ Fit to Screen</button>
<button class="control-btn" onclick="toggleLabels()">🏷️ Toggle Labels</button>
<button class="control-btn" onclick="expandAll()">📂 Expand All</button>
<button class="control-btn" onclick="collapseAll()">📁 Collapse All</button>
</div>
<!-- Statistics Panel - Separate panel below controls -->
<div class="panel" id="stats-panel">
<div class="panel-title">📊 Statistics</div>
<div class="stat-row">
<span class="stat-label">Total Nodes</span>
<span class="stat-value" id="total-nodes">${nodes.length}</span>
</div>
<div class="stat-row">
<span class="stat-label">Visible</span>
<span class="stat-value" id="visible-nodes">${nodes.length}</span>
</div>
<div class="stat-row">
<span class="stat-label">Max Depth</span>
<span class="stat-value" id="max-depth">0</span>
</div>
<div class="stat-row">
<span class="stat-label">Zoom</span>
<span class="stat-value" id="zoom-stat">100%</span>
</div>
</div>
</div>
<!-- Zoom Controls - Bottom Right -->
<div class="zoom-controls">
<button class="zoom-btn" onclick="zoomIn()">+</button>
<div class="zoom-level" id="zoom-level">100%</div>
<button class="zoom-btn" onclick="zoomOut()">−</button>
<button class="zoom-btn" onclick="resetView()">⟲</button>
</div>
<!-- Legend - Bottom Left -->
<div class="legend">
<div class="legend-title">🎨 Health Status</div>
<div class="legend-item">
<div class="legend-dot" style="background: var(--health-excellent);"></div>
<span>Excellent (9-10)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: var(--health-good);"></div>
<span>Good (7-8)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: var(--health-caution);"></div>
<span>Caution (5-7)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: var(--health-warning);"></div>
<span>Warning (3-5)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: var(--health-critical);"></div>
<span>Critical (<3)</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background: var(--root-color);"></div>
<span>Root Package</span>
</div>
</div>
<!-- Tooltip -->
<div class="tooltip" id="tooltip"></div>
<script>
const graphData = ${graphDataJSON};
const width = window.innerWidth;
const height = window.innerHeight;
let showLabels = true;
let currentZoom = 1;
// Build hierarchy from nodes and links
function buildHierarchyFromData(nodes, links) {
if (!nodes || nodes.length === 0) return null;
// Create node map
const nodeMap = new Map();
nodes.forEach(n => {
nodeMap.set(n.id, { ...n, children: [] });
});
// Find root (type === 'root' or depth === 0)
let root = nodes.find(n => n.type === 'root' || n.depth === 0);
if (!root) root = nodes[0];
// Build parent-child relationships from links
const childrenAdded = new Set();
links.forEach(link => {
const sourceId = typeof link.source === 'object' ? link.source.id : link.source;
const targetId = typeof link.target === 'object' ? link.target.id : link.target;
const parent = nodeMap.get(sourceId);
const child = nodeMap.get(targetId);
if (parent && child && !childrenAdded.has(targetId)) {
parent.children.push(child);
childrenAdded.add(targetId);
}
});
return nodeMap.get(root.id);
}
// Get color based on health score
function getHealthColor(node) {
if (node.type === 'root') return 'var(--root-color)';
const score = node.healthScore || 8;
if (score >= 9) return 'var(--health-excellent)';
if (score >= 7) return 'var(--health-good)';
if (score >= 5) return 'var(--health-caution)';
if (score >= 3) return 'var(--health-warning)';
return 'var(--health-critical)';
}
// Get node radius based on type
function getNodeRadius(node) {
if (node.type === 'root') return 18;
if (node.depth === 1) return 10;
return 7;
}
// Create SVG
const svg = d3.select("#container")
.append("svg")
.attr("width", width)
.attr("height", height);
// Create zoom behavior
const zoom = d3.zoom()
.scaleExtent([0.1, 4])
.on("zoom", (event) => {
g.attr("transform", event.transform);
currentZoom = event.transform.k;
updateZoomDisplay();
});
svg.call(zoom);
const g = svg.append("g");
// Build hierarchy
const hierarchyRoot = buildHierarchyFromData(graphData.nodes, graphData.links);
if (hierarchyRoot) {
const root = d3.hierarchy(hierarchyRoot);
// Calculate max depth for display
let maxDepth = 0;
root.each(d => { maxDepth = Math.max(maxDepth, d.depth); });
document.getElementById('max-depth').textContent = maxDepth;
// Create tree layout - HORIZONTAL orientation for better visibility
const treeLayout = d3.tree()
.size([height - 200, width - 400])
.separation((a, b) => (a.parent === b.parent ? 1.5 : 2));
treeLayout(root);
// Swap x and y for horizontal layout (top-to-bottom becomes left-to-right)
root.each(d => {
const temp = d.x;
d.x = d.y + 200; // Add left margin
d.y = temp + 100; // Add top margin
});
// Draw links with curved paths
const linkGenerator = d3.linkHorizontal()
.x(d => d.x)
.y(d => d.y);
g.selectAll(".link")
.data(root.links())
.join("path")
.attr("class", "link")
.attr("d", linkGenerator);
// Draw nodes
const node = g.selectAll(".node")
.data(root.descendants())
.join("g")
.attr("class", "node")
.attr("transform", d => \`translate(\${d.x},\${d.y})\`)
.on("mouseover", showTooltip)
.on("mouseout", hideTooltip)
.on("click", toggleChildren);
// Add circles
node.append("circle")
.attr("r", d => getNodeRadius(d.data))
.attr("fill", d => getHealthColor(d.data));
// Add labels
node.append("text")
.attr("dy", d => d.data.type === 'root' ? -25 : -15)
.attr("text-anchor", "middle")
.text(d => d.data.name || d.data.id)
.attr("class", "node-label");
// Initial fit to screen
setTimeout(fitToScreen, 100);
}
// Tooltip functions
function showTooltip(event, d) {
const tooltip = document.getElementById('tooltip');
const node = d.data;
const score = node.healthScore || 8;
tooltip.innerHTML = \`
<div class="tooltip-title">\${node.name || node.id}</div>
<div class="tooltip-row">
<span class="tooltip-label">Version</span>
<span class="tooltip-value">\${node.version || 'N/A'}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Health Score</span>
<span class="tooltip-value">\${score}/10</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Depth</span>
<span class="tooltip-value">\${d.depth}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Children</span>
<span class="tooltip-value">\${d.children ? d.children.length : 0}</span>
</div>
\`;
tooltip.classList.add('visible');
tooltip.style.left = (event.pageX + 15) + 'px';
tooltip.style.top = (event.pageY - 10) + 'px';
}
function hideTooltip() {
document.getElementById('tooltip').classList.remove('visible');
}
// Toggle children visibility (collapse/expand)
function toggleChildren(event, d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else if (d._children) {
d.children = d._children;
d._children = null;
}
// Note: Full implementation would re-render the tree
}
// Control functions
function resetView() {
svg.transition().duration(750).call(
zoom.transform,
d3.zoomIdentity
);
}
function fitToScreen() {
const bounds = g.node().getBBox();
const fullWidth = width;
const fullHeight = height;
const boundsWidth = bounds.width;
const boundsHeight = bounds.height;
const midX = bounds.x + boundsWidth / 2;
const midY = bounds.y + boundsHeight / 2;
const scale = 0.85 / Math.max(boundsWidth / fullWidth, boundsHeight / fullHeight);
const translate = [fullWidth / 2 - scale * midX, fullHeight / 2 - scale * midY];
svg.transition().duration(750).call(
zoom.transform,
d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale)
);
}
function toggleLabels() {
showLabels = !showLabels;
d3.selectAll('.node-label').style('display', showLabels ? 'block' : 'none');
}
function expandAll() {
// Placeholder for full expand functionality
console.log('Expand all nodes');
}
function collapseAll() {
// Placeholder for full collapse functionality
console.log('Collapse all nodes');
}
function zoomIn() {
svg.transition().duration(300).call(zoom.scaleBy, 1.3);
}
function zoomOut() {
svg.transition().duration(300).call(zoom.scaleBy, 0.7);
}
function updateZoomDisplay() {
const zoomPercent = Math.round(currentZoom * 100) + '%';
document.getElementById('zoom-level').textContent = zoomPercent;
document.getElementById('zoom-stat').textContent = zoomPercent;
}
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === '+' || e.key === '=') zoomIn();
if (e.key === '-') zoomOut();
if (e.key === 'r' || e.key === 'R') resetView();
if (e.key === 'f' || e.key === 'F') fitToScreen();
if (e.key === 'l' || e.key === 'L') toggleLabels();
});
</script>
</body>
</html>`;
}
/**
* Generate empty state HTML when no nodes
*/
function generateEmptyStateHTML(projectName, projectVersion) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>DevCompass - No Dependencies</title>
<style>
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
color: #f1f5f9;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
}
.message {
text-align: center;
padding: 40px;
background: rgba(30, 41, 59, 0.95);
border-radius: 20px;
border: 1px solid #475569;
}
.icon { font-size: 64px; margin-bottom: 20px; }
h1 { margin: 0 0 10px; font-size: 24px; }
p { color: #94a3b8; margin: 0; }
</style>
</head>
<body>
<div class="message">
<div class="icon">📦</div>
<h1>No Dependencies Found</h1>
<p>${projectName} v${projectVersion} has no dependencies to visualize.</p>
</div>
</body>
</html>`;
}
/**
* Build hierarchy structure from flat nodes/links
*/
function buildHierarchy(nodes, links) {
if (!Array.isArray(nodes) || nodes.length === 0) return null;
const nodeMap = new Map();
nodes.forEach(n => nodeMap.set(n.id, { ...n, children: [] }));
const root = nodes.find(n => n.type === 'root' || n.depth === 0) || nodes[0];
const childrenAdded = new Set();
links.forEach(link => {
const sourceId = typeof link.source === 'object' ? link.source.id : link.source;
const targetId = typeof link.target === 'object' ? link.target.id : link.target;
const parent = nodeMap.get(sourceId);
const child = nodeMap.get(targetId);
if (parent && child && !childrenAdded.has(targetId)) {
parent.children.push(child);
childrenAdded.add(targetId);
}
});
return nodeMap.get(root.id);
}
/**
* Create tree layout D3 script (for backward compatibility)
*/
function createTreeLayout(graphData, options = {}) {
return generateTreeLayoutHTML(graphData, options);
}
module.exports = {
createTreeLayout,
generateTreeLayoutHTML,
buildHierarchy
};
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevCompass v3.1.6 - Dependency Graph</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #0a0e1a;
color: #e0e6ed;
overflow: hidden;
}
/* ========== HEADER ========== */
.header {
background: linear-gradient(135deg, #1a1f35 0%, #0f1219 100%);
border-bottom: 1px solid #2a3142;
padding: 1rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
position: relative;
z-index: 1000;
}
.header h1 {
font-size: 1.25rem;
font-weight: 600;
color: #fff;
display: flex;
align-items: center;
gap: 0.5rem;
}
.header h1 .icon {
font-size: 1.5rem;
}
.version-badge {
font-size: 0.7rem;
background: #3b82f6;
padding: 0.15rem 0.5rem;
border-radius: 4px;
font-weight: 500;
}
/* ========== CONTROLS PANEL ========== */
.controls {
display: flex;
gap: 1.5rem;
align-items: center;
flex-wrap: wrap;
}
.control-group {
display: flex;
gap: 0.5rem;
align-items: center;
}
.control-label {
font-size: 0.85rem;
color: #8b92a7;
font-weight: 500;
}
.btn-group {
display: flex;
gap: 0.25rem;
background: #1a1f35;
border-radius: 6px;
padding: 3px;
}
.btn {
padding: 0.4rem 0.9rem;
border: none;
background: transparent;
color: #8b92a7;
font-size: 0.85rem;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
font-weight: 500;
}
.btn:hover {
background: #252b42;
color: #fff;
}
.btn.active {
background: #3b82f6;
color: #fff;
}
.slider-container {
display: flex;
align-items: center;
gap: 0.5rem;
}
input[type="range"] {
width: 100px;
height: 4px;
background: #1a1f35;
border-radius: 2px;
outline: none;
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
background: #3b82f6;
border-radius: 50%;
cursor: pointer;
}
input[type="range"]::-moz-range-thumb {
width: 14px;
height: 14px;
background: #3b82f6;
border-radius: 50%;
cursor: pointer;
border: none;
}
.depth-value {
min-width: 30px;
text-align: center;
font-size: 0.85rem;
color: #3b82f6;
font-weight: 600;
}
/* ========== SEARCH ========== */
.search-container {
position: relative;
}
.search-input {
padding: 0.5rem 1rem;
background: #1a1f35;
border: 1px solid #2a3142;
border-radius: 6px;
color: #e0e6ed;
font-size: 0.85rem;
width: 200px;
outline: none;
transition: all 0.2s;
}
.search-input:focus {
border-color: #3b82f6;
background: #252b42;
}
/* ========== MAIN CONTAINER ========== */
.container {
display: flex;
height: calc(100vh - 80px);
}
/* ========== GRAPH AREA ========== */
.graph-container {
flex: 1;
position: relative;
overflow: hidden;
}
#graph {
width: 100%;
height: 100%;
}
/* ========== SIDEBAR ========== */
.sidebar {
width: 320px;
background: #0f1219;
border-left: 1px solid #2a3142;
overflow-y: auto;
padding: 1.5rem;
}
.sidebar h2 {
font-size: 1rem;
color: #fff;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #2a3142;
}
/* ========== CLUSTERING SECTION ========== */
.cluster-section {
margin-bottom: 1.5rem;
background: #1a1f35;
border-radius: 8px;
padding: 1rem;
border: 1px solid #2a3142;
}
.cluster-mode-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.cluster-mode-btn {
padding: 0.6rem;
background: #252b42;
border: 1px solid #2a3142;
color: #8b92a7;
font-size: 0.8rem;
cursor: pointer;
border-radius: 5px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
font-weight: 500;
}
.cluster-mode-btn:hover {
background: #2d3548;
border-color: #3b82f6;
}
.cluster-mode-btn.active {
background: #3b82f6;
border-color: #3b82f6;
color: #fff;
}
.cluster-list {
max-height: 300px;
overflow-y: auto;
margin-top: 0.75rem;
}
.cluster-item {
background: #252b42;
border: 1px solid #2a3142;
border-radius: 6px;
padding: 0.75rem;
margin-bottom: 0.5rem;
cursor: pointer;
transition: all 0.2s;
}
.cluster-item:hover {
background: #2d3548;
border-color: #3b82f6;
transform: translateX(-2px);
}
.cluster-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.cluster-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
font-weight: 600;
color: #e0e6ed;
}
.cluster-icon {
font-size: 1rem;
}
.cluster-count {
background: rgba(59, 130, 246, 0.2);
color: #60a5fa;
padding: 0.15rem 0.5rem;
border-radius: 10px;
font-size: 0.75rem;
font-weight: 600;
}
.cluster-stats {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
font-size: 0.75rem;
}
.cluster-badge {
padding: 0.15rem 0.4rem;
border-radius: 4px;
font-weight: 500;
}
.cluster-badge.vulnerable {
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
}
.cluster-badge.deprecated {
background: rgba(139, 92, 246, 0.2);
color: #c4b5fd;
}
.cluster-badge.outdated {
background: rgba(245, 158, 11, 0.2);
color: #fcd34d;
}
.cluster-badge.healthy {
background: rgba(16, 185, 129, 0.2);
color: #6ee7b7;
}
/* ========== STATS ========== */
.stat-grid {
display: grid;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.stat-item {
background: #1a1f35;
padding: 0.75rem;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
}
.stat-label {
font-size: 0.85rem;
color: #8b92a7;
}
.stat-value {
font-size: 1.25rem;
font-weight: 600;
color: #fff;
}
.stat-value.danger { color: #ef4444; }
.stat-value.warning { color: #f59e0b; }
.stat-value.success { color: #10b981; }
.stat-value.cluster { color: #3b82f6; }
/* ========== LEGEND ========== */
.legend {
margin-top: 1rem;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0;
font-size: 0.85rem;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 3px;
}
/* ========== CONTROL BUTTONS ========== */
.control-btn {
width: 100%;
padding: 0.6rem 1rem;
background: #1a1f35;
border: 1px solid #2a3142;
border-radius: 6px;
color: #e0e6ed;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
text-align: left;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
gap: 0.5rem;
}
.control-btn:hover {
background: #252b42;
border-color: #3b82f6;
color: #fff;
}
.control-btn:active {
transform: scale(0.98);
background: #3b82f6;
}
/* ========== TOOLTIP ========== */
.tooltip {
position: absolute;
background: rgba(15, 18, 25, 0.95);
border: 1px solid #3b82f6;
border-radius: 8px;
padding: 1rem;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
max-width: 350px;
z-index: 10000;
backdrop-filter: blur(10px);
}
.tooltip.visible {
opacity: 1;
}
.tooltip-title {
font-size: 1rem;
font-weight: 600;
color: #fff;
margin-bottom: 0.5rem;
}
.tooltip-content {
font-size: 0.85rem;
color: #8b92a7;
line-height: 1.5;
}
.tooltip-badge {
display: inline-block;
padding: 0.15rem 0.5rem;
background: #ef4444;
color: #fff;
font-size: 0.75rem;
border-radius: 4px;
margin-right: 0.25rem;
font-weight: 500;
}
.tooltip-badge.warning { background: #f59e0b; }
.tooltip-badge.info { background: #3b82f6; }
.tooltip-badge.deprecated { background: #8b5cf6; }
/* ========== GRAPH STYLES ========== */
.node {
cursor: pointer;
transition: all 0.2s;
}
.node:hover {
filter: brightness(1.3);
}
.node-circle {
stroke-width: 2px;
transition: all 0.2s;
}
.node-label {
font-size: 11px;
fill: #e0e6ed;
pointer-events: none;
text-anchor: middle;
font-weight: 500;
}
.link {
stroke: #2a3142;
stroke-opacity: 0.6;
fill: none;
stroke-width: 1.5px;
}
.link.circular {
stroke: #ef4444;
stroke-dasharray: 5, 5;
stroke-opacity: 0.8;
}
/* ========== LOADING ========== */
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
display: none;
}
.loading.visible {
display: block;
}
.spinner {
width: 50px;
height: 50px;
border: 3px solid #2a3142;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ========== RESPONSIVE ========== */
@media (max-width: 1024px) {
.sidebar {
width: 280px;
}
.controls {
gap: 1rem;
}
}
@media (max-width: 768px) {
.header {
flex-direction: column;
height: auto;
gap: 1rem;
}
.controls {
width: 100%;
justify-content: flex-start;
}
.sidebar {
display: none;
}
}
/* ========== SCROLLBAR ========== */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #1a1f35;
}
::-webkit-scrollbar-thumb {
background: #2a3142;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #3b82f6;
}
</style>
</head>
<body>
<!-- Header -->
<div class="header">
<h1>
<span class="icon">📊</span>
DevCompass - Dependency Graph
<span class="version-badge">v3.1.6</span>
</h1>
<!-- Controls -->
<div class="controls">
<!-- Layout Switcher -->
<div class="control-group">
<span class="control-label">Layout:</span>
<div class="btn-group">
<button class="btn active" data-layout="tree">Tree</button>
<button class="btn" data-layout="force">Force</button>
<button class="btn" data-layout="radial">Radial</button>
<button class="btn" data-layout="conflict">Conflict</button>
</div>
</div>
<!-- Filter Switcher -->
<div class="control-group">
<span class="control-label">Filter:</span>
<div class="btn-group">
<button class="btn active" data-filter="all">All</button>
<button class="btn" data-filter="vulnerable">Vulnerable</button>
<button class="btn" data-filter="outdated">Outdated</button>
<button class="btn" data-filter="deprecated">Deprecated</button>
<button class="btn" data-filter="unused">Unused</button>
</div>
</div>
<!-- Depth Slider -->
<div class="control-group slider-container">
<span class="control-label">Depth:</span>
<input type="range" id="depthSlider" min="1" max="10" value="10">
<span class="depth-value" id="depthValue">∞</span>
</div>
<!-- Search -->
<div class="search-container">
<input type="text" class="search-input" placeholder="Search packages..." id="searchInput">
</div>
</div>
</div>
<!-- Main Container -->
<div class="container">
<!-- Graph -->
<div class="graph-container">
<svg id="graph"></svg>
<div class="loading" id="loading">
<div class="spinner"></div>
<div>Rendering graph...</div>
</div>
</div>
<!-- Sidebar -->
<div class="sidebar">
<!-- Clustering Section -->
<h2>🔲 Clustering</h2>
<div class="cluster-section">
<div class="cluster-mode-grid">
<button class="cluster-mode-btn active" data-cluster="ecosystem">
⚛️ Ecosystem
</button>
<button class="cluster-mode-btn" data-cluster="health">
🏥 Health
</button>
<button class="cluster-mode-btn" data-cluster="depth">
📊 Depth
</button>
</div>
<div class="cluster-list" id="clusterList">
<!-- Populated by JS -->
</div>
</div>
<!-- Statistics -->
<h2>📊 Statistics</h2>
<div class="stat-grid" id="stats">
<!-- Populated by JS -->
</div>
<!-- Legend -->
<h2>🎨 Legend</h2>
<div class="legend">
<div class="legend-item">
<div class="legend-color" style="background: #10b981;"></div>
<span>Healthy</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #f59e0b;"></div>
<span>Outdated</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #ef4444;"></div>
<span>Vulnerable</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #8b5cf6;"></div>
<span>Deprecated</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #6b7280;"></div>
<span>Unused</span>
</div>
</div>
<!-- Controls -->
<h2>⚙️ Controls</h2>
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
<button class="control-btn" onclick="fitToScreen()">⛶ Fit to Screen</button>
<button class="control-btn" onclick="zoomIn()">🔍+ Zoom In</button>
<button class="control-btn" onclick="zoomOut()">🔍− Zoom Out</button>
<button class="control-btn" onclick="resetZoom()">⟲ Reset Zoom</button>
<button class="control-btn" onclick="centerGraph()">⊙ Center View</button>
<hr style="border: none; border-top: 1px solid #2a3142; margin: 0.5rem 0;">
<button class="control-btn" onclick="exportPNG()">📸 Save as PNG</button>
<button class="control-btn" onclick="exportJSON()">💾 Save as JSON</button>
<button class="control-btn" onclick="toggleFullscreen()">🖵 Fullscreen</button>
</div>
</div>
</div>
<!-- Tooltip -->
<div class="tooltip" id="tooltip"></div>
<script>
// Graph data will be injected here
// @ts-nocheck
const graphData = {{GRAPH_DATA}};
const metadata = graphData.metadata || {};
// Clustering code will be injected here
{{CLUSTERING_CODE}}
// State
let currentLayout = metadata.defaultLayout || 'tree';
let currentFilter = metadata.defaultFilter || 'all';
let currentDepth = metadata.defaultDepth || 10;
let currentClusterMode = 'ecosystem'; // For sidebar organization only
let searchTerm = '';
let currentZoom = null;
let currentSvg = null;
let currentG = null;
let clusterer = null;
let clusters = [];
// Initialize
document.addEventListener('DOMContentLoaded', () => {
initializeClustering();
initializeControls();
renderGraph();
updateStats();
});
// Initialize clustering (sidebar only - doesn't affect graph)
function initializeClustering() {
if (typeof DependencyClusterer !== 'undefined') {
clusterer = new DependencyClusterer(graphData.nodes, graphData.links);
updateClusters();
}
}
// Update clusters based on mode (sidebar organization only)
function updateClusters() {
if (!clusterer) {
clusters = [];
renderClusterList();
return;
}
clusters = clusterer.clusterBy(currentClusterMode);
renderClusterList();
}
// Render cluster list (sidebar stats/navigation)
function renderClusterList() {
const container = document.getElementById('clusterList');
if (!container) return;
if (clusters.length === 0) {
container.innerHTML = '<div style="text-align: center; color: #8b92a7; font-size: 0.8rem; padding: 1rem;">No clusters</div>';
return;
}
container.innerHTML = '';
clusters.forEach(cluster => {
const item = document.createElement('div');
item.className = 'cluster-item';
item.innerHTML = `
<div class="cluster-header">
<div class="cluster-title">
<span class="cluster-icon">${cluster.icon}</span>
<span>${cluster.name}</span>
</div>
<span class="cluster-count">${cluster.stats.total}</span>
</div>
<div class="cluster-stats">
${cluster.stats.vulnerable > 0 ? `<span class="cluster-badge vulnerable">🔴 ${cluster.stats.vulnerable}</span>` : ''}
${cluster.stats.deprecated > 0 ? `<span class="cluster-badge deprecated">🟣 ${cluster.stats.deprecated}</span>` : ''}
${cluster.stats.outdated > 0 ? `<span class="cluster-badge outdated">🟡 ${cluster.stats.outdated}</span>` : ''}
${cluster.stats.healthy > 0 ? `<span class="cluster-badge healthy">🟢 ${cluster.stats.healthy}</span>` : ''}
</div>
`;
// Click to highlight related packages in graph
item.onclick = () => highlightCluster(cluster);
container.appendChild(item);
});
}
// Highlight packages from a cluster (visual emphasis only - 3 second fade)
function highlightCluster(cluster) {
const clusterNodeIds = new Set(cluster.nodes.map(n => n.id));
// Update visual emphasis on graph nodes
if (currentSvg) {
currentSvg.selectAll('.node')
.transition()
.duration(300)
.style('opacity', d => {
const nodeId = d.data ? d.data.id : d.id;
return clusterNodeIds.has(nodeId) ? 1 : 0.3;
});
currentSvg.selectAll('.link')
.transition()
.duration(300)
.style('opacity', 0.2);
// Reset after 3 seconds
setTimeout(() => {
currentSvg.selectAll('.node')
.transition()
.duration(500)
.style('opacity', 1);
currentSvg.selectAll('.link')
.transition()
.duration(500)
.style('opacity', 0.6);
}, 3000);
}
}
// Get visible nodes (NO clustering - just normal filtering)
function getVisibleNodes() {
let nodes = graphData.nodes;
// Apply depth filter
const maxDepth = currentDepth === 10 ? Infinity : currentDepth;
nodes = nodes.filter(n => (n.depth || 0) <= maxDepth);
// Apply category filter
if (currentFilter !== 'all') {
nodes = nodes.filter(n => {
if (n.type === 'root') return true;
switch(currentFilter) {
case 'vulnerable': return n.isVulnerable;
case 'outdated': return n.isOutdated;
case 'deprecated': return n.isDeprecated;
case 'unused': return n.isUnused;
default: return true;
}
});
}
// Apply search filter
if (searchTerm) {
nodes = nodes.filter(n =>
n.name.toLowerCase().includes(searchTerm)
);
}
return nodes;
}
// Control handlers
function initializeControls() {
// Layout buttons
document.querySelectorAll('[data-layout]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-layout]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentLayout = btn.dataset.layout;
renderGraph();
});
});
// Filter buttons
document.querySelectorAll('[data-filter]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-filter]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentFilter = btn.dataset.filter;
renderGraph();
});
});
// Cluster mode buttons (sidebar organization only)
document.querySelectorAll('[data-cluster]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-cluster]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentClusterMode = btn.dataset.cluster;
updateClusters(); // Updates sidebar list only
updateStats();
});
});
// Depth slider
const slider = document.getElementById('depthSlider');
const value = document.getElementById('depthValue');
slider.addEventListener('input', (e) => {
currentDepth = parseInt(e.target.value);
value.textContent = currentDepth === 10 ? '∞' : currentDepth;
renderGraph();
});
// Search
document.getElementById('searchInput').addEventListener('input', (e) => {
searchTerm = e.target.value.toLowerCase();
renderGraph();
});
}
// Filter nodes
function filterNodes(nodes) {
return getVisibleNodes();
}
// Render graph (normal visualization - no cluster nodes)
function renderGraph() {
const loading = document.getElementById('loading');
loading.classList.add('visible');
setTimeout(() => {
const svg = d3.select('#graph');
svg.selectAll('*').remove();
const filteredNodes = filterNodes(graphData.nodes);
const nodeIds = new Set(filteredNodes.map(n => n.id));
const filteredLinks = graphData.links.filter(l => {
const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
const targetId = typeof l.target === 'object' ? l.target.id : l.target;
return nodeIds.has(sourceId) && nodeIds.has(targetId);
});
// Render based on layout
switch(currentLayout) {
case 'tree': renderTreeLayout(svg, filteredNodes, filteredLinks); break;
case 'force': renderForceLayout(svg, filteredNodes, filteredLinks); break;
case 'radial': renderRadialLayout(svg, filteredNodes, filteredLinks); break;
case 'conflict': renderConflictLayout(svg, filteredNodes, filteredLinks); break;
}
loading.classList.remove('visible');
updateStats();
}, 100);
}
// Layout implementations (unchanged - normal layouts)
function renderTreeLayout(svg, nodes, links) {
const width = window.innerWidth - 320;
const height = window.innerHeight - 80;
svg.attr('width', width).attr('height', height);
const g = svg.append('g');
const root = buildHierarchy(nodes, links);
if (!root) return;
const tree = d3.tree()
.size([width - 200, height - 200])
.separation((a, b) => (a.parent === b.parent ? 1.5 : 2));
tree(root);
// Links
g.selectAll('.link')
.data(root.links())
.enter().append('path')
.attr('class', 'link')
.attr('d', d3.linkVertical()
.x(d => d.x + 100)
.y(d => d.y + 100));
// Nodes
const node = g.selectAll('.node')
.data(root.descendants())
.enter().append('g')
.attr('class', 'node')
.attr('transform', d => `translate(${d.x + 100},${d.y + 100})`);
node.append('circle')
.attr('class', 'node-circle')
.attr('r', 6)
.attr('fill', d => getNodeColor(d.data))
.attr('stroke', d => getNodeStroke(d.data))
.on('mouseover', showTooltip)
.on('mouseout', hideTooltip);
node.append('text')
.attr('class', 'node-label')
.attr('dy', d => d.children ? -12 : 15)
.attr('text-anchor', 'middle')
.text(d => {
const name = d.data.name;
return name.length > 15 ? name.substring(0, 15) + '...' : name;
})
.style('font-size', '10px')
.style('pointer-events', 'none');
addZoom(svg, g);
setTimeout(() => centerGraph(), 100);
}
function renderForceLayout(svg, nodes, links) {
const width = window.innerWidth - 320;
const height = window.innerHeight - 80;
svg.attr('width', width).attr('height', height);
const g = svg.append('g');
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links).id(d => d.id).distance(100))
.force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(15));
const link = g.selectAll('.link')
.data(links)
.enter().append('line')
.attr('class', 'link');
const node = g.selectAll('.node')
.data(nodes)
.enter().append('g')
.attr('class', 'node')
.call(d3.drag()
.on('start', dragStart)
.on('drag', dragging)
.on('end', dragEnd));
node.append('circle')
.attr('class', 'node-circle')
.attr('r', 8)
.attr('fill', getNodeColor)
.attr('stroke', getNodeStroke)
.on('mouseover', showTooltip)
.on('mouseout', hideTooltip);
node.append('text')
.attr('class', 'node-label')
.attr('dy', -12)
.text(d => {
const name = d.name;
return name.length > 15 ? name.substring(0, 15) + '...' : name;
});
simulation.on('tick', () => {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
node.attr('transform', d => `translate(${d.x},${d.y})`);
});
addZoom(svg, g);
function dragStart(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragging(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragEnd(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
}
function renderRadialLayout(svg, nodes, links) {
const width = window.innerWidth - 320;
const height = window.innerHeight - 80;
const radius = Math.min(width, height) / 2 - 100;
svg.attr('width', width).attr('height', height);
const g = svg.append('g').attr('transform', `translate(${width/2},${height/2})`);
const root = buildHierarchy(nodes, links);
if (!root) return;
const tree = d3.cluster().size([2 * Math.PI, radius]);
tree(root);
// Links
g.selectAll('.link')
.data(root.links())
.enter().append('path')
.attr('class', 'link')
.attr('d', d3.linkRadial()
.angle(d => d.x)
.radius(d => d.y));
// Nodes
const node = g.selectAll('.node')
.data(root.descendants())
.enter().append('g')
.attr('class', 'node')
.attr('transform', d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0)`);
node.append('circle')
.attr('class', 'node-circle')
.attr('r', 6)
.attr('fill', d => getNodeColor(d.data))
.attr('stroke', d => getNodeStroke(d.data))
.on('mouseover', showTooltip)
.on('mouseout', hideTooltip);
node.append('text')
.attr('class', 'node-label')
.attr('dy', -10)
.attr('transform', d => d.x >= Math.PI ? 'rotate(180)' : null)
.text(d => {
const name = d.data.name;
return name.length > 12 ? name.substring(0, 12) + '...' : name;
});
addZoom(svg, g);
}
function renderConflictLayout(svg, nodes, links) {
const conflictNodes = nodes.filter(n =>
n.type === 'root' || n.isVulnerable || n.isDeprecated || n.isOutdated
);
const nodeIds = new Set(conflictNodes.map(n => n.id));
const conflictLinks = links.filter(l =>
nodeIds.has(typeof l.source === 'object' ? l.source.id : l.source) &&
nodeIds.has(typeof l.target === 'object' ? l.target.id : l.target)
);
renderForceLayout(svg, conflictNodes, conflictLinks);
}
// Utilities
function buildHierarchy(nodes, links) {
const root = nodes.find(n => n.type === 'root');
if (!root) return null;
try {
return d3.stratify()
.id(d => d.id)
.parentId(d => {
const link = links.find(l => {
const targetId = typeof l.target === 'object' ? l.target.id : l.target;
return targetId === d.id;
});
return link ? (typeof link.source === 'object' ? link.source.id : link.source) : null;
})(nodes);
} catch (e) {
console.error('Hierarchy build failed:', e);
return null;
}
}
function getNodeColor(node) {
if (node.type === 'root') return '#3b82f6';
if (node.isVulnerable) return '#ef4444';
if (node.isDeprecated) return '#8b5cf6';
if (node.isOutdated) return '#f59e0b';
if (node.isUnused) return '#6b7280';
return '#10b981';
}
function getNodeStroke(node) {
if (node.type === 'root') return '#2563eb';
return '#1a1f35';
}
function showTooltip(event, d) {
const data = d.data || d;
const tooltip = document.getElementById('tooltip');
let content = `<div class="tooltip-title">${data.name}@${data.version || 'unknown'}</div>`;
content += `<div class="tooltip-content">`;
if (data.isVulnerable) content += `<span class="tooltip-badge">Vulnerable</span>`;
if (data.isDeprecated) content += `<span class="tooltip-badge deprecated">Deprecated</span>`;
if (data.isOutdated) content += `<span class="tooltip-badge warning">Outdated</span>`;
if (data.isUnused) content += `<span class="tooltip-badge info">Unused</span>`;
content += `<br>Depth: ${data.depth || 0}`;
if (data.healthScore) content += `<br>Health: ${data.healthScore}/10`;
content += `</div>`;
tooltip.innerHTML = content;
tooltip.style.left = (event.pageX + 10) + 'px';
tooltip.style.top = (event.pageY + 10) + 'px';
tooltip.classList.add('visible');
}
function hideTooltip() {
document.getElementById('tooltip').classList.remove('visible');
}
function addZoom(svg, g) {
const zoom = d3.zoom()
.scaleExtent([0.1, 4])
.on('zoom', (event) => g.attr('transform', event.transform));
svg.call(zoom);
currentZoom = zoom;
currentSvg = svg;
currentG = g;
}
// Control functions
function zoomIn() {
if (currentSvg && currentZoom) {
currentSvg.transition().duration(300).call(currentZoom.scaleBy, 1.3);
}
}
function zoomOut() {
if (currentSvg && currentZoom) {
currentSvg.transition().duration(300).call(currentZoom.scaleBy, 0.7);
}
}
function resetZoom() {
if (currentSvg && currentZoom) {
currentSvg.transition().duration(500).call(currentZoom.transform, d3.zoomIdentity);
}
}
function centerGraph() {
if (!currentSvg || !currentZoom || !currentG) return;
try {
const bbox = currentG.node().getBBox();
const containerWidth = currentSvg.node().clientWidth;
const containerHeight = currentSvg.node().clientHeight;
const padding = 50;
const scaleX = (containerWidth - padding * 2) / bbox.width;
const scaleY = (containerHeight - padding * 2) / bbox.height;
const scale = Math.min(scaleX, scaleY, 1);
const tx = (containerWidth - bbox.width * scale) / 2 - bbox.x * scale;
const ty = (containerHeight - bbox.height * scale) / 2 - bbox.y * scale;
currentSvg.transition().duration(750).call(
currentZoom.transform,
d3.zoomIdentity.translate(tx, ty).scale(scale)
);
} catch (error) {
const width = currentSvg.node().clientWidth;
const height = currentSvg.node().clientHeight;
currentSvg.transition().duration(500).call(
currentZoom.transform,
d3.zoomIdentity.translate(width / 2, height / 2).scale(1)
);
}
}
function fitToScreen() {
centerGraph();
}
function exportPNG() {
try {
const svgElement = document.getElementById('graph');
const svgData = new XMLSerializer().serializeToString(svgElement);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
canvas.width = svgElement.clientWidth;
canvas.height = svgElement.clientHeight;
img.onload = function() {
ctx.fillStyle = '#0a0e1a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
canvas.toBlob(function(blob) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'devcompass-graph-' + currentLayout + '-' + currentFilter + '.png';
a.click();
URL.revokeObjectURL(url);
});
};
img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData)));
} catch (error) {
alert('PNG export requires the graph to be fully loaded. Please try again.');
}
}
function exportJSON() {
const filtered = filterNodes(graphData.nodes);
const nodeIds = new Set(filtered.map(n => n.id));
const filteredLinks = graphData.links.filter(l => {
const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
const targetId = typeof l.target === 'object' ? l.target.id : l.target;
return nodeIds.has(sourceId) && nodeIds.has(targetId);
});
const exportData = {
version: '3.1.6',
layout: currentLayout,
filter: currentFilter,
depth: currentDepth,
clusterMode: currentClusterMode,
nodes: filtered,
links: filteredLinks,
clusters: clusters,
metadata: graphData.metadata
};
const dataStr = JSON.stringify(exportData, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const a = document.createElement('a');
a.href = url;
a.download = 'devcompass-graph-' + currentLayout + '-' + currentClusterMode + '.json';
a.click();
URL.revokeObjectURL(url);
}
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(err => {
alert('Fullscreen not supported on this browser');
});
} else {
document.exitFullscreen();
}
}
function updateStats() {
const filtered = filterNodes(graphData.nodes);
const stats = {
total: filtered.length,
clusters: clusters.length,
vulnerable: filtered.filter(n => n.isVulnerable).length,
deprecated: filtered.filter(n => n.isDeprecated).length,
outdated: filtered.filter(n => n.isOutdated).length,
unused: filtered.filter(n => n.isUnused).length,
healthy: filtered.filter(n => !n.isVulnerable && !n.isDeprecated && !n.isOutdated && !n.isUnused).length
};
document.getElementById('stats').innerHTML = `
<div class="stat-item">
<span class="stat-label">Total</span>
<span class="stat-value">${stats.total}</span>
</div>
<div class="stat-item">
<span class="stat-label">Clusters</span>
<span class="stat-value cluster">${stats.clusters}</span>
</div>
<div class="stat-item">
<span class="stat-label">Vulnerable</span>
<span class="stat-value danger">${stats.vulnerable}</span>
</div>
<div class="stat-item">
<span class="stat-label">Deprecated</span>
<span class="stat-value warning">${stats.deprecated}</span>
</div>
<div class="stat-item">
<span class="stat-label">Outdated</span>
<span class="stat-value warning">${stats.outdated}</span>
</div>
<div class="stat-item">
<span class="stat-label">Healthy</span>
<span class="stat-value success">${stats.healthy}</span>
</div>
`;
}
</script>
</body>
</html>